From 6c7ee0a66691f9755789045b977ae7238bf4c2e4 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Thu, 29 Jan 2026 10:32:57 +0800 Subject: [PATCH 1/8] =?UTF-8?q?add:=20=E6=96=B0=E5=A2=9EWord=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=88=86=E6=9E=90=E6=9C=8D=E5=8A=A1=E5=8F=8A=E8=B7=AF?= =?UTF-8?q?=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.yaml | 2 +- go.mod | 5 + go.sum | 18 ++++ internal/biz/advice.go | 153 ++++++++++++++++++++++++++++ internal/biz/provider_set.go | 3 + internal/data/impl/provider_set.go | 3 + internal/server/router/router.go | 5 +- internal/services/advice.go | 62 +++++++++++ internal/services/dtalk_bot_test.go | 18 ++-- internal/services/provider_set.go | 1 + internal/test/bench_test.go | 108 ++++++++++++++++++++ 11 files changed, 370 insertions(+), 8 deletions(-) create mode 100644 internal/biz/advice.go create mode 100644 internal/services/advice.go create mode 100644 internal/test/bench_test.go diff --git a/config/config.yaml b/config/config.yaml index 6166634..cc45e36 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -146,7 +146,7 @@ eino_tools: # == 通用工具 == # 表格转图片 excel2pic: - base_url: "http://excel2pic:8000/api/v1/convert" + base_url: "http://192.168.6.109:8010/api/v1/convert" dingtalk: api_key: "dingsbbntrkeiyazcfdg" diff --git a/go.mod b/go.mod index 095f736..ac6aa2a 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,8 @@ require ( github.com/spf13/viper v1.17.0 github.com/stretchr/testify v1.11.1 github.com/tmc/langchaingo v0.1.13 + github.com/unidoc/unioffice v1.39.0 + github.com/volcengine/volcengine-go-sdk v1.2.9 github.com/xuri/excelize/v2 v2.10.0 golang.org/x/sync v0.17.0 google.golang.org/grpc v1.64.0 @@ -75,6 +77,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect @@ -112,6 +115,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + github.com/volcengine/volc-sdk-golang v1.0.23 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xuri/efp v0.0.1 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect @@ -128,5 +132,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7d1f8b1..1c08769 100644 --- a/go.sum +++ b/go.sum @@ -101,6 +101,7 @@ github.com/aliyun/credentials-go v1.4.6 h1:CG8rc/nxCNKfXbZWpWDzI9GjF4Tuu3Es14qT8 github.com/aliyun/credentials-go v1.4.6/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= @@ -237,6 +238,7 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -248,6 +250,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -266,6 +269,7 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= @@ -293,6 +297,10 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -310,6 +318,7 @@ github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -443,12 +452,18 @@ github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1Ca github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/unidoc/unioffice v1.39.0 h1:Wo5zvrzCqhyK/1Zi5dg8a5F5+NRftIMZPnFPYwruLto= +github.com/unidoc/unioffice v1.39.0/go.mod h1:Axz6ltIZZTUUyHoEnPe4Mb3VmsN4TRHT5iZCGZ1rgnU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8= +github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU= +github.com/volcengine/volcengine-go-sdk v1.2.9 h1:du2gnImtyWXKkQFnJW/GXCs+UBibGGOXIbP1Ams2pB8= +github.com/volcengine/volcengine-go-sdk v1.2.9/go.mod h1:oxoVo+A17kvkwPkIeIHPVLjSw7EQAm+l/Vau1YGHN+A= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= @@ -839,11 +854,14 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a/go.mod h1:KF9sEfUPAXdG8Oev9e99iLGnl2uJMjc5B+4y3O7x610= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/biz/advice.go b/internal/biz/advice.go new file mode 100644 index 0000000..5c2a93a --- /dev/null +++ b/internal/biz/advice.go @@ -0,0 +1,153 @@ +package biz + +import ( + "ai_scheduler/internal/biz/llm_service/third_party" + "ai_scheduler/internal/entitys" + "context" + "encoding/json" + "fmt" + "os" + + "strings" + + "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" + "github.com/volcengine/volcengine-go-sdk/volcengine" +) + +type AdviceBiz struct { + hsyq *third_party.Hsyq +} + +func NewAdviceBiz(hsyq *third_party.Hsyq) *AdviceBiz { + return &AdviceBiz{ + hsyq: hsyq, + } +} + +const ( + key = "236ba4b6-9daa-4755-b22f-2fd274cd223a" + modelName = "doubao-seed-1-8-251228" +) + +var dataMap = map[string]string{ + "DialectFeatures": (&entitys.DialectFeatures{}).Example(), + "SentencePatterns": (&entitys.SentencePatterns{}).Example(), + "PersonalityTags": (&entitys.PersonalityTags{}).Example(), + "ToneTags": (&entitys.ToneTags{}).Example(), + "SignatureDialogues": (&entitys.SignatureDialogues{}).Example(), + "RegionValue": (&entitys.RegionValue{}).Example(), + "CompetitionComparison": (&entitys.CompetitionComparison{}).Example(), + "CoreSellingPoints": (&entitys.CoreSellingPoints{}).Example(), + "SupportingFacilities": (&entitys.SupportingFacilities{}).Example(), + "DeveloperBacking": (&entitys.DeveloperBacking{}).Example(), + "NeedsMining": (&entitys.NeedsMining{}).Example(), + "PainPointResponse": (&entitys.PainPointResponse{}).Example(), + "ValueBuilding": (&entitys.ValueBuilding{}).Example(), + "ClosingTechniques": (&entitys.ClosingTechniques{}).Example(), + "CommunicationRhythm": (&entitys.CommunicationRhythm{}).Example(), +} + +func (a *AdviceBiz) WordAna(ctx context.Context, wordContent string) error { + examples := a.getAllExamples() + prompt := a.buildSimplePrompt(wordContent, examples) + anaContent, err := a.callLlm(ctx, prompt) + if err != nil { + return err + } + data := a.parseResponse(anaContent) + jsonData, _ := json.MarshalIndent(data, "", " ") + os.WriteFile("extracted.json", jsonData, 0644) + fmt.Println("✅ 数据已保存到 extracted.json") + return nil +} + +func (a *AdviceBiz) callLlm(ctx context.Context, prompt string) (string, error) { + var message = make([]*model.ChatCompletionMessage, 1) + message[0] = &model.ChatCompletionMessage{ + Role: model.ChatMessageRoleSystem, + Content: &model.ChatCompletionMessageContent{ + StringValue: volcengine.String(prompt), + }, + } + + res, err := a.hsyq.RequestHsyq(ctx, key, modelName, message) + if err != nil { + return "", err + } + return *res.Choices[0].Message.Content.StringValue, nil +} + +func (a *AdviceBiz) getAllExamples() map[string]string { + return dataMap +} + +func (a *AdviceBiz) buildSimplePrompt(wordContent string, examples map[string]string) string { + // 最简单的提示词模板 + template := `分析以下房地产销售对话,按指定格式提取信息: + +对话内容: +%s + +请按照以下` + fmt.Sprintf("%d", len(examples)) + `个格式生成JSON数据,每个格式用===分隔: + +%s + +输出要求: +1. 每个结构体一个JSON对象 +2. 严格按照示例格式 +3. 用空行分隔不同结构体` + + // 构建格式部分 + var formats strings.Builder + for name, example := range examples { + formats.WriteString(fmt.Sprintf("=== %s ===\n示例:%s\n\n", name, example)) + } + + return fmt.Sprintf(template, wordContent, formats.String()) +} + +func (a *AdviceBiz) parseResponse(response string) map[string]interface{} { + result := make(map[string]interface{}) + + // 按空行分割 + parts := strings.Split(response, "\n\n") + + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" || !strings.Contains(part, "{") { + continue + } + + // 找到第一个 { 和最后一个 } + start := strings.Index(part, "{") + end := strings.LastIndex(part, "}") + + if start == -1 || end == -1 || end <= start { + continue + } + + jsonStr := part[start : end+1] + + // 尝试解析 + var data interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err == nil { + // 判断是什么结构体 + for _, name := range getStructNames() { + if strings.Contains(jsonStr, `"`+name+`"`) || strings.Contains(part, name) { + result[name] = data + break + } + } + } + } + + return result +} + +func getStructNames() []string { + var res = make([]string, 0, len(dataMap)) + for k, _ := range dataMap { + res = append(res, k) + } + return res +} diff --git a/internal/biz/provider_set.go b/internal/biz/provider_set.go index 6f95898..12eb9c3 100644 --- a/internal/biz/provider_set.go +++ b/internal/biz/provider_set.go @@ -3,6 +3,7 @@ package biz import ( "ai_scheduler/internal/biz/do" "ai_scheduler/internal/biz/llm_service" + "ai_scheduler/internal/biz/llm_service/third_party" "github.com/google/wire" ) @@ -21,4 +22,6 @@ var ProviderSetBiz = wire.NewSet( NewQywxAppBiz, NewGroupConfigBiz, do.NewMacro, + NewAdviceBiz, + third_party.NewHsyq, ) diff --git a/internal/data/impl/provider_set.go b/internal/data/impl/provider_set.go index c200234..9c34713 100644 --- a/internal/data/impl/provider_set.go +++ b/internal/data/impl/provider_set.go @@ -18,4 +18,7 @@ var ProviderImpl = wire.NewSet( NewBotGroupConfigImpl, NewBotGroupQywxImpl, NewReportDailyCacheImpl, + NewAdviceAdvicerImplImpl, + NewAdviceProjectImpl, + NewAdviceTalkImpl, ) diff --git a/internal/server/router/router.go b/internal/server/router/router.go index 17fff12..843a8a7 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -26,7 +26,7 @@ type RouterServer struct { // SetupRoutes 设置路由 func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionService *services.SessionService, task *services.TaskService, gateway *gateway.Gateway, callbackService *services.CallbackService, chatHist *services.HistoryService, - capabilityService *services.CapabilityService, + capabilityService *services.CapabilityService, advice *services.AdviceService, ) { app.Use(func(c *fiber.Ctx) error { // 设置 CORS 头 @@ -98,6 +98,9 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi // 能力 r.Post("/capability/product/ingest", capabilityService.ProductIngest) // 商品数据提取 r.Post("/capability/product/ingest/:thread_id/confirm", capabilityService.ProductIngestConfirm) // 商品数据提取确认 + + advicer := r.Group("advice/") + advicer.Post("file/word", advice.WordAna) } func routerSocket(app *fiber.App, chatService *services.ChatService) { diff --git a/internal/services/advice.go b/internal/services/advice.go new file mode 100644 index 0000000..9ac7352 --- /dev/null +++ b/internal/services/advice.go @@ -0,0 +1,62 @@ +package services + +import ( + "ai_scheduler/internal/biz" + "ai_scheduler/internal/config" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg/file_download" + "context" + "errors" + + "net/url" + + "github.com/gofiber/fiber/v2" +) + +// ChatHandler 聊天处理器 +type AdviceService struct { + adviceBiz *biz.AdviceBiz + cfg *config.Config +} + +// NewChatHandler 创建聊天处理器 +func NewAdviceService( + adviceBiz *biz.AdviceBiz, + cfg *config.Config, +) *AdviceService { + return &AdviceService{ + adviceBiz: adviceBiz, + cfg: cfg, + } +} + +func (a *AdviceService) WordAna(c *fiber.Ctx) error { + req := &entitys.WordAnaReq{} + if err := c.BodyParser(req); err != nil { + return err + } + // URL 解码 + fileURL, err := url.PathUnescape(req.WordFileUrl) + if err != nil { + return errors.New("URL 解码失败") + } + result, _, err := file_download.GetWordTextFromURL(fileURL, file_download.IsWordFile) + if err != nil { + return err + } + return a.adviceBiz.WordAna(c.UserContext(), result) +} + +func (a *AdviceService) WordAnat(path string) error { + + // URL 解码 + fileURL, err := url.PathUnescape(path) + if err != nil { + return errors.New("URL 解码失败") + } + result, _, err := file_download.GetWordTextFromURL(fileURL, file_download.IsWordFile) + if err != nil { + return err + } + return a.adviceBiz.WordAna(context.Background(), result) +} diff --git a/internal/services/dtalk_bot_test.go b/internal/services/dtalk_bot_test.go index 37c206e..8df3265 100644 --- a/internal/services/dtalk_bot_test.go +++ b/internal/services/dtalk_bot_test.go @@ -6,6 +6,7 @@ import ( dingtalk2 "ai_scheduler/internal/biz/handle/dingtalk" "ai_scheduler/internal/biz/handle/qywx" "ai_scheduler/internal/biz/llm_service" + "ai_scheduler/internal/biz/llm_service/third_party" "ai_scheduler/internal/biz/tools_regis" "ai_scheduler/internal/config" "ai_scheduler/internal/data/impl" @@ -29,13 +30,13 @@ import ( ) func Test_Report(t *testing.T) { - run() + Run() a := cronService.CronReportSendDingTalk(context.Background()) t.Log(a) } func Test_Report_QYWX(t *testing.T) { - run() + Run() a := cronService.CronReportSendQywx(context.Background()) t.Log(a) } @@ -48,7 +49,7 @@ var ( ) // run 函数是程序的入口函数,负责初始化和配置各个组件 -func run() { +func Run() { // 加载测试配置 // configConfig, err = config.LoadConfigWithTest() configConfig, err = config.LoadConfigWithEnv() @@ -62,6 +63,7 @@ func run() { botConfigImpl := impl.NewBotConfigImpl(db) botGroupImpl := impl.NewBotGroupImpl(db) botUserImpl := impl.NewBotUserImpl(db) + reportDailyCacheImpl := impl.NewReportDailyCacheImpl(db) // 初始化Do业务对象 doDo := do.NewDo(sessionImpl, sysImpl, taskImpl, chatHisImpl, configConfig) // 初始化Ollama客户端 @@ -92,7 +94,7 @@ func run() { // 初始化Ollama服务 ollamaService := llm_service.NewOllamaGenerate(client, utils_vllmClient, configConfig, chatHisImpl) // 初始化工具管理器 - manager := tools.NewManager(configConfig, client) + manager := tools.NewManager(configConfig, client, rdb) // 初始化钉钉联系人客户端 contactClient, _ := dingtalk.NewContactClient(configConfig) // 初始化钉钉记事本客户端 @@ -117,11 +119,15 @@ func run() { botGroupConfigImpl := impl.NewBotGroupConfigImpl(db) botGroupQywxImpl := impl.NewBotGroupQywxImpl(db) qywxAuth := qywx.NewAuth(configConfig, rdb) + macro := do.NewMacro(botGroupImpl, reportDailyCacheImpl, configConfig, rdb) group := qywx.NewGroup(botGroupQywxImpl, qywxAuth) other := qywx.NewOther(qywxAuth) qywxAppBiz := biz.NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other) - groupConfigBiz := biz.NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, registry, configConfig, impl.NewReportDailyCacheImpl(db), rdb) - dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, botChatHisImpl, impl.NewReportDailyCacheImpl(db), manager, configConfig, sendCardClient, groupConfigBiz) + groupConfigBiz := biz.NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, registry, configConfig, reportDailyCacheImpl, rdb, macro, manager, handle) + dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, botChatHisImpl, reportDailyCacheImpl, manager, configConfig, sendCardClient, groupConfigBiz, macro) // 初始化钉钉机器人服务 cronService = NewCronService(configConfig, dingTalkBotBiz, qywxAppBiz, groupConfigBiz) + hsyq := third_party.NewHsyq() + advicerbiz := biz.NewAdviceBiz(hsyq) + advicer = NewAdviceService(advicerbiz, configConfig) } diff --git a/internal/services/provider_set.go b/internal/services/provider_set.go index 55eed7a..83a9373 100644 --- a/internal/services/provider_set.go +++ b/internal/services/provider_set.go @@ -15,4 +15,5 @@ var ProviderSetServices = wire.NewSet( NewHistoryService, NewCapabilityService, NewCronService, + NewAdviceService, ) diff --git a/internal/test/bench_test.go b/internal/test/bench_test.go new file mode 100644 index 0000000..cbcbad5 --- /dev/null +++ b/internal/test/bench_test.go @@ -0,0 +1,108 @@ +package test + +import ( + "runtime" + "testing" +) + +// 测试用的结构体 +type Person struct { + Name string + Age int + Address string + Email string + Phone string + Balance float64 + Active bool +} + +// 返回指针 +func NewPersonPtr(name string, age int) *Person { + return &Person{ + Name: name, + Age: age, + Address: "Some Address", + Email: "test@example.com", + Phone: "1234567890", + Balance: 1000.0, + Active: true, + } +} + +// 返回值 +func NewPersonValue(name string, age int) Person { + return Person{ + Name: name, + Age: age, + Address: "Some Address", + Email: "test@example.com", + Phone: "1234567890", + Balance: 1000.0, + Active: true, + } +} + +var globalPtr *Person +var globalValue Person + +func BenchmarkSmallStruct(b *testing.B) { + runtime.GC() + b.Run("ValueWithSmallStruct", func(b *testing.B) { + for i := 0; i < b.N; i++ { + p := NewPersonValue("John", 30) + globalValue = p + } + }) + + runtime.Gosched() + runtime.GC() + b.Run("PointerWithSmallStruct", func(b *testing.B) { + for i := 0; i < b.N; i++ { + p := NewPersonPtr("John", 30) + globalPtr = p + } + }) +} + +var globalLargePtr *LargeStruct +var globalLargeValue LargeStruct + +func BenchmarkLargeStruct(b *testing.B) { + runtime.GC() + b.Run("ValueWithLargeStruct", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s := NewLargeValue(1) + globalLargeValue = s + } + }) + + runtime.Gosched() + runtime.GC() + b.Run("PointerWithLargeStruct", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + p := NewLargePtr(1) + globalLargePtr = p + } + }) +} + +func NewLargePtr(id int) *LargeStruct { + return &LargeStruct{ + ID: id, + } +} + +// 返回值 +func NewLargeValue(id int) LargeStruct { + return LargeStruct{ + ID: id, + } +} + +// Benchmark 大结构体指针 +type LargeStruct struct { + Data [1024]byte + ID int +} From e5bbddd58d2671a83656be954332b4ed74287fe0 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Thu, 29 Jan 2026 10:34:57 +0800 Subject: [PATCH 2/8] =?UTF-8?q?add:=20=E6=96=B0=E5=A2=9EWord=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=88=86=E6=9E=90=E6=9C=8D=E5=8A=A1=E5=8F=8A=E8=B7=AF?= =?UTF-8?q?=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.yaml b/config/config.yaml index cc45e36..6166634 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -146,7 +146,7 @@ eino_tools: # == 通用工具 == # 表格转图片 excel2pic: - base_url: "http://192.168.6.109:8010/api/v1/convert" + base_url: "http://excel2pic:8000/api/v1/convert" dingtalk: api_key: "dingsbbntrkeiyazcfdg" From 27b71918653fca6c861b814d678573f94119cfd2 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Thu, 29 Jan 2026 10:58:18 +0800 Subject: [PATCH 3/8] =?UTF-8?q?add:=20=E6=96=B0=E5=A2=9EWord=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=88=86=E6=9E=90=E6=9C=8D=E5=8A=A1=E5=8F=8A=E8=B7=AF?= =?UTF-8?q?=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/llm_service/third_party/hsyq.go | 50 ++++ internal/data/impl/advice_advicer_impl.go | 18 ++ internal/data/impl/advice_project_impl.go | 18 ++ internal/data/impl/advice_talk_impl.go | 18 ++ internal/data/model/ai_advice_advicer.gen.go | 33 +++ internal/data/model/ai_advice_project.gen.go | 28 +++ internal/data/model/ai_advice_talk.gen.go | 27 ++ internal/entitys/advicer.go | 135 ++++++++++ internal/pkg/file_download/file_download.go | 246 +++++++++++++++++++ internal/services/advicer_test.go | 15 ++ 10 files changed, 588 insertions(+) create mode 100644 internal/biz/llm_service/third_party/hsyq.go create mode 100644 internal/data/impl/advice_advicer_impl.go create mode 100644 internal/data/impl/advice_project_impl.go create mode 100644 internal/data/impl/advice_talk_impl.go create mode 100644 internal/data/model/ai_advice_advicer.gen.go create mode 100644 internal/data/model/ai_advice_project.gen.go create mode 100644 internal/data/model/ai_advice_talk.gen.go create mode 100644 internal/entitys/advicer.go create mode 100644 internal/pkg/file_download/file_download.go create mode 100644 internal/services/advicer_test.go diff --git a/internal/biz/llm_service/third_party/hsyq.go b/internal/biz/llm_service/third_party/hsyq.go new file mode 100644 index 0000000..763db1d --- /dev/null +++ b/internal/biz/llm_service/third_party/hsyq.go @@ -0,0 +1,50 @@ +package third_party + +import ( + "context" + "time" + + "github.com/volcengine/volcengine-go-sdk/service/arkruntime" + "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" +) + +type Hsyq struct { + mapClient map[string]*arkruntime.Client +} + +func NewHsyq() *Hsyq { + return &Hsyq{ + mapClient: make(map[string]*arkruntime.Client), + } +} + +func (h *Hsyq) getClient(key string) *arkruntime.Client { + var client *arkruntime.Client + if _, ok := h.mapClient[key]; !ok { + client = h.mapClient[key] + } else { + client = arkruntime.NewClientWithApiKey( + key, + arkruntime.WithRegion("cn-beijing"), + arkruntime.WithTimeout(2*time.Minute), + arkruntime.WithRetryTimes(2), + ) + h.mapClient[key] = client + } + return client +} + +// 火山引擎 +func (h *Hsyq) RequestHsyq(ctx context.Context, key string, modelName string, prompt []*model.ChatCompletionMessage) (model.ChatCompletionResponse, error) { + req := model.CreateChatCompletionRequest{ + Model: modelName, + Messages: prompt, + } + + resp, err := h.getClient(key).CreateChatCompletion(ctx, req) + if err != nil { + return model.ChatCompletionResponse{ID: ""}, err + } + + return resp, err +} diff --git a/internal/data/impl/advice_advicer_impl.go b/internal/data/impl/advice_advicer_impl.go new file mode 100644 index 0000000..ffbbeaa --- /dev/null +++ b/internal/data/impl/advice_advicer_impl.go @@ -0,0 +1,18 @@ +package impl + +import ( + "ai_scheduler/internal/data/model" + "ai_scheduler/tmpl/dataTemp" + "ai_scheduler/utils" +) + +type AdviceAdvicerImpl struct { + dataTemp.DataTemp + BaseRepository[model.AiTask] +} + +func NewAdviceAdvicerImplImpl(db *utils.Db) *AdviceAdvicerImpl { + return &AdviceAdvicerImpl{ + DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceAdvicer)), + } +} diff --git a/internal/data/impl/advice_project_impl.go b/internal/data/impl/advice_project_impl.go new file mode 100644 index 0000000..ca75899 --- /dev/null +++ b/internal/data/impl/advice_project_impl.go @@ -0,0 +1,18 @@ +package impl + +import ( + "ai_scheduler/internal/data/model" + "ai_scheduler/tmpl/dataTemp" + "ai_scheduler/utils" +) + +type AdviceProjectImpl struct { + dataTemp.DataTemp + BaseRepository[model.AiTask] +} + +func NewAdviceProjectImpl(db *utils.Db) *AdviceProjectImpl { + return &AdviceProjectImpl{ + DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceProject)), + } +} diff --git a/internal/data/impl/advice_talk_impl.go b/internal/data/impl/advice_talk_impl.go new file mode 100644 index 0000000..aaf9ba3 --- /dev/null +++ b/internal/data/impl/advice_talk_impl.go @@ -0,0 +1,18 @@ +package impl + +import ( + "ai_scheduler/internal/data/model" + "ai_scheduler/tmpl/dataTemp" + "ai_scheduler/utils" +) + +type AdviceTalkImpl struct { + dataTemp.DataTemp + BaseRepository[model.AiTask] +} + +func NewAdviceTalkImpl(db *utils.Db) *AdviceTalkImpl { + return &AdviceTalkImpl{ + DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceTalk)), + } +} diff --git a/internal/data/model/ai_advice_advicer.gen.go b/internal/data/model/ai_advice_advicer.gen.go new file mode 100644 index 0000000..c525ec6 --- /dev/null +++ b/internal/data/model/ai_advice_advicer.gen.go @@ -0,0 +1,33 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package model + +import ( + "time" +) + +const TableNameAiAdviceAdvicer = "ai_advice_advicer" + +// AiAdviceAdvicer mapped from table +type AiAdviceAdvicer struct { + AdvicerID int32 `gorm:"column:advicer_id;primaryKey;autoIncrement:true" json:"advicer_id"` + Name string `gorm:"column:name;not null;comment:姓名" json:"name"` // 姓名 + Birth time.Time `gorm:"column:birth;not null;comment:用户名称" json:"birth"` // 用户名称 + Gender int32 `gorm:"column:gender;not null;comment:1:男,2:女" json:"gender"` // 1:男,2:女 + WorkingYears int32 `gorm:"column:working_years;not null;default:1;comment:工作年限" json:"working_years"` // 工作年限 + ContactTags string `gorm:"column:contact_tags;not null;comment:联系方式" json:"contact_tags"` // 联系方式 + NativeRegion string `gorm:"column:native_region;not null;comment:籍贯" json:"native_region"` // 籍贯 + DialectFeatures string `gorm:"column:dialect_features;not null;comment:语言风格" json:"dialect_features"` // 语言风格 + SentencePatterns string `gorm:"column:sentence_patterns;comment:句子模式" json:"sentence_patterns"` // 句子模式 + ToneTags string `gorm:"column:tone_tags;comment:语气标签" json:"tone_tags"` // 语气标签 + PersonalityTags string `gorm:"column:personality_tags;not null;comment:个性标签" json:"personality_tags"` // 个性标签 + SignatureDialogues string `gorm:"column:signature_dialogues;comment:代表性对话示例" json:"signature_dialogues"` // 代表性对话示例 + CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` +} + +// TableName AiAdviceAdvicer's table name +func (*AiAdviceAdvicer) TableName() string { + return TableNameAiAdviceAdvicer +} diff --git a/internal/data/model/ai_advice_project.gen.go b/internal/data/model/ai_advice_project.gen.go new file mode 100644 index 0000000..5829e36 --- /dev/null +++ b/internal/data/model/ai_advice_project.gen.go @@ -0,0 +1,28 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package model + +import ( + "time" +) + +const TableNameAiAdviceProject = "ai_advice_project" + +// AiAdviceProject mapped from table +type AiAdviceProject struct { + ProjectID int32 `gorm:"column:project_id;primaryKey;autoIncrement:true" json:"project_id"` + Name string `gorm:"column:name;not null;comment:姓名" json:"name"` // 姓名 + RegionValue string `gorm:"column:region_value;comment:区域价值话术库" json:"region_value"` // 区域价值话术库 + CompetitionComparison string `gorm:"column:competition_comparison;comment:竞品对比话术" json:"competition_comparison"` // 竞品对比话术 + CoreSellingPoints string `gorm:"column:core_selling_points;comment:项目核心卖点" json:"core_selling_points"` // 项目核心卖点 + SupportingFacilities string `gorm:"column:supporting_facilities;comment:配套体系" json:"supporting_facilities"` // 配套体系 + DeveloperBacking string `gorm:"column:developer_backing;comment:开发商背书" json:"developer_backing"` // 开发商背书 + CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` +} + +// TableName AiAdviceProject's table name +func (*AiAdviceProject) TableName() string { + return TableNameAiAdviceProject +} diff --git a/internal/data/model/ai_advice_talk.gen.go b/internal/data/model/ai_advice_talk.gen.go new file mode 100644 index 0000000..f465123 --- /dev/null +++ b/internal/data/model/ai_advice_talk.gen.go @@ -0,0 +1,27 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package model + +import ( + "time" +) + +const TableNameAiAdviceTalk = "ai_advice_talk" + +// AiAdviceTalk mapped from table +type AiAdviceTalk struct { + TalkID int32 `gorm:"column:talk_id;primaryKey;autoIncrement:true" json:"talk_id"` + NeedsMining string `gorm:"column:needs_mining;comment:需求挖掘话术" json:"needs_mining"` // 需求挖掘话术 + PainPointResponse string `gorm:"column:pain_point_response;comment:痛点应对策略" json:"pain_point_response"` // 痛点应对策略 + ValueBuilding string `gorm:"column:value_building;comment:价值塑造技巧" json:"value_building"` // 价值塑造技巧 + ClosingTechniques string `gorm:"column:closing_techniques;comment:促单话术" json:"closing_techniques"` // 促单话术 + CommunicationRhythm string `gorm:"column:communication_rhythm;comment:沟通节奏控制" json:"communication_rhythm"` // 沟通节奏控制 + CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` +} + +// TableName AiAdviceTalk's table name +func (*AiAdviceTalk) TableName() string { + return TableNameAiAdviceTalk +} diff --git a/internal/entitys/advicer.go b/internal/entitys/advicer.go new file mode 100644 index 0000000..aaa92f1 --- /dev/null +++ b/internal/entitys/advicer.go @@ -0,0 +1,135 @@ +package entitys + +type WordAnaReq struct { + WordFileUrl string `json:"word_file_url"` +} + +// -------顾问 + +// DialectFeatures 方言特征 +type DialectFeatures struct { + Region string `json:"region"` //方言使用程度 + Intensity float64 `json:"intensity"` // 方言使用强度(0-1) + KeyWords []string `json:"KeyWords"` +} + +func (e *DialectFeatures) Example() string { + return `{"region":"四川成都话","intensity":0.4,"key_words":["噻","要得","没得","不晓得","是不是","对的嘛","好嘛","晓得嘛","真的","正儿八经","说实话"]}` +} + +// SentencePatterns 句子模式 +type SentencePatterns struct { + OpeningMode []string `json:"openingMode"` //开场模式 + ExplanationMode []string `json:"explanationMode"` //解释模式 + ConfirmationMode []string `json:"confirmationMode"` //确认模式 + SummaryMode []string `json:"summaryMode"` //总结模式 + TransitionMode []string `json:"transitionMode"` //过渡模式 +} + +func (e *SentencePatterns) Example() string { + return `{"openingMode":["我是你们的职业顾问","我给你介绍一下","我们先来看一下"],"explanationMode":["是这样的","我跟你讲","因为...所以...","你发现没得"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?"],"summaryMode":["所以说","总结一下哈","简单说就是"],"transitionMode":["然后的话","再其次","除此之外","还有一点"]}` +} + +// PersonalityTags 个性标签 +type PersonalityTags []string + +func (e *PersonalityTags) Example() string { + return `["耐心细致","本地通","数据控","关系型","细节控","乐观积极","诚实可信"]` +} + +// ToneTags 语气标签 +type ToneTags struct { + Enthusiasm float64 `json:"enthusiasm"` + Patience float64 `json:"patience"` + Confidence float64 `json:"confidence"` + Friendliness float64 `json:"friendliness"` + Persuasion float64 `json:"persuasion"` +} + +func (e *ToneTags) Example() string { + return `{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.75,"persuasion":0.7}` +} + +// SignatureDialogues 代表性对话示例 +type SignatureDialogues struct { + Context string `json:"context"` + Dialogue string `json:"dialogue"` //解释 +} + +func (e *SignatureDialogues) Example() string { + return `[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都是2.5环内城买房,这种是个普遍存在的一个现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有管家送外卖、免费宠物喂养这些增值服务。你算一下,就算贵一块钱,十年也就多14000,但好物业让房子增值不止这点!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比28板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"},{"context":"介绍项目优势","dialogue":"我跟你讲,我们项目就三个核心:地段在2.5环内槐树店板块,产品是全玻璃幕墙+三层中空玻璃,价格是板块最低门槛。花400多万买这里,真的是可上可下!"},{"context":"处理客户异议","dialogue":"我懂你的担心,很多客户刚开始也这样想。但你看嘛,我们旁边那块48亩地还没拍,以后肯定也是大品牌开发商来做豪宅。到时候我们这边全是高端盘,价格只会往上走!"}]` +} + +// -------项目 + +// RegionValue 区域价值话术库 +type RegionValue map[string]string + +func (e *RegionValue) Example() string { + return `{"区位层级":["成华区2.5环内侧,这个位置真的稀缺","槐树店板块现在是成华区的number one板块","北接三板桥商圈,西靠万象城,东临火车东站","属于淮舜板块,万象城东的核心位置"],"地价论证":["我们地价19500,华晨府20400,棕榈也是2万+","2.5环内现在地价没有低于19000的","面粉贵了,面包不可能便宜"],"板块热度":["从21年新希望锦麟一品开始,这边全是高端盘","龙湖最高端的滨江系列在这里,新希望的锦麟系列也在这里","各大品牌开发商争相恐后都在这边拿地"],"发展规划":["槐树店板块是棋盘成钢之后第二个富人区","整个板块都是300万到900万的总价段","未来全是改善型住宅,没有刚需盘"]}` +} + +// CompetitionComparison 竞品对比话术 +type CompetitionComparison map[string]string + +func (e *CompetitionComparison) Example() string { + return `{"龙湖滨江云河颂":{"优点承认":"龙湖位置确实好,看沙河公园","价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们得房率118平实得132平,套内单价才33000"},"邦泰云锦":{"定位相似":"邦泰也是首个项目,要打造口碑","价格参考":"他们当时12800拿地,现在卖34000","品质对比":"我们外立面全玻璃幕墙,比他们成本高30%"},"招商景程序":{"板块差异":"他们在28板块,我们是槐树店板块","地价对比":"他们地价13000左右,我们19500","价值判断":"地价比我们便宜6000,但单价差不多,我们更值"},"万景/绿城":{"地块对比":"他们都是13-15亩,我们14亩差不多","楼间距":"他们楼间距只有30米,我们有200多米","产品力":"我们全屋木饰面交付,他们很多是简装"}}` +} + +// CoreSellingPoints 竞品对比话术 +type CoreSellingPoints map[string]string + +func (e *CoreSellingPoints) Example() string { + return `{"规划设计":{"楼间距":"一号楼看公园280米,二号楼看邦泰190米,三号楼间距71米","布局":"L型布局,最大化利用公园景观","容积率":"2.0,在主城区非常低","绿化率":"35%,加公园绿化实际超过50%"},"建筑品质":{"外立面":"全玻璃幕墙+铝单板,浅金色铝板","玻璃":"三层中空氩气玻璃(非双层),成本高一倍","层高":"3.2米层高,豪宅标准(很多盘只有3.05米)","架空层":"6米挑高,全架空设计"},"户型产品":{"118户型":"118平做四房三卫,成都唯一,实得132平","140户型":"270度全景舱,两面采光","得房率":"赠送多,实得率高","功能设计":"动静分区,双套房设计"},"装修标准":{"三大件":"菲斯曼地暖,日立中央空调,霍尼韦尔新风","厨房":"方太Y9烟灶,20套洗碗机,安吉尔净水器","卫浴":"高仪全套,无缝拼接台盆","地面":"简一大理石瓷砖,无缝拼接","墙面":"全屋木饰面,部分硬质软包"}}` +} + +// SupportingFacilities 配套体系 +type SupportingFacilities map[string]string + +func (e *SupportingFacilities) Example() string { + return `{"交通配套":{"地铁":"双店路站350米(7号线),槐树店站550米(4号线),未来12号线","道路":"中环路、成洛大道,到春熙路5个站","通达性":"到火车东站2个站,到华西30分钟内"},"商业配套":{"高端商圈":"万象城1.6公里,三板桥1.8公里","社区商业":"成华奥园广场、十里风荷底商","未来商业":"上东里商业(明年开业,有永辉超市)"},"生态配套":{"公园体系":"楼下40亩槐树店公园,400米沙河公园,700米多宝寺公园","绿道系统":"沿沙河跑到塔山公园20多公里","环境优势":"2.5环内唯一有300亩生态带的楼盘"},"教育配套":{"幼儿园":"楼下公立幼儿园(明年9月招生)","小学":"城市附小锦汇东城(成华区生源最好的学校)","生源优势":"周边新盘都是300万+,生源纯粹"},"医疗配套":{"三甲医院":"市六医院、市二医院3公里内","顶尖医疗":"华西医院锦江院区30分钟车程","便利性":"到华西本部也是30分钟内"}}` +} + +// DeveloperBacking 开发商背书 +type DeveloperBacking map[string]string + +func (e *DeveloperBacking) Example() string { + return `{"公司实力":"中信资产,多元化民营企业","资金安全":"在河南渑池有铝土矿,每年稳定收入10亿","开发经验":"宜宾有5个项目,贵州2个,成都是首个项目","合作方":"招商铂金物业,首次与外部企业合作"}` +} + +// -------销售话术 + +// NeedsMining 需求挖掘话术 +type NeedsMining map[string]string + +func (e *NeedsMining) Example() string { + return `{"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"],"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"],"偏好需求":["更看重地段还是产品本身?","喜欢安静的还是热闹的?","对楼层、朝向有偏好吗?"],"时间需求":["打算什么时候入住?","现在看到什么阶段了?","决策需要跟家人商量吗?"]}` +} + +// PainPointResponse 痛点应对策略 +type PainPointResponse map[string]string + +func (e *PainPointResponse) Example() string { + return `{"地块太小":{"承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩","转化优势":"但人少安静,楼间距反而更开阔","对比竞品":"339的邦泰才11亩,人家上千万豪宅"},"物业费高":{"理解感受":"我懂你,我们也觉得有点贵","价值分析":"但6块里3块是增值服务(保洁、送外卖、宠物喂养)","价格补贴":"前三年补贴到5块,跟其他盘差不多","保值论证":"好物业让房子增值,紫东星座就是例子"},"开发商不知名":{"坦诚相告":"没听说过很正常,我们是成都首个项目","实力展示":"但中信主业是矿产,房地产只占20%,资金安全","案例对比":"邦泰、奥兰刚开始也没人知道,现在都认可了","品质承诺":"首个项目更要做好口碑,不计成本打造"},"周边有社区用地":{"明确规划":"那边是社区服务中心,最多三层楼","距离保证":"离我们有100米,不影响采光","未来价值":"社区配套齐全,生活更方便","对比安慰":"总比修高楼挡光好嘛"},"价格偏高":{"地段价值":"地段值这个价,2.5环内没得选","产品价值":"全玻璃幕墙、3.2米层高,成本就高","比较价值":"比龙湖、邦泰单价都低","门槛价值":"板块最低总价,以后更买不起"}}` +} + +// ValueBuilding 价值塑造技巧 +type ValueBuilding map[string]string + +func (e *ValueBuilding) Example() string { + return `{"地段价值塑造":["买房最重要的是地段、地段、还是地段","核心地段的核心资产才保值增值","2.5环内的地卖一块少一块,不可再生"],"产品价值塑造":["我们是用改善的价格,买豪宅的标准","很多细节都是3000万豪宅才有的配置","外立面成本比竞品高30%,但单价差不多"],"稀缺性塑造":["整个槐树店板块,我们是唯一有400万以下产品的","118四房三卫,全成都找不出第二个","200多米楼间距,主城区绝版"],"未来价值塑造":["现在垫垫脚买了,未来换房可上可下","旁边48亩地以后拍出来,肯定刷新地价","板块全部交付后,城市界面会完全不一样"]}` +} + +// ClosingTechniques 促单话术 +type ClosingTechniques map[string]string + +func (e *ClosingTechniques) Example() string { + return `{"紧迫感营造":{"时间紧迫":["今天是月底最后一天,领导有压力价格可谈","我们刚刚开盘,还有额外优惠","月底冲业绩,价格最有弹性"],"房源稀缺":["118只剩20多套了,好楼层不多","这栋楼就60户,卖一套少一套","特价房只有这几套,今天不定可能就没了"]},"优惠策略":{"价格优惠":["今天定的话,我可以跟领导申请额外折扣","买车位的话,总价多给两个点优惠","一次性付款再优惠一个点"],"附加价值":["送一年物业费","送品牌家电礼包","优先选车位"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠优先通知你","今天不定的话,我帮你留意好楼层"],"风险规避":["现在不定,下次来可能就不是这个价了","好楼层不等人,上次有客户犹豫一天就没了","月底优惠政策可能明天就没了"]},"成交确认":{"二选一":["您是选902还是1002?","倾向118还是140?","喜欢东边户还是西边户?"],"假设成交":["那我给您准备合同了","车位您选哪个位置?","按揭资料我带您去准备"]}}` +} + +// CommunicationRhythm 沟通节奏控制 +type CommunicationRhythm map[string]string + +func (e *CommunicationRhythm) Example() string { + return `{"开场阶段":{"时间占比":"5%","目标":"建立关系,了解需求","关键动作":"亲切称呼,简单寒暄,确认看房重点"},"沙盘讲解":{"时间占比":"30%","目标":"建立价值认知","关键动作":"板块价值→周边配套→项目亮点→开发商介绍"},"样板间体验":{"时间占比":"35%","目标":"激发购买欲望","关键动作":"让客户亲自体验(开关窗、看视野、感受层高)"},"价格谈判":{"时间占比":"25%","目标":"促成决策","关键动作":"算价→解释优惠→对比竞品→促单"},"结束跟进":{"时间占比":"5%","目标":"建立长期联系","关键动作":"送资料,加微信,约定下次,小礼物"}}` +} diff --git a/internal/pkg/file_download/file_download.go b/internal/pkg/file_download/file_download.go new file mode 100644 index 0000000..26fc5c2 --- /dev/null +++ b/internal/pkg/file_download/file_download.go @@ -0,0 +1,246 @@ +package file_download + +import ( + "bytes" + "errors" + "fmt" + "io" + + "github.com/unidoc/unioffice/document" + + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" +) + +// 下载文件 +func DownloadFile(url string, validFunc func(resp *http.Response) error) ([]byte, string, error) { + // 设置超时 + client := &http.Client{ + Timeout: 30 * time.Second, + } + + // 发送请求 + resp, err := client.Get(url) + if err != nil { + return nil, "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status) + } + + if validFunc != nil { + err = validFunc(resp) + if err != nil { + return nil, "", err + } + } + + // 读取文件数据 + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, "", err + } + + // 获取文件名 + filename := getFilenameFromURL(url, resp) + + return data, filename, nil +} + +// 从 URL 或响应头获取文件名 +func getFilenameFromURL(urlStr string, resp *http.Response) string { + // 1. 尝试从 Content-Disposition 头获取 + contentDisposition := resp.Header.Get("Content-Disposition") + if contentDisposition != "" { + if strings.Contains(contentDisposition, "filename=") { + parts := strings.Split(contentDisposition, "filename=") + if len(parts) > 1 { + filename := strings.Trim(parts[1], `"' `) + return sanitizeFilename(filename) + } + } + } + + // 2. 从 URL 路径获取 + parsedURL, err := url.Parse(urlStr) + if err == nil { + path := parsedURL.Path + if path != "" { + filename := filepath.Base(path) + if filename != "" && filename != "." && filename != "/" { + return sanitizeFilename(filename) + } + } + } + + // 3. 生成默认文件名 + return fmt.Sprintf("word_%d.docx", time.Now().Unix()) +} + +// 清理文件名 +func sanitizeFilename(filename string) string { + // 移除非法字符 + illegalChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} + for _, char := range illegalChars { + filename = strings.ReplaceAll(filename, char, "_") + } + + // 确保有扩展名 + if !strings.Contains(filename, ".") { + filename += ".docx" + } + + return filename +} + +// 从URL获取Word文件的纯文本内容 +func GetWordTextFromURL(url string, validFunc func(resp *http.Response) error) (string, string, error) { + // 1. 下载文件 + data, fileName, err := DownloadFile(url, validFunc) + if err != nil { + return "", "", fmt.Errorf("下载失败: %w", err) + } + + // 2. 解析Word文件 + text, err := parseWordContent(data) + if err != nil { + return "", "", fmt.Errorf("解析失败: %w", err) + } + + return text, fileName, nil +} + +// 解析Word内容 - 简单版本,只提取文字 +func parseWordContent(data []byte) (string, error) { + // 创建reader + reader := bytes.NewReader(data) + + // 打开Word文档 + doc, err := document.Read(reader, int64(len(data))) + if err != nil { + return "", err + } + defer doc.Close() + + // 提取所有文字 + var textBuilder strings.Builder + + // 遍历所有段落 + for _, paragraph := range doc.Paragraphs() { + // 遍历段落中的所有文本块 + for _, run := range paragraph.Runs() { + textBuilder.WriteString(run.Text()) + } + // 每个段落后面加换行 + textBuilder.WriteString("\n") + } + + // 返回清理过的文本 + result := strings.TrimSpace(textBuilder.String()) + return result, nil +} + +// 解析 Word 文件内容 +func parseWordFile(filePath string) (map[string]interface{}, error) { + ext := strings.ToLower(filepath.Ext(filePath)) + + result := map[string]interface{}{ + "filepath": filePath, + "format": ext, + } + + // 解析 .docx 文件 + if ext == ".docx" { + doc, err := document.Open(filePath) + if err != nil { + return nil, err + } + defer doc.Close() + + // 提取段落文本 + var paragraphs []string + for _, p := range doc.Paragraphs() { + text := "" + for _, run := range p.Runs() { + text += run.Text() + } + if strings.TrimSpace(text) != "" { + paragraphs = append(paragraphs, text) + } + } + + // 提取表格内容 + var tables []map[string]interface{} + for _, table := range doc.Tables() { + tableData := make(map[string]interface{}) + var rows []map[int]string + + for _, row := range table.Rows() { + rowData := make(map[int]string) + for cellIdx, cell := range row.Cells() { + cellText := "" + for _, p := range cell.Paragraphs() { + for _, run := range p.Runs() { + cellText += run.Text() + } + } + rowData[cellIdx] = cellText + } + rows = append(rows, rowData) + } + + tableData["rows"] = rows + tableData["row_count"] = len(rows) + tables = append(tables, tableData) + } + + result["paragraphs"] = paragraphs + result["tables"] = tables + result["paragraph_count"] = len(paragraphs) + result["table_count"] = len(tables) + + } else if ext == ".doc" { + // 对于 .doc 文件,可能需要其他库或转换 + // 这里简单读取为二进制文件 + data, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + result["binary_size"] = len(data) + result["note"] = ".doc 文件需要专门的解析库" + } + + // 获取文件信息 + fileInfo, _ := os.Stat(filePath) + if fileInfo != nil { + result["filesize"] = fileInfo.Size() + result["modified"] = fileInfo.ModTime() + } + + return result, nil +} + +// 判断是否为 Word 文件 +func IsWordFile(resp *http.Response) error { + contentType := resp.Header.Get("Content-Type") + wordContentTypes := []string{ + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-word", + "application/octet-stream", // 有些服务器可能返回这个 + } + + contentType = strings.ToLower(contentType) + for _, ct := range wordContentTypes { + if strings.Contains(contentType, ct) { + return nil + } + } + return errors.New("错误的文件类型") +} diff --git a/internal/services/advicer_test.go b/internal/services/advicer_test.go new file mode 100644 index 0000000..bce9307 --- /dev/null +++ b/internal/services/advicer_test.go @@ -0,0 +1,15 @@ +package services + +import ( + "testing" +) + +func Test_WordAna(t *testing.T) { + Run() + + advicer.WordAnat("https://attachment-public.oss-cn-hangzhou.aliyuncs.com/ai-scheduler/data-analytics/word/content.docx") +} + +var ( + advicer *AdviceService +) From f79481d2bf6c5ab21c3298091987358770d532f7 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Thu, 29 Jan 2026 14:36:31 +0800 Subject: [PATCH 4/8] =?UTF-8?q?add:=20=E6=96=B0=E5=A2=9EWord=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=88=86=E6=9E=90=E6=9C=8D=E5=8A=A1=E5=8F=8A=E8=B7=AF?= =?UTF-8?q?=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 1 + go.sum | 3 + internal/biz/advice.go | 2 +- internal/biz/llm_service/third_party/hsyq.go | 4 +- internal/entitys/advicer.go | 24 +-- internal/pkg/file_download/file_download.go | 164 +++++++++---------- 6 files changed, 97 insertions(+), 101 deletions(-) diff --git a/go.mod b/go.mod index ac6aa2a..eb351cb 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/gofiber/websocket/v2 v2.2.1 github.com/google/uuid v1.6.0 github.com/google/wire v0.7.0 + github.com/lukasjarosch/go-docx v0.5.0 github.com/ollama/ollama v0.12.7 github.com/redis/go-redis/v9 v9.16.0 github.com/robfig/cron/v3 v3.0.1 diff --git a/go.sum b/go.sum index 1c08769..b069ba2 100644 --- a/go.sum +++ b/go.sum @@ -327,6 +327,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lukasjarosch/go-docx v0.5.0 h1:4vU+gJ4WMdqwRvRVFF+XMw3rPfUGSXlToPJIX3mHQsQ= +github.com/lukasjarosch/go-docx v0.5.0/go.mod h1:ka/NZgDIJId48vMvcfWfduVTY7uV0/f8EgsmCjuS9X0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -582,6 +584,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= diff --git a/internal/biz/advice.go b/internal/biz/advice.go index 5c2a93a..94ccd71 100644 --- a/internal/biz/advice.go +++ b/internal/biz/advice.go @@ -64,7 +64,7 @@ func (a *AdviceBiz) WordAna(ctx context.Context, wordContent string) error { func (a *AdviceBiz) callLlm(ctx context.Context, prompt string) (string, error) { var message = make([]*model.ChatCompletionMessage, 1) message[0] = &model.ChatCompletionMessage{ - Role: model.ChatMessageRoleSystem, + Role: model.ChatMessageRoleUser, Content: &model.ChatCompletionMessageContent{ StringValue: volcengine.String(prompt), }, diff --git a/internal/biz/llm_service/third_party/hsyq.go b/internal/biz/llm_service/third_party/hsyq.go index 763db1d..5257774 100644 --- a/internal/biz/llm_service/third_party/hsyq.go +++ b/internal/biz/llm_service/third_party/hsyq.go @@ -20,7 +20,7 @@ func NewHsyq() *Hsyq { func (h *Hsyq) getClient(key string) *arkruntime.Client { var client *arkruntime.Client - if _, ok := h.mapClient[key]; !ok { + if _, ok := h.mapClient[key]; ok { client = h.mapClient[key] } else { client = arkruntime.NewClientWithApiKey( @@ -39,6 +39,8 @@ func (h *Hsyq) RequestHsyq(ctx context.Context, key string, modelName string, pr req := model.CreateChatCompletionRequest{ Model: modelName, Messages: prompt, + Stream: new(bool), + Thinking: &model.Thinking{Type: model.ThinkingTypeDisabled}, } resp, err := h.getClient(key).CreateChatCompletion(ctx, req) diff --git a/internal/entitys/advicer.go b/internal/entitys/advicer.go index aaa92f1..46dd76e 100644 --- a/internal/entitys/advicer.go +++ b/internal/entitys/advicer.go @@ -14,7 +14,7 @@ type DialectFeatures struct { } func (e *DialectFeatures) Example() string { - return `{"region":"四川成都话","intensity":0.4,"key_words":["噻","要得","没得","不晓得","是不是","对的嘛","好嘛","晓得嘛","真的","正儿八经","说实话"]}` + return `{"region":"四川成都话","intensity":0.4,"key_words":["噻","要得","没得","不晓得","是不是"]}` } // SentencePatterns 句子模式 @@ -27,14 +27,14 @@ type SentencePatterns struct { } func (e *SentencePatterns) Example() string { - return `{"openingMode":["我是你们的职业顾问","我给你介绍一下","我们先来看一下"],"explanationMode":["是这样的","我跟你讲","因为...所以...","你发现没得"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?"],"summaryMode":["所以说","总结一下哈","简单说就是"],"transitionMode":["然后的话","再其次","除此之外","还有一点"]}` + return `{"openingMode":["我给你介绍一下","我们先来看一下"],"explanationMode":["是这样的","我跟你讲","你发现没得"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?"],"summaryMode":["所以说","简单说就是"],"transitionMode":["然后的话","再其次","还有一点"]}` } // PersonalityTags 个性标签 type PersonalityTags []string func (e *PersonalityTags) Example() string { - return `["耐心细致","本地通","数据控","关系型","细节控","乐观积极","诚实可信"]` + return `["耐心细致","细节控"]` } // ToneTags 语气标签 @@ -57,7 +57,7 @@ type SignatureDialogues struct { } func (e *SignatureDialogues) Example() string { - return `[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都是2.5环内城买房,这种是个普遍存在的一个现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有管家送外卖、免费宠物喂养这些增值服务。你算一下,就算贵一块钱,十年也就多14000,但好物业让房子增值不止这点!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比28板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"},{"context":"介绍项目优势","dialogue":"我跟你讲,我们项目就三个核心:地段在2.5环内槐树店板块,产品是全玻璃幕墙+三层中空玻璃,价格是板块最低门槛。花400多万买这里,真的是可上可下!"},{"context":"处理客户异议","dialogue":"我懂你的担心,很多客户刚开始也这样想。但你看嘛,我们旁边那块48亩地还没拍,以后肯定也是大品牌开发商来做豪宅。到时候我们这边全是高端盘,价格只会往上走!"}]` + return `[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都是2.5环内城买房,这种是个普遍存在的一个现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有管家送外卖、免费宠物喂养这些增值服务。你算一下,就算贵一块钱,十年也就多14000,但好物业让房子增值不止这点!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比28板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"}]` } // -------项目 @@ -73,21 +73,21 @@ func (e *RegionValue) Example() string { type CompetitionComparison map[string]string func (e *CompetitionComparison) Example() string { - return `{"龙湖滨江云河颂":{"优点承认":"龙湖位置确实好,看沙河公园","价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们得房率118平实得132平,套内单价才33000"},"邦泰云锦":{"定位相似":"邦泰也是首个项目,要打造口碑","价格参考":"他们当时12800拿地,现在卖34000","品质对比":"我们外立面全玻璃幕墙,比他们成本高30%"},"招商景程序":{"板块差异":"他们在28板块,我们是槐树店板块","地价对比":"他们地价13000左右,我们19500","价值判断":"地价比我们便宜6000,但单价差不多,我们更值"},"万景/绿城":{"地块对比":"他们都是13-15亩,我们14亩差不多","楼间距":"他们楼间距只有30米,我们有200多米","产品力":"我们全屋木饰面交付,他们很多是简装"}}` + return `{"龙湖滨江云河颂":{"优点承认":"龙湖位置确实好,看沙河公园","价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们得房率118平实得132平,套内单价才33000"},"邦泰云锦":{"定位相似":"邦泰也是首个项目,要打造口碑","价格参考":"他们当时12800拿地,现在卖34000","品质对比":"我们外立面全玻璃幕墙,比他们成本高30%"}}` } // CoreSellingPoints 竞品对比话术 type CoreSellingPoints map[string]string func (e *CoreSellingPoints) Example() string { - return `{"规划设计":{"楼间距":"一号楼看公园280米,二号楼看邦泰190米,三号楼间距71米","布局":"L型布局,最大化利用公园景观","容积率":"2.0,在主城区非常低","绿化率":"35%,加公园绿化实际超过50%"},"建筑品质":{"外立面":"全玻璃幕墙+铝单板,浅金色铝板","玻璃":"三层中空氩气玻璃(非双层),成本高一倍","层高":"3.2米层高,豪宅标准(很多盘只有3.05米)","架空层":"6米挑高,全架空设计"},"户型产品":{"118户型":"118平做四房三卫,成都唯一,实得132平","140户型":"270度全景舱,两面采光","得房率":"赠送多,实得率高","功能设计":"动静分区,双套房设计"},"装修标准":{"三大件":"菲斯曼地暖,日立中央空调,霍尼韦尔新风","厨房":"方太Y9烟灶,20套洗碗机,安吉尔净水器","卫浴":"高仪全套,无缝拼接台盆","地面":"简一大理石瓷砖,无缝拼接","墙面":"全屋木饰面,部分硬质软包"}}` + return `{"龙湖滨江云河颂":{"优点承认":"龙湖位置确实好,看沙河公园","价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们得房率118平实得132平,套内单价才33000"},"邦泰云锦":{"定位相似":"邦泰也是首个项目,要打造口碑","价格参考":"他们当时12800拿地,现在卖34000","品质对比":"我们外立面全玻璃幕墙,比他们成本高30%"}` } // SupportingFacilities 配套体系 type SupportingFacilities map[string]string func (e *SupportingFacilities) Example() string { - return `{"交通配套":{"地铁":"双店路站350米(7号线),槐树店站550米(4号线),未来12号线","道路":"中环路、成洛大道,到春熙路5个站","通达性":"到火车东站2个站,到华西30分钟内"},"商业配套":{"高端商圈":"万象城1.6公里,三板桥1.8公里","社区商业":"成华奥园广场、十里风荷底商","未来商业":"上东里商业(明年开业,有永辉超市)"},"生态配套":{"公园体系":"楼下40亩槐树店公园,400米沙河公园,700米多宝寺公园","绿道系统":"沿沙河跑到塔山公园20多公里","环境优势":"2.5环内唯一有300亩生态带的楼盘"},"教育配套":{"幼儿园":"楼下公立幼儿园(明年9月招生)","小学":"城市附小锦汇东城(成华区生源最好的学校)","生源优势":"周边新盘都是300万+,生源纯粹"},"医疗配套":{"三甲医院":"市六医院、市二医院3公里内","顶尖医疗":"华西医院锦江院区30分钟车程","便利性":"到华西本部也是30分钟内"}}` + return `{"交通配套":{"地铁":"双店路站350米(7号线),槐树店站550米(4号线),未来12号线","道路":"中环路、成洛大道,到春熙路5个站","通达性":"到火车东站2个站,到华西30分钟内"},"教育配套":{"幼儿园":"楼下公立幼儿园","小学":"城市附小锦汇东城(成华区生源最好的学校)","生源优势":"周边新盘都是300万+,生源纯粹"},"医疗配套":{"三甲医院":"市六医院、市二医院3公里内","顶尖医疗":"华西医院锦江院区30分钟车程","便利性":"到华西本部也是30分钟内"}}` } // DeveloperBacking 开发商背书 @@ -103,33 +103,33 @@ func (e *DeveloperBacking) Example() string { type NeedsMining map[string]string func (e *NeedsMining) Example() string { - return `{"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"],"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"],"偏好需求":["更看重地段还是产品本身?","喜欢安静的还是热闹的?","对楼层、朝向有偏好吗?"],"时间需求":["打算什么时候入住?","现在看到什么阶段了?","决策需要跟家人商量吗?"]}` + return `{"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"],"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"]}` } // PainPointResponse 痛点应对策略 type PainPointResponse map[string]string func (e *PainPointResponse) Example() string { - return `{"地块太小":{"承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩","转化优势":"但人少安静,楼间距反而更开阔","对比竞品":"339的邦泰才11亩,人家上千万豪宅"},"物业费高":{"理解感受":"我懂你,我们也觉得有点贵","价值分析":"但6块里3块是增值服务(保洁、送外卖、宠物喂养)","价格补贴":"前三年补贴到5块,跟其他盘差不多","保值论证":"好物业让房子增值,紫东星座就是例子"},"开发商不知名":{"坦诚相告":"没听说过很正常,我们是成都首个项目","实力展示":"但中信主业是矿产,房地产只占20%,资金安全","案例对比":"邦泰、奥兰刚开始也没人知道,现在都认可了","品质承诺":"首个项目更要做好口碑,不计成本打造"},"周边有社区用地":{"明确规划":"那边是社区服务中心,最多三层楼","距离保证":"离我们有100米,不影响采光","未来价值":"社区配套齐全,生活更方便","对比安慰":"总比修高楼挡光好嘛"},"价格偏高":{"地段价值":"地段值这个价,2.5环内没得选","产品价值":"全玻璃幕墙、3.2米层高,成本就高","比较价值":"比龙湖、邦泰单价都低","门槛价值":"板块最低总价,以后更买不起"}}` + return `{"地块太小":{"承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩","转化优势":"但人少安静,楼间距反而更开阔","对比竞品":"339的邦泰才11亩,人家上千万豪宅"},"物业费高":{"理解感受":"我懂你,我们也觉得有点贵","价值分析":"但6块里3块是增值服务(保洁、送外卖)","价格补贴":"前三年补贴到5块,跟其他盘差不多"}}` } // ValueBuilding 价值塑造技巧 type ValueBuilding map[string]string func (e *ValueBuilding) Example() string { - return `{"地段价值塑造":["买房最重要的是地段、地段、还是地段","核心地段的核心资产才保值增值","2.5环内的地卖一块少一块,不可再生"],"产品价值塑造":["我们是用改善的价格,买豪宅的标准","很多细节都是3000万豪宅才有的配置","外立面成本比竞品高30%,但单价差不多"],"稀缺性塑造":["整个槐树店板块,我们是唯一有400万以下产品的","118四房三卫,全成都找不出第二个","200多米楼间距,主城区绝版"],"未来价值塑造":["现在垫垫脚买了,未来换房可上可下","旁边48亩地以后拍出来,肯定刷新地价","板块全部交付后,城市界面会完全不一样"]}` + return `{"地段价值塑造":["买房最重要的是地段、地段、还是地段","核心地段的核心资产才保值增值","2.5环内的地卖一块少一块,不可再生"],"产品价值塑造":["我们是用改善的价格,买豪宅的标准","很多细节都是3000万豪宅才有的配置","外立面成本比竞品高30%,但单价差不多"]}` } // ClosingTechniques 促单话术 type ClosingTechniques map[string]string func (e *ClosingTechniques) Example() string { - return `{"紧迫感营造":{"时间紧迫":["今天是月底最后一天,领导有压力价格可谈","我们刚刚开盘,还有额外优惠","月底冲业绩,价格最有弹性"],"房源稀缺":["118只剩20多套了,好楼层不多","这栋楼就60户,卖一套少一套","特价房只有这几套,今天不定可能就没了"]},"优惠策略":{"价格优惠":["今天定的话,我可以跟领导申请额外折扣","买车位的话,总价多给两个点优惠","一次性付款再优惠一个点"],"附加价值":["送一年物业费","送品牌家电礼包","优先选车位"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠优先通知你","今天不定的话,我帮你留意好楼层"],"风险规避":["现在不定,下次来可能就不是这个价了","好楼层不等人,上次有客户犹豫一天就没了","月底优惠政策可能明天就没了"]},"成交确认":{"二选一":["您是选902还是1002?","倾向118还是140?","喜欢东边户还是西边户?"],"假设成交":["那我给您准备合同了","车位您选哪个位置?","按揭资料我带您去准备"]}}` + return `{"紧迫感营造":{"时间紧迫":["今天是月底最后一天,领导有压力价格可谈","我们刚刚开盘,还有额外优惠","月底冲业绩,价格最有弹性"],"房源稀缺":["118只剩20多套了,好楼层不多","这栋楼就60户,卖一套少一套","特价房只有这几套,今天不定可能就没了"]},"优惠策略":{"价格优惠":["今天定的话,我可以跟领导申请额外折扣","买车位的话,总价多给两个点优惠","一次性付款再优惠一个点"],"附加价值":["送一年物业费","送品牌家电礼包","优先选车位"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠优先通知你","今天不定的话,我帮你留意好楼层"]}}` } // CommunicationRhythm 沟通节奏控制 type CommunicationRhythm map[string]string func (e *CommunicationRhythm) Example() string { - return `{"开场阶段":{"时间占比":"5%","目标":"建立关系,了解需求","关键动作":"亲切称呼,简单寒暄,确认看房重点"},"沙盘讲解":{"时间占比":"30%","目标":"建立价值认知","关键动作":"板块价值→周边配套→项目亮点→开发商介绍"},"样板间体验":{"时间占比":"35%","目标":"激发购买欲望","关键动作":"让客户亲自体验(开关窗、看视野、感受层高)"},"价格谈判":{"时间占比":"25%","目标":"促成决策","关键动作":"算价→解释优惠→对比竞品→促单"},"结束跟进":{"时间占比":"5%","目标":"建立长期联系","关键动作":"送资料,加微信,约定下次,小礼物"}}` + return `{"开场阶段":{"时间占比":"5%","目标":"建立关系,了解需求","关键动作":"亲切称呼,简单寒暄,确认看房重点"},"沙盘讲解":{"时间占比":"30%","目标":"建立价值认知","关键动作":"板块价值→周边配套→项目亮点→开发商介绍"}}` } diff --git a/internal/pkg/file_download/file_download.go b/internal/pkg/file_download/file_download.go index 26fc5c2..70429be 100644 --- a/internal/pkg/file_download/file_download.go +++ b/internal/pkg/file_download/file_download.go @@ -1,16 +1,16 @@ package file_download import ( + "archive/zip" "bytes" + "encoding/xml" "errors" "fmt" "io" - "github.com/unidoc/unioffice/document" - "net/http" "net/url" - "os" + "path/filepath" "strings" "time" @@ -118,112 +118,102 @@ func GetWordTextFromURL(url string, validFunc func(resp *http.Response) error) ( // 解析Word内容 - 简单版本,只提取文字 func parseWordContent(data []byte) (string, error) { - // 创建reader reader := bytes.NewReader(data) - - // 打开Word文档 - doc, err := document.Read(reader, int64(len(data))) + zipReader, err := zip.NewReader(reader, int64(len(data))) if err != nil { - return "", err + return "", fmt.Errorf("解压docx失败: %v", err) } - defer doc.Close() - // 提取所有文字 var textBuilder strings.Builder - // 遍历所有段落 - for _, paragraph := range doc.Paragraphs() { - // 遍历段落中的所有文本块 - for _, run := range paragraph.Runs() { - textBuilder.WriteString(run.Text()) + // 遍历 ZIP 文件中的文件 + for _, file := range zipReader.File { + // 只处理文档主体文件 + if file.Name == "word/document.xml" { + rc, err := file.Open() + if err != nil { + return "", fmt.Errorf("打开文档文件失败: %v", err) + } + defer rc.Close() + + // 读取 XML 内容 + xmlData, err := io.ReadAll(rc) + if err != nil { + return "", fmt.Errorf("读取XML失败: %v", err) + } + + // 提取文本 + text, err := parseWordXML(xmlData) + if err != nil { + return "", fmt.Errorf("解析XML失败: %v", err) + } + + textBuilder.WriteString(text) + break // 找到主文档后退出循环 + } + } + + return textBuilder.String(), nil +} + +// 解析 Word XML 文档 +func parseWordXML(xmlData []byte) (string, error) { + type WordDocument struct { + XMLName xml.Name `xml:"document"` + Body struct { + Paragraphs []struct { + Runs []struct { + Text string `xml:"t"` + } `xml:"r"` + } `xml:"p"` + } `xml:"body"` + } + + var doc WordDocument + if err := xml.Unmarshal(xmlData, &doc); err != nil { + // 尝试简化解析 + return extractTextSimple(xmlData), nil + } + + var textBuilder strings.Builder + for _, para := range doc.Body.Paragraphs { + for _, run := range para.Runs { + textBuilder.WriteString(run.Text) } - // 每个段落后面加换行 textBuilder.WriteString("\n") } - // 返回清理过的文本 - result := strings.TrimSpace(textBuilder.String()) - return result, nil + return textBuilder.String(), nil } -// 解析 Word 文件内容 -func parseWordFile(filePath string) (map[string]interface{}, error) { - ext := strings.ToLower(filepath.Ext(filePath)) +// 简化文本提取(处理更复杂的文档结构) +func extractTextSimple(xmlData []byte) string { + var textBuilder strings.Builder - result := map[string]interface{}{ - "filepath": filePath, - "format": ext, - } - - // 解析 .docx 文件 - if ext == ".docx" { - doc, err := document.Open(filePath) + // 简单提取 标签内容 + decoder := xml.NewDecoder(bytes.NewReader(xmlData)) + for { + token, err := decoder.Token() + if err == io.EOF { + break + } if err != nil { - return nil, err - } - defer doc.Close() - - // 提取段落文本 - var paragraphs []string - for _, p := range doc.Paragraphs() { - text := "" - for _, run := range p.Runs() { - text += run.Text() - } - if strings.TrimSpace(text) != "" { - paragraphs = append(paragraphs, text) - } + continue } - // 提取表格内容 - var tables []map[string]interface{} - for _, table := range doc.Tables() { - tableData := make(map[string]interface{}) - var rows []map[int]string - - for _, row := range table.Rows() { - rowData := make(map[int]string) - for cellIdx, cell := range row.Cells() { - cellText := "" - for _, p := range cell.Paragraphs() { - for _, run := range p.Runs() { - cellText += run.Text() - } + if startElem, ok := token.(xml.StartElement); ok { + if startElem.Name.Local == "t" { + // 读取文本内容 + if nextToken, err := decoder.Token(); err == nil { + if charData, ok := nextToken.(xml.CharData); ok { + textBuilder.WriteString(string(charData)) } - rowData[cellIdx] = cellText } - rows = append(rows, rowData) } - - tableData["rows"] = rows - tableData["row_count"] = len(rows) - tables = append(tables, tableData) } - - result["paragraphs"] = paragraphs - result["tables"] = tables - result["paragraph_count"] = len(paragraphs) - result["table_count"] = len(tables) - - } else if ext == ".doc" { - // 对于 .doc 文件,可能需要其他库或转换 - // 这里简单读取为二进制文件 - data, err := os.ReadFile(filePath) - if err != nil { - return nil, err - } - result["binary_size"] = len(data) - result["note"] = ".doc 文件需要专门的解析库" } - // 获取文件信息 - fileInfo, _ := os.Stat(filePath) - if fileInfo != nil { - result["filesize"] = fileInfo.Size() - result["modified"] = fileInfo.ModTime() - } - - return result, nil + return textBuilder.String() } // 判断是否为 Word 文件 From a950a7b025943aee33412ad4c776e5a57336300b Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Thu, 29 Jan 2026 15:06:08 +0800 Subject: [PATCH 5/8] =?UTF-8?q?add:=20=E6=96=B0=E5=A2=9EWord=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=88=86=E6=9E=90=E6=9C=8D=E5=8A=A1=E5=8F=8A=E8=B7=AF?= =?UTF-8?q?=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/advice.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/biz/advice.go b/internal/biz/advice.go index 94ccd71..3f2eb1f 100644 --- a/internal/biz/advice.go +++ b/internal/biz/advice.go @@ -50,11 +50,14 @@ var dataMap = map[string]string{ func (a *AdviceBiz) WordAna(ctx context.Context, wordContent string) error { examples := a.getAllExamples() prompt := a.buildSimplePrompt(wordContent, examples) + os.WriteFile("requset.json", []byte(prompt), 0644) anaContent, err := a.callLlm(ctx, prompt) if err != nil { return err } + os.WriteFile("res.json", []byte(anaContent), 0644) data := a.parseResponse(anaContent) + jsonData, _ := json.MarshalIndent(data, "", " ") os.WriteFile("extracted.json", jsonData, 0644) fmt.Println("✅ 数据已保存到 extracted.json") @@ -88,15 +91,15 @@ func (a *AdviceBiz) buildSimplePrompt(wordContent string, examples map[string]st 对话内容: %s -请按照以下` + fmt.Sprintf("%d", len(examples)) + `个格式生成JSON数据,每个格式用===分隔: +请按照以下` + fmt.Sprintf("%d", len(examples)) + `个格式生成JSON数据,key为格式名称,value为对应值: %s 输出要求: 1. 每个结构体一个JSON对象 -2. 严格按照示例格式 -3. 用空行分隔不同结构体` - +2. 所有内容必须严格基于提供的对话原文,不得编造 +3. 严格按照示例格式 +4. 将上述生成的` + fmt.Sprintf("%d", len(examples)) + `个JSON对象,json不需要有可读性,不要有特殊符号,比如"\n",用map[string]json来包裹所有json对象:{"SupportingFacilities":{...},"SignatureDialogues":[{...},{...}]}` // 构建格式部分 var formats strings.Builder for name, example := range examples { From 6fedb76631de8f3a6bde0e9d3eea58e4e7f55193 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Sat, 31 Jan 2026 18:11:02 +0800 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84advice?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8F=8A=E6=B7=BB=E5=8A=A0MongoDB=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-test.md | 7 +- cmd/server/main.go | 2 +- cmd/server/wire.go | 3 +- config/config_test.yaml | 8 + deploy.sh | 9 +- docker-compose.yml | 70 +++++ internal/biz/advice.go | 156 ---------- internal/biz/advice_advicer.go | 76 +++++ internal/biz/advice_file.go | 189 ++++++++++++ internal/biz/provider_set.go | 3 +- internal/config/config.go | 10 + internal/data/impl/advice_advicer_impl.go | 3 +- .../data/impl/advice_advicer_version_impl.go | 17 ++ internal/data/impl/advice_client_impl.go | 17 ++ internal/data/impl/provider_set.go | 4 +- internal/data/model/ai_advice_advicer.gen.go | 19 +- .../model/ai_advice_advicer_version.gen.go | 29 ++ internal/data/model/ai_advice_client.gen.go | 27 ++ internal/entitys/advicer.go | 278 +++++++++++++++++- internal/entitys/advicer_data.go | 29 ++ internal/entitys/bot.go | 4 + internal/pkg/mongo.go | 35 +++ internal/pkg/provider_set.go | 1 + internal/pkg/response.go | 26 ++ internal/pkg/utils_mongo/client.go | 29 ++ internal/server/router/router.go | 28 +- internal/services/advice/advicer_test.go | 110 +++++++ internal/services/advice/data.go | 60 ++++ .../services/{advice.go => advice/file.go} | 41 ++- internal/services/advicer_test.go | 15 - internal/services/provider_set.go | 3 +- 31 files changed, 1076 insertions(+), 232 deletions(-) create mode 100644 docker-compose.yml delete mode 100644 internal/biz/advice.go create mode 100644 internal/biz/advice_advicer.go create mode 100644 internal/biz/advice_file.go create mode 100644 internal/data/impl/advice_advicer_version_impl.go create mode 100644 internal/data/impl/advice_client_impl.go create mode 100644 internal/data/model/ai_advice_advicer_version.gen.go create mode 100644 internal/data/model/ai_advice_client.gen.go create mode 100644 internal/entitys/advicer_data.go create mode 100644 internal/pkg/mongo.go create mode 100644 internal/pkg/response.go create mode 100644 internal/pkg/utils_mongo/client.go create mode 100644 internal/services/advice/advicer_test.go create mode 100644 internal/services/advice/data.go rename internal/services/{advice.go => advice/file.go} (55%) delete mode 100644 internal/services/advicer_test.go diff --git a/README-test.md b/README-test.md index 161ea9a..459e555 100644 --- a/README-test.md +++ b/README-test.md @@ -1,4 +1,3 @@ -[https://p6-img.searchpstatp.com/tos-cn-i-vvloioitz3/6e5e76d274df2efabde9194a06f97e89~tplv-vvloioitz3-6:190:124.jpeg] - - -![图片](https://p6-img.searchpstatp.com/tos-cn-i-vvloioitz3/ab5ae998d8162b431f44fb2a0ed9ae33~tplv-vvloioitz3-6:190:124.jpeg) \ No newline at end of file +```json +{"advicer":{"dialectFeatures":{"region":"四川成都话","intensity":0.6,"KeyWords":null},"personalityTags":["耐心细致","专业严谨","善于对比","共情能力强"],"sentencePatterns":{"openingMode":["我给你介绍一下","我们先来看一下","我跟你说一下"],"explanationMode":["是这样的","我跟你讲","你发现没得","我跟你说"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?"],"summaryMode":["所以说","简单说就是","总而言之"],"transitionMode":["然后的话","再其次","还有一点","除此之外"]},"signatureDialogues":[{"context":"客户质疑地块太小","dialogue":"哥,14亩确实不大,但你要在成都是2.5环内城买房,这种是个普遍存在的一个现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个邦泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有管家送外卖、免费宠物喂养这些增值服务。你算一下,就算贵一块钱,十年也就多14000,但好物业让房子增值不止这点!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比28板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"}],"toneTags":{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.8,"persuasion":0.75}},"client":{"customer":[{"personalInfo":{"name":"唐先生","gender":"男","location":"成都北门","isFirstHome":false,"familyOrganize":"夫妻+1孩+父母同住"},"purchasePurpose":{"primaryPurpose":"改善居住条件","secondaryPurpose":"资产保值","decisionMakers":"夫妻双方"},"coreDemands":{"totalBudgetMin":350,"preferredLayout":"118㎡四房三卫双套房","coreAppeal":"在预算内满足家庭居住功能,确保房产保值"},"concerns":["总价超预算风险","板块保值能力","开发商资金实力","社区小,绿化空间有限"],"decisionProfile":["预算导向,严格控制总价","重点关注户型功能性和实用性","需要对比板块发展潜力","对开发商交付能力有顾虑"]},{"personalInfo":{"name":"冯女士","gender":"女","location":"成都","isFirstHome":false,"familyOrganize":"夫妻+1孩"},"purchasePurpose":{"primaryPurpose":"改善居住条件","secondaryPurpose":"资产保值","decisionMakers":"夫妻双方"},"coreDemands":{"totalBudgetMin":380,"preferredLayout":"118㎡四房三卫(非临路)","coreAppeal":"安静舒适、品质高端的改善型住房"},"concerns":["临路噪音影响","社区小,活动空间不足","未来周边施工影响","装修标准细节"],"decisionProfile":["对噪音敏感,需要安静环境","重视社区品质和舒适度","关注装修细节和硬件配置","需要详细对比不同户型差异"]}]},"project":{"competitionComparison":{"招商景成序":{"产品对比":"我们118平做四房三卫双套房,功能性更强","价格对比":"招商卖3万左右,我们卖33000,但我们地价比他们贵6000,其实利润更低","地段对比":"招商在28板块,我们在槐树店板块,是成华区number one板块,配套更成熟"},"邦泰云锦":{"价格参考":"他们当时12800拿地,现在卖34000","品质对比":"我们外立面全玻璃幕墙+铝单板,比他们成本高30%,装修标准也更高","定位相似":"邦泰也是首个项目,要打造口碑"},"龙湖滨江云河颂":{"价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们得房率118平实得132平,套内单价才33000,而且楼间距比他们开阔","优点承认":"龙湖位置确实好,看沙河公园,是龙湖最高端的滨江系列"}},"coreSellingPoints":{"产品配置高端":"全玻璃幕墙+铝单板外立面,三层中空氩气玻璃,3.2米层高,方太Y9烟机灶具、20套洗碗机、安吉尔净水器、高仪卫浴","地段稀缺性":"成华区2.5环内侧核心地段,槐树店板块是成华区number one板块,被三板桥、万象城、火车东站包围","得房率高":"118平实得132平,套内单价33000,比龙湖滨江云河颂套内单价低3000","户型功能性强":"118平做四房三卫双套房,满足三代人居住需求,南向采光好","物业高端":"招商铂金物业,前三年5块物业费,提供酒店式增值服务"},"developerBacking":{"company实力":"中兴资产,多元化民营企业","合作方":"招商铂金物业,招商首次与外部民营企业合作的高端物业","开发经验":"宜宾有5个项目,贵州有2个项目,成都是首个项目","资金安全":"在河南渑池有铝土一矿和二矿,每年稳定注入10亿资金"},"regionValue":{"区位层级":["成华区2.5环内侧,槐树店板块是成华区number one板块","北接三板桥商圈,西靠万象城,东临火车东站","被成都东门第一梯队板块包围,锦江区、高新的客户都首选"],"发展规划":["槐树店板块是攀成钢之后第二个富人区","整个板块都是300万到900万的总价段","未来全是改善型住宅,没有刚需盘"],"地价论证":["我们地价19500,华晨府20400,棕榈也是2万+","2.5环内现在地价没有低于19000的","面粉贵了,面包不可能便宜"],"板块热度":["从21年新希望锦麟一品开始,这边全是高端盘","龙湖最高端的滨江系列在这里,新希望的锦麟系列也在这里","各大品牌开发商争相恐后都在这边拿地"]},"supportingFacilities":{"交通配套":{"地铁":"双店路站300-350米(7号线),槐树店站550米(4号线),未来12号线","通达性":"到华西锦江院区30分钟车程,到万象城1.6公里,到三板桥1.8公里","道路":"中环路、成洛大道,到春熙路5个站,到火车东站2个站"},"医疗配套":{"三甲医院":"成都市第六人民医院、市二医院3公里内","便利性":"社区医院就在附近,日常就医方便","顶尖医疗":"华西医院锦江院区30分钟车程"},"商业配套":{"大型商圈":"万象城1.6公里,三板桥商圈1.8公里","未来商业":"槐树店上东里商业,明年年底开业,有永辉超市","社区商业":"成华奥园广场商业、十里风荷商业,满足日常消费"},"教育配套":{"小学":"城市附小锦汇东城小学,成华区生源最好的学校,周边新盘都读这个学校","幼儿园":"楼下公立幼儿园,明年9月招生","私立学校":"金苹果幼儿园,成华区英华英才学校"},"生态配套":{"公园":"楼下40亩槐树店公园,400米沙河公园,100亩多宝寺公园,700米内3个公园","绿化":"小区内有风雨连廊,口袋公园,周边是高端住宅绿化"}}},"skill":{"closingTechniques":{"优惠策略":{"价格优惠":["今天定的话,我可以跟领导申请额外折扣","买车位的话,总价多给两个点优惠","一次性付款再优惠一个点"],"附加价值":["优先选车位","享受招商物业的增值服务"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠优先通知你","今天不定的话,我帮你留意好楼层"]},"紧迫感营造":{"房源稀缺":["118只剩20多套了,好楼层不多","这栋楼就60户,卖一套少一套","特价房只有这几套,今天不定可能就没了"],"时间紧迫":["今天是月底最后一天,领导有压力价格可谈","我们刚刚开盘,还有额外优惠,月底冲业绩价格最有弹性"]}},"communicationRhythm":{"开场阶段":{"关键动作":"亲切称呼,简单寒暄,确认看房重点","时间占比":"5%","目标":"建立关系,了解需求"},"样板间带看":{"关键动作":"户型讲解→装修标准介绍→居住场景模拟","时间占比":"40%","目标":"体验产品细节"},"沙盘讲解":{"关键动作":"板块价值→周边配套→项目亮点→开发商介绍","时间占比":"30%","目标":"建立价值认知"},"洽谈阶段":{"关键动作":"算价→痛点应对→优惠讲解→促单","时间占比":"25%","目标":"解决顾虑,推动成交"}},"needsMining":{"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"],"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"]},"painPointResponse":{"临路噪音":{"户型优势":"双主卧和客厅朝中庭,只有书房和儿童房临路,影响小","硬件保障":"用的是三层中空氩气玻璃,隔音隔热效果比双层玻璃好一倍","距离说明":"我们楼盘离路有25米以上,还有23米的绿化隔离带"},"地块太小":{"对比竞品":"其他楼盘楼间距大多30米左右,我们的居住私密性和舒适度更好","承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩,339的邦泰才11亩","转化优势":"但人少安静,楼间距反而更开阔,我们楼间距最高有300米"},"物业费高":{"价值分析":"但6块里3块是增值服务,包括每年一次深度保洁、6次免费宠物喂养、夜间外卖送上门、生日宴布置等酒店式服务","价格补贴":"前三年补贴到5块,跟其他高端盘差不多,而且物业是招商铂金物业,服务过清华大学、最高人民法院","理解感受":"我懂你,我们也觉得物业费是有点贵"}},"valueBuilding":{"产品价值塑造":["我们是用改善的价格,买豪宅的标准,很多细节都是3000万豪宅才有的配置","外立面成本比竞品高30%,装修标准也是同面积段最高的","118平做四房三卫双套房,功能性在成都独一无二"],"地段价值塑造":["买房最重要的是地段、地段、还是地段","核心地段的核心资产才保值增值,2.5环内的地卖一块少一块","槐树店板块是攀成钢之后的第二个富人区,未来全是高端住宅"],"物业价值塑造":["招商铂金物业是央企物业,服务过清华大学、最高人民法院","增值服务每年能省几千块,而且好物业能让房子增值更多","前三年补贴后物业费5块,跟其他高端盘差不多,但服务更好"]}}} +``` \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 806c43d..fbcd581 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -20,7 +20,7 @@ func main() { log.Fatalf("加载配置失败: %v", err) } - app, cleanup, err := InitializeApp(bc, log.DefaultLogger()) + app, cleanup, err := InitializeApp(ctx, bc, log.DefaultLogger()) if err != nil { log.Fatalf("项目初始化失败: %v", err) } diff --git a/cmd/server/wire.go b/cmd/server/wire.go index ed710a3..b4f4437 100644 --- a/cmd/server/wire.go +++ b/cmd/server/wire.go @@ -16,6 +16,7 @@ import ( "ai_scheduler/internal/pkg" "ai_scheduler/internal/server" "ai_scheduler/internal/services" + "context" // "ai_scheduler/internal/tool_callback" "ai_scheduler/internal/tools" @@ -26,7 +27,7 @@ import ( ) // InitializeApp 初始化应用程序 -func InitializeApp(*config.Config, log.AllLogger) (*server.Servers, func(), error) { +func InitializeApp(ctx context.Context, *config.Config, log.AllLogger) (*server.Servers, func(), error) { panic(wire.Build( server.ProviderSetServer, workflow.ProviderSetWorkflow, diff --git a/config/config_test.yaml b/config/config_test.yaml index a6f07b9..003be9e 100644 --- a/config/config_test.yaml +++ b/config/config_test.yaml @@ -52,6 +52,14 @@ redis: db: driver: mysql source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +mongo: + source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai + maxPoolSize: 100 + minPoolSize: 10 + maxConnIdleTime: 30 + connectTimeout: 10 + socketTimeout: 30 + oss: access_key: "LTAI5tGGZzjf3tvqWk8SQj2G" secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq" diff --git a/deploy.sh b/deploy.sh index a9f6bd0..48bb268 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,10 +1,6 @@ -#export GO111MODULE=on -#export GOPROXY=https://goproxy.cn,direct -#export GOPATH=/root/go -#export GOCACHE=/root/.cache/go-build export CONTAINER_NAME=ai_scheduler export NETWORK_NAME=ai_scheduler_network -#export CGO_ENABLED='0' + MODE="$1" @@ -27,8 +23,7 @@ fi git fetch origin git checkout "$BRANCH" git pull origin "$BRANCH" -#go mod tidy -#make build + docker build -t ${CONTAINER_NAME} . docker stop ${CONTAINER_NAME} docker rm -f ${CONTAINER_NAME} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ab49b41 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,70 @@ +version: '3.8' + +services: + # MySQL 8.0 服务 + mysql: + image: mysql:8.0 + container_name: mysql_db + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword123} + MYSQL_DATABASE: ${MYSQL_DATABASE:-myapp} + MYSQL_USER: ${MYSQL_USER:-myuser} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-mypassword} + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf + networks: + - ai_scheduler_network + command: + --default-authentication-plugin=mysql_native_password + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci + --max_connections=1000 + + # MongoDB 服务 + mongodb: + image: mongo:latest + container_name: mongodb + restart: unless-stopped + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USERNAME:-root} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:-lsxd2026123} + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + - ./mongodb/mongod.conf:/etc/mongod.conf + command: + --config /etc/mongod.conf + --bind_ip_all # 允许所有IP连接 + networks: + - ai_scheduler_network + + # Redis 服务up + redis: + image: redis:alpine + container_name: redis + restart: unless-stopped + command: redis-server --requirepass ${REDIS_PASSWORD:-redispassword123} --bind 0.0.0.0 + ports: + - "6379:6379" + volumes: + - redis_data:/data + - ./redis/redis.conf:/usr/local/etc/redis/redis.conf + networks: + - ai_scheduler_network + +networks: + ai_scheduler_network: + driver: bridge + +volumes: + mysql_data: + driver: local + mongodb_data: + driver: local + redis_data: + driver: local \ No newline at end of file diff --git a/internal/biz/advice.go b/internal/biz/advice.go deleted file mode 100644 index 3f2eb1f..0000000 --- a/internal/biz/advice.go +++ /dev/null @@ -1,156 +0,0 @@ -package biz - -import ( - "ai_scheduler/internal/biz/llm_service/third_party" - "ai_scheduler/internal/entitys" - "context" - "encoding/json" - "fmt" - "os" - - "strings" - - "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" - "github.com/volcengine/volcengine-go-sdk/volcengine" -) - -type AdviceBiz struct { - hsyq *third_party.Hsyq -} - -func NewAdviceBiz(hsyq *third_party.Hsyq) *AdviceBiz { - return &AdviceBiz{ - hsyq: hsyq, - } -} - -const ( - key = "236ba4b6-9daa-4755-b22f-2fd274cd223a" - modelName = "doubao-seed-1-8-251228" -) - -var dataMap = map[string]string{ - "DialectFeatures": (&entitys.DialectFeatures{}).Example(), - "SentencePatterns": (&entitys.SentencePatterns{}).Example(), - "PersonalityTags": (&entitys.PersonalityTags{}).Example(), - "ToneTags": (&entitys.ToneTags{}).Example(), - "SignatureDialogues": (&entitys.SignatureDialogues{}).Example(), - "RegionValue": (&entitys.RegionValue{}).Example(), - "CompetitionComparison": (&entitys.CompetitionComparison{}).Example(), - "CoreSellingPoints": (&entitys.CoreSellingPoints{}).Example(), - "SupportingFacilities": (&entitys.SupportingFacilities{}).Example(), - "DeveloperBacking": (&entitys.DeveloperBacking{}).Example(), - "NeedsMining": (&entitys.NeedsMining{}).Example(), - "PainPointResponse": (&entitys.PainPointResponse{}).Example(), - "ValueBuilding": (&entitys.ValueBuilding{}).Example(), - "ClosingTechniques": (&entitys.ClosingTechniques{}).Example(), - "CommunicationRhythm": (&entitys.CommunicationRhythm{}).Example(), -} - -func (a *AdviceBiz) WordAna(ctx context.Context, wordContent string) error { - examples := a.getAllExamples() - prompt := a.buildSimplePrompt(wordContent, examples) - os.WriteFile("requset.json", []byte(prompt), 0644) - anaContent, err := a.callLlm(ctx, prompt) - if err != nil { - return err - } - os.WriteFile("res.json", []byte(anaContent), 0644) - data := a.parseResponse(anaContent) - - jsonData, _ := json.MarshalIndent(data, "", " ") - os.WriteFile("extracted.json", jsonData, 0644) - fmt.Println("✅ 数据已保存到 extracted.json") - return nil -} - -func (a *AdviceBiz) callLlm(ctx context.Context, prompt string) (string, error) { - var message = make([]*model.ChatCompletionMessage, 1) - message[0] = &model.ChatCompletionMessage{ - Role: model.ChatMessageRoleUser, - Content: &model.ChatCompletionMessageContent{ - StringValue: volcengine.String(prompt), - }, - } - - res, err := a.hsyq.RequestHsyq(ctx, key, modelName, message) - if err != nil { - return "", err - } - return *res.Choices[0].Message.Content.StringValue, nil -} - -func (a *AdviceBiz) getAllExamples() map[string]string { - return dataMap -} - -func (a *AdviceBiz) buildSimplePrompt(wordContent string, examples map[string]string) string { - // 最简单的提示词模板 - template := `分析以下房地产销售对话,按指定格式提取信息: - -对话内容: -%s - -请按照以下` + fmt.Sprintf("%d", len(examples)) + `个格式生成JSON数据,key为格式名称,value为对应值: - -%s - -输出要求: -1. 每个结构体一个JSON对象 -2. 所有内容必须严格基于提供的对话原文,不得编造 -3. 严格按照示例格式 -4. 将上述生成的` + fmt.Sprintf("%d", len(examples)) + `个JSON对象,json不需要有可读性,不要有特殊符号,比如"\n",用map[string]json来包裹所有json对象:{"SupportingFacilities":{...},"SignatureDialogues":[{...},{...}]}` - // 构建格式部分 - var formats strings.Builder - for name, example := range examples { - formats.WriteString(fmt.Sprintf("=== %s ===\n示例:%s\n\n", name, example)) - } - - return fmt.Sprintf(template, wordContent, formats.String()) -} - -func (a *AdviceBiz) parseResponse(response string) map[string]interface{} { - result := make(map[string]interface{}) - - // 按空行分割 - parts := strings.Split(response, "\n\n") - - for _, part := range parts { - part = strings.TrimSpace(part) - if part == "" || !strings.Contains(part, "{") { - continue - } - - // 找到第一个 { 和最后一个 } - start := strings.Index(part, "{") - end := strings.LastIndex(part, "}") - - if start == -1 || end == -1 || end <= start { - continue - } - - jsonStr := part[start : end+1] - - // 尝试解析 - var data interface{} - if err := json.Unmarshal([]byte(jsonStr), &data); err == nil { - // 判断是什么结构体 - for _, name := range getStructNames() { - if strings.Contains(jsonStr, `"`+name+`"`) || strings.Contains(part, name) { - result[name] = data - break - } - } - } - } - - return result -} - -func getStructNames() []string { - var res = make([]string, 0, len(dataMap)) - for k, _ := range dataMap { - res = append(res, k) - } - return res -} diff --git a/internal/biz/advice_advicer.go b/internal/biz/advice_advicer.go new file mode 100644 index 0000000..af49954 --- /dev/null +++ b/internal/biz/advice_advicer.go @@ -0,0 +1,76 @@ +package biz + +import ( + "ai_scheduler/internal/data/impl" + "ai_scheduler/internal/data/model" + "ai_scheduler/internal/entitys" + "time" + + "context" + + "xorm.io/builder" +) + +type AdviceAdvicerBiz struct { + advicerImpl *impl.AdviceAdvicerImpl + adviceAdvicerVersionImpl *impl.AdviceAdvicerVersionImpl +} + +func NewAdviceAdvicerBiz( + advicerImpl *impl.AdviceAdvicerImpl, + adviceAdvicerVersionImpl *impl.AdviceAdvicerVersionImpl, +) *AdviceAdvicerBiz { + return &AdviceAdvicerBiz{ + advicerImpl: advicerImpl, + adviceAdvicerVersionImpl: adviceAdvicerVersionImpl, + } +} + +func (a *AdviceAdvicerBiz) Update(ctx context.Context, data *entitys.AdvicerInitReq) error { + birth, err := time.Parse("2006-01-02", data.Birth) + if err != nil { + return err + } + param := &model.AiAdviceAdvicer{ + AdvicerID: data.AdvicerID, + Name: data.Name, + Birth: birth, + Gender: data.Gender, + WorkingYears: data.WorkingYears, + } + if param.AdvicerID == 0 { + _, err = a.advicerImpl.Add(param) + } else { + cond := builder.NewCond() + cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID}) + err = a.advicerImpl.UpdateByCond(&cond, param) + } + return err +} + +func (a *AdviceAdvicerBiz) List(ctx context.Context, data *entitys.AdvicerListReq) ([]map[string]interface{}, error) { + + cond := builder.NewCond() + cond = cond.And(builder.Eq{"project_id": data.ProjectId}) + list, err := a.advicerImpl.GetRange(&cond) + return list, err +} + +func (a *AdviceAdvicerBiz) VersionUpdate(ctx context.Context, param *entitys.AdvicerVersionInitReq) (err error) { + + if param.VersionID == 0 { + _, err = a.adviceAdvicerVersionImpl.Add(param) + } else { + cond := builder.NewCond() + cond = cond.And(builder.Eq{"version_id": param.VersionID}) + err = a.adviceAdvicerVersionImpl.UpdateByCond(&cond, param) + } + return err +} + +func (a *AdviceAdvicerBiz) VersionList(ctx context.Context, data *entitys.AdvicerVersionListReq) ([]map[string]interface{}, error) { + cond := builder.NewCond() + cond = cond.And(builder.Eq{"advicer_id": data.AdvicerID}) + list, err := a.adviceAdvicerVersionImpl.GetRange(&cond) + return list, err +} diff --git a/internal/biz/advice_file.go b/internal/biz/advice_file.go new file mode 100644 index 0000000..897843b --- /dev/null +++ b/internal/biz/advice_file.go @@ -0,0 +1,189 @@ +package biz + +import ( + "ai_scheduler/internal/biz/llm_service/third_party" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/gofiber/fiber/v2/log" + "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" + "github.com/volcengine/volcengine-go-sdk/volcengine" +) + +type AdviceFileBiz struct { + hsyq *third_party.Hsyq +} + +func NewAdviceFileBiz(hsyq *third_party.Hsyq) *AdviceFileBiz { + return &AdviceFileBiz{ + hsyq: hsyq, + } +} + +const ( + key = "236ba4b6-9daa-4755-b22f-2fd274cd223a" + fileModel = "doubao-seed-1-8-251228" + jsonModel = "doubao-seed-1-6-flash-250828" +) + +var DataMap = map[string]entitys.AdviceData{ + "dialectFeatures": &entitys.DialectFeatures{}, + "sentencePatterns": &entitys.SentencePatterns{}, + "personalityTags": &entitys.PersonalityTags{}, + "toneTags": &entitys.ToneTags{}, + "signatureDialogues": &entitys.SignatureDialogues{}, + "regionValue": &entitys.RegionValue{}, + "competitionComparison": &entitys.CompetitionComparison{}, + "coreSellingPoints": &entitys.CoreSellingPoints{}, + "supportingFacilities": &entitys.SupportingFacilities{}, + "developerBacking": &entitys.DeveloperBacking{}, + "needsMining": &entitys.NeedsMining{}, + "painPointResponse": &entitys.PainPointResponse{}, + "valueBuilding": &entitys.ValueBuilding{}, + "closingTechniques": &entitys.ClosingTechniques{}, + "communicationRhythm": &entitys.CommunicationRhythm{}, + "customer": &entitys.Customer{}, +} + +func (a *AdviceFileBiz) WordAna(ctx context.Context, wordContent string) (map[entitys.AdviceRole]map[string]entitys.AdviceData, error) { + timeSte := time.Now().Format("200601021504") + dir := "./cache/" + timeSte + os.Mkdir(dir, 0755) + //获取示例 + examples := a.getAllExamples() + + //构建提示词 + prompt := a.buildSimplePrompt(wordContent, examples) + os.WriteFile(dir+"/requset.json", []byte(prompt), 0644) + + //llm提取信息 + anaContent, err := a.callLlm(ctx, prompt, fileModel) + if err != nil { + return nil, err + } + os.WriteFile(dir+"/res.json", []byte(anaContent), 0644) + + //格式整理 + data, err := a.parseResponse(ctx, []byte(anaContent)) + if err != nil { + return nil, err + } + + //组装数据 + resData := a.cateData(data) + os.WriteFile("./cache/"+timeSte+"/extracted.json", pkg.JsonByteIgonErr(resData), 0644) + return resData, err +} + +func (a *AdviceFileBiz) cateData(data map[string]entitys.AdviceData) map[entitys.AdviceRole]map[string]entitys.AdviceData { + var res = make(map[entitys.AdviceRole]map[string]entitys.AdviceData) + for k, v := range data { + if _, ok := res[v.Role()]; !ok { + res[v.Role()] = make(map[string]entitys.AdviceData) + } + res[v.Role()][k] = v + } + return res +} + +func (a *AdviceFileBiz) parseResponse(ctx context.Context, responseByte []byte) (resultOutPut map[string]entitys.AdviceData, err error) { + //只尝试修复一次 + if isValid := json.Valid(responseByte); !isValid { + responseByte, err = a.fixJson(ctx, responseByte) + if err != nil { + return nil, fmt.Errorf("json格式错误,修复失败:%s", err.Error()) + } + } + if isValid := json.Valid(responseByte); !isValid { + return nil, fmt.Errorf("json格式错误") + } + + var ( + result map[string]interface{} + ) + + resultOutPut = make(map[string]entitys.AdviceData) + if err = json.Unmarshal(responseByte, &result); err != nil { + + return + } + for k, v := range result { + if _, ok := DataMap[k]; !ok { + return + } + var vbyte []byte + if vbyte, err = json.Marshal(v); err != nil { + return + } + newData := DataMap[k].Copy() + + if err = json.Unmarshal(vbyte, newData); err != nil { + return + } + resultOutPut[k] = newData + } + + return +} + +func (a *AdviceFileBiz) fixJson(ctx context.Context, json []byte) ([]byte, error) { + prompt := "你是一个专业的JSON修复专家。请帮我修复以下错误的JSON格式。\n\n要求:\n1. 保持原有数据的结构和内容不变\n2. 修复JSON语法错误\n3. 输出格式化的正确JSON\n4. 简要说明修复了哪些问题\n\n错误的JSON:\n" + string(json) + "\n\n请直接输出修复后的JSON。" + call, err := a.callLlm(ctx, prompt, jsonModel) + if err != nil { + return nil, err + } + + return []byte(call), nil +} + +func (a *AdviceFileBiz) callLlm(ctx context.Context, prompt string, modelName string) (string, error) { + var message = make([]*model.ChatCompletionMessage, 1) + message[0] = &model.ChatCompletionMessage{ + Role: model.ChatMessageRoleUser, + Content: &model.ChatCompletionMessageContent{ + StringValue: volcengine.String(prompt), + }, + } + + res, err := a.hsyq.RequestHsyq(ctx, key, modelName, message) + if err != nil { + return "", err + } + log.Info("token用量:", res.Usage.TotalTokens) + return *res.Choices[0].Message.Content.StringValue, nil +} + +func (a *AdviceFileBiz) getAllExamples() map[string]entitys.AdviceData { + return DataMap +} + +func (a *AdviceFileBiz) buildSimplePrompt(wordContent string, examples map[string]entitys.AdviceData) string { + // 最简单的提示词模板 + template := `分析以下房地产销售对话,按指定格式提取信息: + +对话内容: +%s + +请按照以下` + fmt.Sprintf("%d", len(examples)) + `个格式生成JSON数据,key为格式名称,value为对应值: + +%s + +输出要求: +1. 所有内容必须严格基于提供的对话原文,不得编造(重要!) +2. 每个结构体一个JSON对象 +3. 严格按照示例格式 +4. 将上述生成的` + fmt.Sprintf("%d", len(examples)) + `个JSON对象,json不需要有可读性,不要有特殊符号,比如"\n",用map[string]json来包裹所有json对象:{"SupportingFacilities":{...},"SignatureDialogues":[{...},{...}]}` + // 构建格式部分 + var formats strings.Builder + for name, example := range examples { + formats.WriteString(fmt.Sprintf("=== %s (%s:%s)===\n示例:%s\n\n", name, entitys.RoleDesc[example.Role()], example.Desc(), example.Example())) + } + + return fmt.Sprintf(template, wordContent, formats.String()) +} diff --git a/internal/biz/provider_set.go b/internal/biz/provider_set.go index 12eb9c3..cb50b0e 100644 --- a/internal/biz/provider_set.go +++ b/internal/biz/provider_set.go @@ -22,6 +22,7 @@ var ProviderSetBiz = wire.NewSet( NewQywxAppBiz, NewGroupConfigBiz, do.NewMacro, - NewAdviceBiz, + NewAdviceFileBiz, third_party.NewHsyq, + NewAdviceAdvicerBiz, ) diff --git a/internal/config/config.go b/internal/config/config.go index 64857a9..d9201dd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -21,6 +21,7 @@ type Config struct { Logging LoggingConfig `mapstructure:"logging"` Redis Redis `mapstructure:"redis"` DB DB `mapstructure:"db"` + Mongo Mongo `mapstructure:"mongo"` Oss Oss `mapstructure:"oss"` DefaultPrompt SysPrompt `mapstructure:"default_prompt"` PermissionConfig PermissionConfig `mapstructure:"permissionConfig"` @@ -165,6 +166,15 @@ type DB struct { IsDebug bool `mapstructure:"isDebug"` } +type Mongo struct { + Source string `mapstructure:"source"` + MaxPoolSize uint64 `mapstructure:"maxPoolSize"` + MinPoolSize uint64 `mapstructure:"minPoolSize"` + MaxConnIdleTime int32 `mapstructure:"maxConnIdleTime"` + ConnectTimeout int32 `mapstructure:"connectTimeout"` + SocketTimeout int32 `mapstructure:"socketTimeout"` +} + // Oss 阿里云OSS配置 type Oss struct { AccessKey string `mapstructure:"access_key"` diff --git a/internal/data/impl/advice_advicer_impl.go b/internal/data/impl/advice_advicer_impl.go index ffbbeaa..500fc00 100644 --- a/internal/data/impl/advice_advicer_impl.go +++ b/internal/data/impl/advice_advicer_impl.go @@ -8,10 +8,9 @@ import ( type AdviceAdvicerImpl struct { dataTemp.DataTemp - BaseRepository[model.AiTask] } -func NewAdviceAdvicerImplImpl(db *utils.Db) *AdviceAdvicerImpl { +func NewAdviceAdvicerImpl(db *utils.Db) *AdviceAdvicerImpl { return &AdviceAdvicerImpl{ DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceAdvicer)), } diff --git a/internal/data/impl/advice_advicer_version_impl.go b/internal/data/impl/advice_advicer_version_impl.go new file mode 100644 index 0000000..0f8eaa8 --- /dev/null +++ b/internal/data/impl/advice_advicer_version_impl.go @@ -0,0 +1,17 @@ +package impl + +import ( + "ai_scheduler/internal/data/model" + "ai_scheduler/tmpl/dataTemp" + "ai_scheduler/utils" +) + +type AdviceAdvicerVersionImpl struct { + dataTemp.DataTemp +} + +func NewAdviceAdvicerVersionImpl(db *utils.Db) *AdviceAdvicerVersionImpl { + return &AdviceAdvicerVersionImpl{ + DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceAdvicerVersion)), + } +} diff --git a/internal/data/impl/advice_client_impl.go b/internal/data/impl/advice_client_impl.go new file mode 100644 index 0000000..9e3b1f9 --- /dev/null +++ b/internal/data/impl/advice_client_impl.go @@ -0,0 +1,17 @@ +package impl + +import ( + "ai_scheduler/internal/data/model" + "ai_scheduler/tmpl/dataTemp" + "ai_scheduler/utils" +) + +type AdviceClientImpl struct { + dataTemp.DataTemp +} + +func NewAdviceClientImpl(db *utils.Db) *AdviceClientImpl { + return &AdviceClientImpl{ + DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceClient)), + } +} diff --git a/internal/data/impl/provider_set.go b/internal/data/impl/provider_set.go index 9c34713..588a21f 100644 --- a/internal/data/impl/provider_set.go +++ b/internal/data/impl/provider_set.go @@ -18,7 +18,9 @@ var ProviderImpl = wire.NewSet( NewBotGroupConfigImpl, NewBotGroupQywxImpl, NewReportDailyCacheImpl, - NewAdviceAdvicerImplImpl, + NewAdviceAdvicerImpl, NewAdviceProjectImpl, NewAdviceTalkImpl, + NewAdviceAdvicerVersionImpl, + NewAdviceClientImpl, ) diff --git a/internal/data/model/ai_advice_advicer.gen.go b/internal/data/model/ai_advice_advicer.gen.go index c525ec6..67f7035 100644 --- a/internal/data/model/ai_advice_advicer.gen.go +++ b/internal/data/model/ai_advice_advicer.gen.go @@ -12,19 +12,12 @@ const TableNameAiAdviceAdvicer = "ai_advice_advicer" // AiAdviceAdvicer mapped from table type AiAdviceAdvicer struct { - AdvicerID int32 `gorm:"column:advicer_id;primaryKey;autoIncrement:true" json:"advicer_id"` - Name string `gorm:"column:name;not null;comment:姓名" json:"name"` // 姓名 - Birth time.Time `gorm:"column:birth;not null;comment:用户名称" json:"birth"` // 用户名称 - Gender int32 `gorm:"column:gender;not null;comment:1:男,2:女" json:"gender"` // 1:男,2:女 - WorkingYears int32 `gorm:"column:working_years;not null;default:1;comment:工作年限" json:"working_years"` // 工作年限 - ContactTags string `gorm:"column:contact_tags;not null;comment:联系方式" json:"contact_tags"` // 联系方式 - NativeRegion string `gorm:"column:native_region;not null;comment:籍贯" json:"native_region"` // 籍贯 - DialectFeatures string `gorm:"column:dialect_features;not null;comment:语言风格" json:"dialect_features"` // 语言风格 - SentencePatterns string `gorm:"column:sentence_patterns;comment:句子模式" json:"sentence_patterns"` // 句子模式 - ToneTags string `gorm:"column:tone_tags;comment:语气标签" json:"tone_tags"` // 语气标签 - PersonalityTags string `gorm:"column:personality_tags;not null;comment:个性标签" json:"personality_tags"` // 个性标签 - SignatureDialogues string `gorm:"column:signature_dialogues;comment:代表性对话示例" json:"signature_dialogues"` // 代表性对话示例 - CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` + AdvicerID int32 `gorm:"column:advicer_id;primaryKey;autoIncrement:true" json:"advicer_id"` + Name string `gorm:"column:name;not null;comment:姓名" json:"name"` // 姓名 + Birth time.Time `gorm:"column:birth;not null;comment:用户名称" json:"birth"` // 用户名称 + Gender int32 `gorm:"column:gender;not null;comment:1:男,2:女" json:"gender"` // 1:男,2:女 + WorkingYears int32 `gorm:"column:working_years;not null;default:1;comment:工作年限" json:"working_years"` // 工作年限 + CreateAt *time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` } // TableName AiAdviceAdvicer's table name diff --git a/internal/data/model/ai_advice_advicer_version.gen.go b/internal/data/model/ai_advice_advicer_version.gen.go new file mode 100644 index 0000000..f0f489e --- /dev/null +++ b/internal/data/model/ai_advice_advicer_version.gen.go @@ -0,0 +1,29 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package model + +import ( + "time" +) + +const TableNameAiAdviceAdvicerVersion = "ai_advice_advicer_version" + +// AiAdviceAdvicerVersion mapped from table +type AiAdviceAdvicerVersion struct { + VersionID int32 `gorm:"column:version_id;primaryKey;autoIncrement:true" json:"version_id"` + AdvicerID int32 `gorm:"column:advicer_id;not null" json:"advicer_id"` + VersionDesc string `gorm:"column:version_desc;not null;comment:版本名称" json:"version_desc"` // 版本名称 + DialectFeatures string `gorm:"column:dialect_features;not null;comment:语言风格" json:"dialect_features"` // 语言风格 + SentencePatterns string `gorm:"column:sentence_patterns;comment:句子模式" json:"sentence_patterns"` // 句子模式 + ToneTags string `gorm:"column:tone_tags;comment:语气标签" json:"tone_tags"` // 语气标签 + PersonalityTags string `gorm:"column:personality_tags;not null;comment:个性标签" json:"personality_tags"` // 个性标签 + SignatureDialogues string `gorm:"column:signature_dialogues;comment:代表性对话示例" json:"signature_dialogues"` // 代表性对话示例 + CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` +} + +// TableName AiAdviceAdvicerVersion's table name +func (*AiAdviceAdvicerVersion) TableName() string { + return TableNameAiAdviceAdvicerVersion +} diff --git a/internal/data/model/ai_advice_client.gen.go b/internal/data/model/ai_advice_client.gen.go new file mode 100644 index 0000000..47d9a38 --- /dev/null +++ b/internal/data/model/ai_advice_client.gen.go @@ -0,0 +1,27 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package model + +import ( + "time" +) + +const TableNameAiAdviceClient = "ai_advice_client" + +// AiAdviceClient mapped from table +type AiAdviceClient struct { + ClientID int32 `gorm:"column:client_id;primaryKey;autoIncrement:true" json:"client_id"` + PersonalInfo string `gorm:"column:personal_info;comment:区域价值话术库" json:"personal_info"` // 区域价值话术库 + PurchasePurpose string `gorm:"column:purchase_purpose;comment:竞品对比话术" json:"purchase_purpose"` // 竞品对比话术 + CoreDemands string `gorm:"column:core_demands;comment:项目核心卖点" json:"core_demands"` // 项目核心卖点 + Concerns string `gorm:"column:concerns;comment:配套体系" json:"concerns"` // 配套体系 + DecisionProfile string `gorm:"column:decision_profile;comment:开发商背书" json:"decision_profile"` // 开发商背书 + CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` +} + +// TableName AiAdviceClient's table name +func (*AiAdviceClient) TableName() string { + return TableNameAiAdviceClient +} diff --git a/internal/entitys/advicer.go b/internal/entitys/advicer.go index 46dd76e..22968c0 100644 --- a/internal/entitys/advicer.go +++ b/internal/entitys/advicer.go @@ -1,7 +1,26 @@ package entitys -type WordAnaReq struct { - WordFileUrl string `json:"word_file_url"` +type AdviceData interface { + Example() string + Copy() AdviceData + Role() AdviceRole + Desc() string +} + +type AdviceRole string + +const ( + RoleAdvicer AdviceRole = "advicer" + RoleProject AdviceRole = "project" + RoleSkill AdviceRole = "skill" + RoleClient AdviceRole = "client" +) + +var RoleDesc = map[AdviceRole]string{ + RoleAdvicer: "顾问", + RoleProject: "项目", + RoleSkill: "沟通技巧", + RoleClient: "客户", } // -------顾问 @@ -17,6 +36,18 @@ func (e *DialectFeatures) Example() string { return `{"region":"四川成都话","intensity":0.4,"key_words":["噻","要得","没得","不晓得","是不是"]}` } +func (e *DialectFeatures) Copy() AdviceData { + return new(DialectFeatures) +} + +func (e *DialectFeatures) Role() AdviceRole { + return RoleAdvicer +} + +func (e *DialectFeatures) Desc() string { + return "方言特征" +} + // SentencePatterns 句子模式 type SentencePatterns struct { OpeningMode []string `json:"openingMode"` //开场模式 @@ -30,12 +61,35 @@ func (e *SentencePatterns) Example() string { return `{"openingMode":["我给你介绍一下","我们先来看一下"],"explanationMode":["是这样的","我跟你讲","你发现没得"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?"],"summaryMode":["所以说","简单说就是"],"transitionMode":["然后的话","再其次","还有一点"]}` } +func (e *SentencePatterns) Copy() AdviceData { + return new(SentencePatterns) +} + +func (e *SentencePatterns) Role() AdviceRole { + return RoleAdvicer +} + +func (e *SentencePatterns) Desc() string { + return "句子模式" +} + // PersonalityTags 个性标签 type PersonalityTags []string func (e *PersonalityTags) Example() string { return `["耐心细致","细节控"]` } +func (e *PersonalityTags) Copy() AdviceData { + return new(PersonalityTags) +} + +func (e *PersonalityTags) Role() AdviceRole { + return RoleAdvicer +} + +func (e *PersonalityTags) Desc() string { + return "个性标签" +} // ToneTags 语气标签 type ToneTags struct { @@ -50,8 +104,20 @@ func (e *ToneTags) Example() string { return `{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.75,"persuasion":0.7}` } +func (e *ToneTags) Copy() AdviceData { + return new(ToneTags) +} + +func (e *ToneTags) Role() AdviceRole { + return RoleAdvicer +} + +func (e *ToneTags) Desc() string { + return "语气标签" +} + // SignatureDialogues 代表性对话示例 -type SignatureDialogues struct { +type SignatureDialogues []struct { Context string `json:"context"` Dialogue string `json:"dialogue"` //解释 } @@ -60,36 +126,94 @@ func (e *SignatureDialogues) Example() string { return `[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都是2.5环内城买房,这种是个普遍存在的一个现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有管家送外卖、免费宠物喂养这些增值服务。你算一下,就算贵一块钱,十年也就多14000,但好物业让房子增值不止这点!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比28板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"}]` } +func (e *SignatureDialogues) Copy() AdviceData { + return new(SignatureDialogues) +} + +func (e *SignatureDialogues) Role() AdviceRole { + return RoleAdvicer +} + +func (e *SignatureDialogues) Desc() string { + return "代表性对话示例" +} + // -------项目 // RegionValue 区域价值话术库 -type RegionValue map[string]string +type RegionValue map[string][]string func (e *RegionValue) Example() string { return `{"区位层级":["成华区2.5环内侧,这个位置真的稀缺","槐树店板块现在是成华区的number one板块","北接三板桥商圈,西靠万象城,东临火车东站","属于淮舜板块,万象城东的核心位置"],"地价论证":["我们地价19500,华晨府20400,棕榈也是2万+","2.5环内现在地价没有低于19000的","面粉贵了,面包不可能便宜"],"板块热度":["从21年新希望锦麟一品开始,这边全是高端盘","龙湖最高端的滨江系列在这里,新希望的锦麟系列也在这里","各大品牌开发商争相恐后都在这边拿地"],"发展规划":["槐树店板块是棋盘成钢之后第二个富人区","整个板块都是300万到900万的总价段","未来全是改善型住宅,没有刚需盘"]}` } +func (e *RegionValue) Copy() AdviceData { + return new(RegionValue) +} + +func (e *RegionValue) Role() AdviceRole { + return RoleProject +} + +func (e *RegionValue) Desc() string { + return "区域价值话术" +} // CompetitionComparison 竞品对比话术 -type CompetitionComparison map[string]string +type CompetitionComparison map[string]map[string]string func (e *CompetitionComparison) Example() string { return `{"龙湖滨江云河颂":{"优点承认":"龙湖位置确实好,看沙河公园","价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们得房率118平实得132平,套内单价才33000"},"邦泰云锦":{"定位相似":"邦泰也是首个项目,要打造口碑","价格参考":"他们当时12800拿地,现在卖34000","品质对比":"我们外立面全玻璃幕墙,比他们成本高30%"}}` } -// CoreSellingPoints 竞品对比话术 +func (e *CompetitionComparison) Copy() AdviceData { + return new(CompetitionComparison) +} + +func (e *CompetitionComparison) Role() AdviceRole { + return RoleProject +} + +func (e *CompetitionComparison) Desc() string { + return "竞品对比话术" +} + +// CoreSellingPoints 核心卖点 type CoreSellingPoints map[string]string func (e *CoreSellingPoints) Example() string { - return `{"龙湖滨江云河颂":{"优点承认":"龙湖位置确实好,看沙河公园","价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们得房率118平实得132平,套内单价才33000"},"邦泰云锦":{"定位相似":"邦泰也是首个项目,要打造口碑","价格参考":"他们当时12800拿地,现在卖34000","品质对比":"我们外立面全玻璃幕墙,比他们成本高30%"}` + return `{"产品配置高端":"全玻璃幕墙+铝单板外立面,三层中空氩气玻璃,3.2米层高,方太Y9烟机灶具,高仪卫浴","地段稀缺性":"成华区2.5环内侧核心地段,槐树店板块是成华区number one板块,被三板桥、万象城、火车东站包围","得房率高":"118平实得132平,套内单价33000,比龙湖滨江云河颂套内单价低3000"}` +} +func (e *CoreSellingPoints) Copy() AdviceData { + return new(CoreSellingPoints) +} + +func (e *CoreSellingPoints) Role() AdviceRole { + return RoleProject +} + +func (e *CoreSellingPoints) Desc() string { + return "核心卖点" } // SupportingFacilities 配套体系 -type SupportingFacilities map[string]string +type SupportingFacilities map[string]map[string]string func (e *SupportingFacilities) Example() string { return `{"交通配套":{"地铁":"双店路站350米(7号线),槐树店站550米(4号线),未来12号线","道路":"中环路、成洛大道,到春熙路5个站","通达性":"到火车东站2个站,到华西30分钟内"},"教育配套":{"幼儿园":"楼下公立幼儿园","小学":"城市附小锦汇东城(成华区生源最好的学校)","生源优势":"周边新盘都是300万+,生源纯粹"},"医疗配套":{"三甲医院":"市六医院、市二医院3公里内","顶尖医疗":"华西医院锦江院区30分钟车程","便利性":"到华西本部也是30分钟内"}}` } +func (e *SupportingFacilities) Copy() AdviceData { + return new(SupportingFacilities) +} + +func (e *SupportingFacilities) Role() AdviceRole { + return RoleProject +} + +func (e *SupportingFacilities) Desc() string { + return "配套体系" +} + // DeveloperBacking 开发商背书 type DeveloperBacking map[string]string @@ -97,39 +221,167 @@ func (e *DeveloperBacking) Example() string { return `{"公司实力":"中信资产,多元化民营企业","资金安全":"在河南渑池有铝土矿,每年稳定收入10亿","开发经验":"宜宾有5个项目,贵州2个,成都是首个项目","合作方":"招商铂金物业,首次与外部企业合作"}` } +func (e *DeveloperBacking) Copy() AdviceData { + return new(DeveloperBacking) +} + +func (e *DeveloperBacking) Role() AdviceRole { + return RoleProject +} + +func (e *DeveloperBacking) Desc() string { + return "开发商背书" +} + // -------销售话术 // NeedsMining 需求挖掘话术 -type NeedsMining map[string]string +type NeedsMining map[string][]string func (e *NeedsMining) Example() string { return `{"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"],"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"]}` } +func (e *NeedsMining) Copy() AdviceData { + return new(NeedsMining) +} + +func (e *NeedsMining) Role() AdviceRole { + return RoleSkill +} + +func (e *NeedsMining) Desc() string { + return "需求挖掘话术" +} + // PainPointResponse 痛点应对策略 -type PainPointResponse map[string]string +type PainPointResponse map[string]map[string]string func (e *PainPointResponse) Example() string { return `{"地块太小":{"承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩","转化优势":"但人少安静,楼间距反而更开阔","对比竞品":"339的邦泰才11亩,人家上千万豪宅"},"物业费高":{"理解感受":"我懂你,我们也觉得有点贵","价值分析":"但6块里3块是增值服务(保洁、送外卖)","价格补贴":"前三年补贴到5块,跟其他盘差不多"}}` } +func (e *PainPointResponse) Copy() AdviceData { + return new(PainPointResponse) +} + +func (e *PainPointResponse) Role() AdviceRole { + return RoleSkill +} + +func (e *PainPointResponse) Desc() string { + return "痛点应对策略" +} // ValueBuilding 价值塑造技巧 -type ValueBuilding map[string]string +type ValueBuilding map[string][]string func (e *ValueBuilding) Example() string { return `{"地段价值塑造":["买房最重要的是地段、地段、还是地段","核心地段的核心资产才保值增值","2.5环内的地卖一块少一块,不可再生"],"产品价值塑造":["我们是用改善的价格,买豪宅的标准","很多细节都是3000万豪宅才有的配置","外立面成本比竞品高30%,但单价差不多"]}` } +func (e *ValueBuilding) Copy() AdviceData { + return new(ValueBuilding) +} + +func (e *ValueBuilding) Role() AdviceRole { + return RoleSkill +} + +func (e *ValueBuilding) Desc() string { + return "价值塑造技巧" +} + // ClosingTechniques 促单话术 -type ClosingTechniques map[string]string +type ClosingTechniques map[string]map[string][]string func (e *ClosingTechniques) Example() string { return `{"紧迫感营造":{"时间紧迫":["今天是月底最后一天,领导有压力价格可谈","我们刚刚开盘,还有额外优惠","月底冲业绩,价格最有弹性"],"房源稀缺":["118只剩20多套了,好楼层不多","这栋楼就60户,卖一套少一套","特价房只有这几套,今天不定可能就没了"]},"优惠策略":{"价格优惠":["今天定的话,我可以跟领导申请额外折扣","买车位的话,总价多给两个点优惠","一次性付款再优惠一个点"],"附加价值":["送一年物业费","送品牌家电礼包","优先选车位"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠优先通知你","今天不定的话,我帮你留意好楼层"]}}` } +func (e *ClosingTechniques) Copy() AdviceData { + return new(ClosingTechniques) +} + +func (e *ClosingTechniques) Role() AdviceRole { + return RoleSkill +} + +func (e *ClosingTechniques) Desc() string { + return "促单话术" +} + // CommunicationRhythm 沟通节奏控制 -type CommunicationRhythm map[string]string +type CommunicationRhythm map[string]map[string]string func (e *CommunicationRhythm) Example() string { return `{"开场阶段":{"时间占比":"5%","目标":"建立关系,了解需求","关键动作":"亲切称呼,简单寒暄,确认看房重点"},"沙盘讲解":{"时间占比":"30%","目标":"建立价值认知","关键动作":"板块价值→周边配套→项目亮点→开发商介绍"}}` } + +func (e *CommunicationRhythm) Copy() AdviceData { + return new(CommunicationRhythm) +} + +func (e *CommunicationRhythm) Role() AdviceRole { + return RoleSkill +} + +func (e *CommunicationRhythm) Desc() string { + return "沟通节奏控制" +} + +//----------客户 + +// Customer 客户信息 +type Customer []ClientInfo +type ClientInfo struct { + // 个人信息 + PersonalInfo PersonalInfo `json:"personalInfo"` + + // 购房目的 + PurchasePurpose PurchasePurpose `json:"purchasePurpose"` + + // 核心需求 + CoreDemands CoreDemands `json:"coreDemands"` + + // 关注点与顾虑 + Concerns []string `json:"concerns"` + + // 决策建议 + DecisionProfile []string `json:"decisionProfile"` +} + +type PersonalInfo struct { + Name string `json:"name"` // 姓氏 + Gender string `json:"gender"` // 性别 + Location string `json:"location"` // 来源地/当前居住地 + IsFirstHome bool `json:"isFirstHome"` // 是否首套房 + FamilyOrganize string `json:"familyOrganize"` // 家庭人数 +} + +type PurchasePurpose struct { + PrimaryPurpose string `json:"primaryPurpose"` // 主要目的 + SecondaryPurpose string `json:"secondaryPurpose"` // 次要目的 + DecisionMakers string `json:"decisionMakers"` // 决策人 +} + +type CoreDemands struct { + TotalBudget string `json:"totalBudget"` // 预算范围 + PreferredLayout string `json:"preferredLayout"` // 偏好户型 + CoreAppeal string `json:"coreAppeal"` // 核心述求 +} + +func (e *Customer) Example() string { + return `[{"personalInfo":{"name":"唐","gender":"男","location":"成都北门","isFirstHome":true,"familyOrganize":"夫妻+1孩+父母同住"},"purchasePurpose":{"primaryPurpose":"首次置业,解决自住","secondaryPurpose":"资产保值,未来可出租","decisionMakers":"夫妻双方"},"coreDemands":{"totalBudget":"350-400"","preferredLayout":"118㎡四房三卫双套房","coreAppeal":"在有限预算内满足家庭居住功能,确保房产保值"},"concerns":["总价超预算风险","板块保值能力","未来租金回报率","开发商资金实力"],"decisionProfile":["预算导向,严格控制总价","重点关注户型功能性和实用性","需要对比板块发展潜力","对开发商交付能力有顾虑"]},{"personalInfo":{"name":"冯女士","gender":"女","location":"","isFirstHome":false,"familyOrganize":"夫妻+1孩+父母同住"},"purchasePurpose":{"primaryPurpose":"改善居住条件","secondaryPurpose":"子女教育质量提升","decisionMakers":"夫妻双方需家庭共同商议]},"coreDemands":{"totalBudget":"400-500","preferredLayout":"118㎡四房三卫(非全景户型)","coreAppeal":"安静舒适、学区有保障的改善型住房"},"concerns":["临路噪音影响老人休息","学区质量和稳定性","社区小,绿化空间有限","得房率是否足够","二八板块学区对比"],"decisionProfile":["对噪音敏感,需要安静环境","重视教育资源配置","关注社区品质和舒适度","需要详细对比不同板块学区优势"]}]` +} + +func (e *Customer) Copy() AdviceData { + return new(Customer) +} + +func (e *Customer) Role() AdviceRole { + return RoleClient +} + +func (e *Customer) Desc() string { + return "客户信息" +} diff --git a/internal/entitys/advicer_data.go b/internal/entitys/advicer_data.go new file mode 100644 index 0000000..ed72545 --- /dev/null +++ b/internal/entitys/advicer_data.go @@ -0,0 +1,29 @@ +package entitys + +type AdvicerInitReq struct { + AdvicerID int32 `json:"advicer_id"` + ProjectID int32 `json:"project_id"` + Name string `json:"name"` // 姓名 + Birth string `json:"birth"` // 用户名称 + Gender int32 `json:"gender"` // 1:男,2:女 + WorkingYears int32 `json:"working_years"` // 工作年限 +} + +type AdvicerListReq struct { + ProjectId int32 `json:"project_id"` +} + +type AdvicerVersionInitReq struct { + VersionID int32 `json:"version_id"` + AdvicerID int32 `json:"advicer_id"` + VersionDesc string `json:"version_desc"` // 版本名称 + DialectFeatures string `json:"dialect_features"` // 语言风格 + SentencePatterns string `json:"sentence_patterns"` // 句子模式 + ToneTags string `json:"tone_tags"` // 语气标签 + PersonalityTags string `json:"personality_tags"` // 个性标签 + SignatureDialogues string `json:"signature_dialogues"` // 代表性对话示例 +} + +type AdvicerVersionListReq struct { + AdvicerID int32 `json:"advicer_id"` +} diff --git a/internal/entitys/bot.go b/internal/entitys/bot.go index 03b1a09..23071b9 100644 --- a/internal/entitys/bot.go +++ b/internal/entitys/bot.go @@ -6,6 +6,10 @@ import ( "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" ) +type WordAnaReq struct { + WordFileUrl string `json:"word_file_url"` +} + type RequireDataDingTalkBot struct { Histories []model.AiChatHi UserInfo *DingTalkUserInfo diff --git a/internal/pkg/mongo.go b/internal/pkg/mongo.go new file mode 100644 index 0000000..c6dfe76 --- /dev/null +++ b/internal/pkg/mongo.go @@ -0,0 +1,35 @@ +package pkg + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/utils_mongo" + "context" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/mongo" +) + +type Mongo struct { + Client *mongo.Client +} + +func NewMongoDb(ctx context.Context, c *config.Config) (*Mongo, func()) { + transDBClient, err := utils_mongo.NewMongoClient(ctx, &utils_mongo.ClientStruct{ + Uri: c.Mongo.Source, + MaxPoolSize: c.Mongo.MaxPoolSize, + MinPoolSize: c.Mongo.MinPoolSize, + MaxConnIdleTime: time.Duration(c.Mongo.MaxConnIdleTime) * time.Minute, + ConnectTimeout: time.Duration(c.Mongo.ConnectTimeout) * time.Second, + SocketTimeout: time.Duration(c.Mongo.SocketTimeout) * time.Second, + }) + + if err != nil { + panic(fmt.Sprintf("mongo数据库错误: %v", err)) + } + return &Mongo{ + Client: transDBClient, + }, func() { + transDBClient.Disconnect(ctx) + } +} diff --git a/internal/pkg/provider_set.go b/internal/pkg/provider_set.go index 16bef95..b791bb6 100644 --- a/internal/pkg/provider_set.go +++ b/internal/pkg/provider_set.go @@ -24,4 +24,5 @@ var ProviderSetClient = wire.NewSet( utils_oss.NewClient, lsxd.NewLogin, + NewMongoDb, ) diff --git a/internal/pkg/response.go b/internal/pkg/response.go new file mode 100644 index 0000000..b65b780 --- /dev/null +++ b/internal/pkg/response.go @@ -0,0 +1,26 @@ +package pkg + +import ( + "encoding/json" + "fmt" + + "github.com/gofiber/fiber/v2" +) + +func HandleResponse(c *fiber.Ctx, data interface{}, e error) (err error) { + if e != nil { + return e + } + switch data.(type) { + case error: + err = data.(error) + case int, int32, int64, float32, float64, string, bool: + c.Response().SetBody([]byte(fmt.Sprintf("%s", data))) + case []byte: + c.Response().SetBody(data.([]byte)) + default: + dataByte, _ := json.Marshal(data) + c.Response().SetBody(dataByte) + } + return +} diff --git a/internal/pkg/utils_mongo/client.go b/internal/pkg/utils_mongo/client.go new file mode 100644 index 0000000..8271e88 --- /dev/null +++ b/internal/pkg/utils_mongo/client.go @@ -0,0 +1,29 @@ +package utils_mongo + +import ( + "context" + "time" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type ClientStruct struct { + Uri string + MaxPoolSize uint64 + MinPoolSize uint64 + MaxConnIdleTime time.Duration + ConnectTimeout time.Duration + SocketTimeout time.Duration +} + +func NewMongoClient(ctx context.Context, config *ClientStruct) (*mongo.Client, error) { + clientOptions := options.Client().ApplyURI(config.Uri). + SetMaxPoolSize(config.MaxPoolSize). // 最大连接数 + SetMinPoolSize(config.MinPoolSize). // 最小连接数 + SetMaxConnIdleTime(config.MaxConnIdleTime). // 连接最大空闲时间 + SetConnectTimeout(config.ConnectTimeout). // 连接超时 + SetSocketTimeout(config.ConnectTimeout) // 操作超时 + + return mongo.Connect(ctx, clientOptions) +} diff --git a/internal/server/router/router.go b/internal/server/router/router.go index 843a8a7..b7a6131 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -4,6 +4,7 @@ import ( errors "ai_scheduler/internal/data/error" "ai_scheduler/internal/gateway" "ai_scheduler/internal/services" + "ai_scheduler/internal/services/advice" "encoding/json" "fmt" "strings" @@ -26,7 +27,7 @@ type RouterServer struct { // SetupRoutes 设置路由 func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionService *services.SessionService, task *services.TaskService, gateway *gateway.Gateway, callbackService *services.CallbackService, chatHist *services.HistoryService, - capabilityService *services.CapabilityService, advice *services.AdviceService, + capabilityService *services.CapabilityService, adviceFile *advice.FileService, adviceData *advice.DataService, ) { app.Use(func(c *fiber.Ctx) error { // 设置 CORS 头 @@ -100,7 +101,30 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi r.Post("/capability/product/ingest/:thread_id/confirm", capabilityService.ProductIngestConfirm) // 商品数据提取确认 advicer := r.Group("advice/") - advicer.Post("file/word", advice.WordAna) + advicer.Post("file/word/ana", adviceFile.WordAna) + //顾问 + advicer.Post("file/advicer/add", adviceData.AdvicerAdd) + advicer.Post("file/advicer/update", adviceData.AdvicerUpdate) + advicer.Post("file/advicer/list", adviceData.AdvicerList) + advicer.Post("file/advicer/version/add", adviceData.AdvicerVersionAdd) + advicer.Post("file/advicer/version/update", adviceFile.WordAna) + advicer.Post("file/advicer/version/del", adviceFile.WordAna) + advicer.Post("file/advicer/version/list", adviceFile.WordAna) + //聊天技巧 + advicer.Post("file/skill/list", adviceFile.WordAna) + advicer.Post("file/skill/init", adviceFile.WordAna) + advicer.Post("file/skill/add", adviceFile.WordAna) + advicer.Post("file/skill/update", adviceFile.WordAna) + advicer.Post("file/skill/del", adviceFile.WordAna) + advicer.Post("file/skill/list", adviceFile.WordAna) + //项目 + advicer.Post("file/project/init", adviceFile.WordAna) + advicer.Post("file/project/add", adviceFile.WordAna) + advicer.Post("file/project/update", adviceFile.WordAna) + //客户 + advicer.Post("file/client/init", adviceFile.WordAna) + advicer.Post("file/client/add", adviceFile.WordAna) + advicer.Post("file/client/update", adviceFile.WordAna) } func routerSocket(app *fiber.App, chatService *services.ChatService) { diff --git a/internal/services/advice/advicer_test.go b/internal/services/advice/advicer_test.go new file mode 100644 index 0000000..3ced2d2 --- /dev/null +++ b/internal/services/advice/advicer_test.go @@ -0,0 +1,110 @@ +package advice + +import ( + "ai_scheduler/internal/biz" + "ai_scheduler/internal/biz/llm_service/third_party" + "ai_scheduler/internal/config" + "ai_scheduler/internal/data/impl" + "ai_scheduler/internal/entitys" + "ai_scheduler/utils" + + "encoding/json" + + "os" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func Test_WordAna(t *testing.T) { + Run(nil) + ana, err := file.WordAnat("https://attachment-public.oss-cn-hangzhou.aliyuncs.com/ai-scheduler/data-analytics/word/content3.docx") + t.Log(ana, err) +} + +func Test_AdvicerInit(t *testing.T) { + reqBody := `{"advicer_id": 124, "name": "张三111", "birth": "1990-01-01", "gender": 1, "working_years": 10}` + Run([]byte(reqBody)) + + err := advicer.AdvicerUpdate(fiberCtx) + t.Log(err) +} + +func Test_Json(t *testing.T) { + responseByte, err := os.ReadFile("./res.json") + if err != nil { + panic(err) + } + var ( + result map[string]interface{} + res = make(map[string]entitys.AdviceData) + ) + + if err = json.Unmarshal(responseByte, &result); err != nil { + panic(err) + } + for k, v := range result { + if _, ok := dataMap[k]; !ok { + continue + } + var vbyte []byte + if vbyte, err = json.Marshal(v); err != nil { + panic(err) + } + newData := dataMap[k].Copy() + + if err = json.Unmarshal(vbyte, newData); err != nil { + panic(err) + } + res[k] = newData + } + t.Log(result) +} + +var ( + file *FileService + advicer *DataService + configConfig *config.Config + fiberCtx *fiber.Ctx +) + +// run 函数是程序的入口函数,负责初始化和配置各个组件 +func Run(reqBody []byte) { + if reqBody != nil { + app := fiber.New() + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod("POST") + fctx.Request.SetBody(reqBody) + fctx.Request.Header.SetContentType("application/json") + fiberCtx = app.AcquireCtx(fctx) + } + configConfig, _ = config.LoadConfigWithEnv() + // 初始化数据库连接 + db, _ := utils.NewGormDb(configConfig) + advicerImpl := impl.NewAdviceAdvicerImpl(db) + advicerVersionImpl := impl.NewAdviceAdvicerVersionImpl(db) + hsyq := third_party.NewHsyq() + advicerfilebiz := biz.NewAdviceFileBiz(hsyq) + advicerbiz := biz.NewAdviceAdvicerBiz(advicerImpl, advicerVersionImpl) + file = NewFileService(advicerfilebiz, configConfig) + advicer = NewDataService(advicerbiz, configConfig) +} + +var dataMap = map[string]entitys.AdviceData{ + "DialectFeatures": &entitys.DialectFeatures{}, + "SentencePatterns": &entitys.SentencePatterns{}, + "PersonalityTags": &entitys.PersonalityTags{}, + "ToneTags": &entitys.ToneTags{}, + "SignatureDialogues": &entitys.SignatureDialogues{}, + "RegionValue": &entitys.RegionValue{}, + "CompetitionComparison": &entitys.CompetitionComparison{}, + "CoreSellingPoints": &entitys.CoreSellingPoints{}, + "SupportingFacilities": &entitys.SupportingFacilities{}, + "DeveloperBacking": &entitys.DeveloperBacking{}, + "NeedsMining": &entitys.NeedsMining{}, + "PainPointResponse": &entitys.PainPointResponse{}, + "ValueBuilding": &entitys.ValueBuilding{}, + "ClosingTechniques": &entitys.ClosingTechniques{}, + "CommunicationRhythm": &entitys.CommunicationRhythm{}, +} diff --git a/internal/services/advice/data.go b/internal/services/advice/data.go new file mode 100644 index 0000000..fc12207 --- /dev/null +++ b/internal/services/advice/data.go @@ -0,0 +1,60 @@ +package advice + +import ( + "ai_scheduler/internal/biz" + "ai_scheduler/internal/config" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + + "github.com/gofiber/fiber/v2" +) + +// DataService 数据处理 +type DataService struct { + adviceBiz *biz.AdviceAdvicerBiz + cfg *config.Config +} + +// NewDataService +func NewDataService( + adviceBiz *biz.AdviceAdvicerBiz, + cfg *config.Config, +) *DataService { + return &DataService{ + adviceBiz: adviceBiz, + cfg: cfg, + } +} + +func (d *DataService) AdvicerAdd(c *fiber.Ctx) error { + req := &entitys.AdvicerInitReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.adviceBiz.Update(c.UserContext(), req) +} + +func (d *DataService) AdvicerUpdate(c *fiber.Ctx) error { + req := &entitys.AdvicerInitReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.adviceBiz.Update(c.UserContext(), req) +} + +func (d *DataService) AdvicerList(c *fiber.Ctx) error { + req := &entitys.AdvicerListReq{} + if err := c.BodyParser(req); err != nil { + return err + } + list, err := d.adviceBiz.List(c.UserContext(), req) + return pkg.HandleResponse(c, list, err) +} + +func (d *DataService) AdvicerVersionAdd(c *fiber.Ctx) error { + req := &entitys.AdvicerVersionInitReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.adviceBiz.VersionUpdate(c.UserContext(), req) +} diff --git a/internal/services/advice.go b/internal/services/advice/file.go similarity index 55% rename from internal/services/advice.go rename to internal/services/advice/file.go index 9ac7352..6152525 100644 --- a/internal/services/advice.go +++ b/internal/services/advice/file.go @@ -1,9 +1,10 @@ -package services +package advice import ( "ai_scheduler/internal/biz" "ai_scheduler/internal/config" "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg/file_download" "context" "errors" @@ -13,24 +14,24 @@ import ( "github.com/gofiber/fiber/v2" ) -// ChatHandler 聊天处理器 -type AdviceService struct { - adviceBiz *biz.AdviceBiz +// FileService 文件处理 +type FileService struct { + adviceBiz *biz.AdviceFileBiz cfg *config.Config } -// NewChatHandler 创建聊天处理器 -func NewAdviceService( - adviceBiz *biz.AdviceBiz, +// NewFileService +func NewFileService( + adviceBiz *biz.AdviceFileBiz, cfg *config.Config, -) *AdviceService { - return &AdviceService{ +) *FileService { + return &FileService{ adviceBiz: adviceBiz, cfg: cfg, } } -func (a *AdviceService) WordAna(c *fiber.Ctx) error { +func (a *FileService) WordAna(c *fiber.Ctx) error { req := &entitys.WordAnaReq{} if err := c.BodyParser(req); err != nil { return err @@ -44,19 +45,29 @@ func (a *AdviceService) WordAna(c *fiber.Ctx) error { if err != nil { return err } - return a.adviceBiz.WordAna(c.UserContext(), result) + ana, err := a.adviceBiz.WordAna(context.Background(), result) + if err != nil { + return err + } + err = c.JSON(ana) + return err } -func (a *AdviceService) WordAnat(path string) error { +func (a *FileService) WordAnat(path string) ([]byte, error) { // URL 解码 fileURL, err := url.PathUnescape(path) if err != nil { - return errors.New("URL 解码失败") + return nil, errors.New("URL 解码失败") } result, _, err := file_download.GetWordTextFromURL(fileURL, file_download.IsWordFile) if err != nil { - return err + return nil, err } - return a.adviceBiz.WordAna(context.Background(), result) + ana, err := a.adviceBiz.WordAna(context.Background(), result) + if err != nil { + return nil, err + } + + return pkg.JsonByteIgonErr(ana), err } diff --git a/internal/services/advicer_test.go b/internal/services/advicer_test.go deleted file mode 100644 index bce9307..0000000 --- a/internal/services/advicer_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package services - -import ( - "testing" -) - -func Test_WordAna(t *testing.T) { - Run() - - advicer.WordAnat("https://attachment-public.oss-cn-hangzhou.aliyuncs.com/ai-scheduler/data-analytics/word/content.docx") -} - -var ( - advicer *AdviceService -) diff --git a/internal/services/provider_set.go b/internal/services/provider_set.go index 83a9373..dfce67a 100644 --- a/internal/services/provider_set.go +++ b/internal/services/provider_set.go @@ -2,6 +2,7 @@ package services import ( "ai_scheduler/internal/gateway" + "ai_scheduler/internal/services/advice" "github.com/google/wire" ) @@ -15,5 +16,5 @@ var ProviderSetServices = wire.NewSet( NewHistoryService, NewCapabilityService, NewCronService, - NewAdviceService, + advice.NewAdviceService, ) From ec5ff4a0a9c4141362640841864b57643660b67b Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Thu, 5 Feb 2026 13:37:42 +0800 Subject: [PATCH 7/8] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84advice?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8F=8A=E6=B7=BB=E5=8A=A0MongoDB=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/wire.go | 4 +- config/config.yaml | 8 + config/config_env.yaml | 8 + config/config_test.yaml | 3 +- docker-compose.yml | 5 +- go.mod | 11 +- go.sum | 20 +- internal/biz/advice_advicer.go | 166 +++++++- internal/biz/advice_chat.go | 124 ++++++ internal/biz/advice_client.go | 150 +++++++ internal/biz/advice_file.go | 58 ++- internal/biz/advice_project.go | 100 +++++ internal/biz/advice_skill.go | 164 ++++++++ internal/biz/llm_service/third_party/hsyq.go | 26 +- internal/biz/provider_set.go | 4 + internal/config/config.go | 6 +- internal/data/model/ai_advice_advicer.gen.go | 13 +- .../data/model/ai_advice_advicer_fn.gen.go | 37 ++ internal/data/mongo_model/advicer_client.go | 97 +++++ internal/data/mongo_model/advicer_project.go | 150 +++++++ .../data/mongo_model/advicer_talk_skill.go | 137 ++++++ internal/data/mongo_model/advicer_version.go | 155 +++++++ internal/data/mongo_model/common.go | 24 ++ internal/data/mongo_model/provider_set.go | 10 + internal/entitys/advicer.go | 391 +----------------- internal/entitys/advicer_data.go | 178 +++++++- internal/pkg/mongo.go | 15 +- internal/server/http.go | 19 +- internal/server/router/router.go | 79 ++-- internal/services/advice/advicer.go | 78 ++++ internal/services/advice/advicer_test.go | 95 +++-- internal/services/advice/chat.go | 117 ++++++ internal/services/advice/chat_test.go | 37 ++ internal/services/advice/client.go | 61 +++ internal/services/advice/client_test.go | 39 ++ internal/services/advice/data.go | 60 --- internal/services/advice/project.go | 52 +++ internal/services/advice/project_test.go | 30 ++ internal/services/advice/talk_skill.go | 61 +++ internal/services/advice/talk_skill_test.go | 41 ++ internal/services/provider_set.go | 7 +- 41 files changed, 2237 insertions(+), 603 deletions(-) create mode 100644 internal/biz/advice_chat.go create mode 100644 internal/biz/advice_client.go create mode 100644 internal/biz/advice_project.go create mode 100644 internal/biz/advice_skill.go create mode 100644 internal/data/model/ai_advice_advicer_fn.gen.go create mode 100644 internal/data/mongo_model/advicer_client.go create mode 100644 internal/data/mongo_model/advicer_project.go create mode 100644 internal/data/mongo_model/advicer_talk_skill.go create mode 100644 internal/data/mongo_model/advicer_version.go create mode 100644 internal/data/mongo_model/common.go create mode 100644 internal/data/mongo_model/provider_set.go create mode 100644 internal/services/advice/advicer.go create mode 100644 internal/services/advice/chat.go create mode 100644 internal/services/advice/chat_test.go create mode 100644 internal/services/advice/client.go create mode 100644 internal/services/advice/client_test.go delete mode 100644 internal/services/advice/data.go create mode 100644 internal/services/advice/project.go create mode 100644 internal/services/advice/project_test.go create mode 100644 internal/services/advice/talk_skill.go create mode 100644 internal/services/advice/talk_skill_test.go diff --git a/cmd/server/wire.go b/cmd/server/wire.go index b4f4437..711e7b2 100644 --- a/cmd/server/wire.go +++ b/cmd/server/wire.go @@ -10,6 +10,7 @@ import ( "ai_scheduler/internal/biz/tools_regis" "ai_scheduler/internal/config" "ai_scheduler/internal/data/impl" + "ai_scheduler/internal/data/mongo_model" "ai_scheduler/internal/domain/component" "ai_scheduler/internal/domain/repo" "ai_scheduler/internal/domain/workflow" @@ -27,7 +28,7 @@ import ( ) // InitializeApp 初始化应用程序 -func InitializeApp(ctx context.Context, *config.Config, log.AllLogger) (*server.Servers, func(), error) { +func InitializeApp(context.Context, *config.Config, log.AllLogger) (*server.Servers, func(), error) { panic(wire.Build( server.ProviderSetServer, workflow.ProviderSetWorkflow, @@ -43,6 +44,7 @@ func InitializeApp(ctx context.Context, *config.Config, log.AllLogger) (*server. // tool_callback.ProviderSetCallBackTools, component.ProviderSet, repo.ProviderSet, + mongo_model.ProviderSetMongo, )) } diff --git a/config/config.yaml b/config/config.yaml index 6166634..c1300c2 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -56,6 +56,14 @@ redis: db: driver: mysql source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +mongo: + source: mongodb://root:lsxd2026123@192.168.6.115:27017 + dataBase: ai_scheduler + maxPoolSize: 100 + minPoolSize: 10 + maxConnIdleTime: 30 + connectTimeout: 10 + socketTimeout: 30 oss: access_key: "LTAI5tGGZzjf3tvqWk8SQj2G" secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq" diff --git a/config/config_env.yaml b/config/config_env.yaml index 1b1a8bd..e9f7bbb 100644 --- a/config/config_env.yaml +++ b/config/config_env.yaml @@ -50,6 +50,14 @@ redis: db: driver: mysql source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +mongo: + source: mongodb://root:lsxd2026123@192.168.6.115:27017 + dataBase: ai_scheduler_test + maxPoolSize: 100 + minPoolSize: 10 + maxConnIdleTime: 30 + connectTimeout: 10 + socketTimeout: 30 oss: access_key: "LTAI5tGGZzjf3tvqWk8SQj2G" secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq" diff --git a/config/config_test.yaml b/config/config_test.yaml index 003be9e..df57522 100644 --- a/config/config_test.yaml +++ b/config/config_test.yaml @@ -53,7 +53,8 @@ db: driver: mysql source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai mongo: - source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai + source: mongodb://root:lsxd2026123@192.168.6.115:27017 + dataBase: ai_scheduler_test maxPoolSize: 100 minPoolSize: 10 maxConnIdleTime: 30 diff --git a/docker-compose.yml b/docker-compose.yml index ab49b41..26198fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: container_name: mysql_db restart: unless-stopped environment: - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword123} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-lsxd2026} MYSQL_DATABASE: ${MYSQL_DATABASE:-myapp} MYSQL_USER: ${MYSQL_USER:-myuser} MYSQL_PASSWORD: ${MYSQL_PASSWORD:-mypassword} @@ -36,9 +36,8 @@ services: - "27017:27017" volumes: - mongodb_data:/data/db - - ./mongodb/mongod.conf:/etc/mongod.conf command: - --config /etc/mongod.conf + --auth --bind_ip_all # 允许所有IP连接 networks: - ai_scheduler_network diff --git a/go.mod b/go.mod index eb351cb..5634e43 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,6 @@ require ( github.com/gofiber/websocket/v2 v2.2.1 github.com/google/uuid v1.6.0 github.com/google/wire v0.7.0 - github.com/lukasjarosch/go-docx v0.5.0 github.com/ollama/ollama v0.12.7 github.com/redis/go-redis/v9 v9.16.0 github.com/robfig/cron/v3 v3.0.1 @@ -34,9 +33,10 @@ require ( github.com/spf13/viper v1.17.0 github.com/stretchr/testify v1.11.1 github.com/tmc/langchaingo v0.1.13 - github.com/unidoc/unioffice v1.39.0 + github.com/valyala/fasthttp v1.51.0 github.com/volcengine/volcengine-go-sdk v1.2.9 github.com/xuri/excelize/v2 v2.10.0 + go.mongodb.org/mongo-driver v1.14.0 golang.org/x/sync v0.17.0 google.golang.org/grpc v1.64.0 gorm.io/driver/mysql v1.6.0 @@ -73,6 +73,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/goph/emperror v0.17.2 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -92,6 +93,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/nikolalohinski/gonja v1.5.3 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -114,13 +116,16 @@ require ( github.com/tjfoc/gmsm v1.4.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect github.com/volcengine/volc-sdk-golang v1.0.23 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xuri/efp v0.0.1 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect github.com/yargevad/filepathx v1.0.0 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.11.0 // indirect diff --git a/go.sum b/go.sum index b069ba2..1d57f5e 100644 --- a/go.sum +++ b/go.sum @@ -239,6 +239,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -327,8 +329,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lukasjarosch/go-docx v0.5.0 h1:4vU+gJ4WMdqwRvRVFF+XMw3rPfUGSXlToPJIX3mHQsQ= -github.com/lukasjarosch/go-docx v0.5.0/go.mod h1:ka/NZgDIJId48vMvcfWfduVTY7uV0/f8EgsmCjuS9X0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -354,6 +354,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= @@ -454,8 +456,6 @@ github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1Ca github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/unidoc/unioffice v1.39.0 h1:Wo5zvrzCqhyK/1Zi5dg8a5F5+NRftIMZPnFPYwruLto= -github.com/unidoc/unioffice v1.39.0/go.mod h1:Axz6ltIZZTUUyHoEnPe4Mb3VmsN4TRHT5iZCGZ1rgnU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= @@ -470,6 +470,12 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4= @@ -478,6 +484,8 @@ github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBL github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -485,6 +493,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -584,7 +594,6 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -699,6 +708,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/internal/biz/advice_advicer.go b/internal/biz/advice_advicer.go index af49954..935d58f 100644 --- a/internal/biz/advice_advicer.go +++ b/internal/biz/advice_advicer.go @@ -3,26 +3,35 @@ package biz import ( "ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/model" + "ai_scheduler/internal/data/mongo_model" "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + "errors" + "fmt" "time" "context" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" "xorm.io/builder" ) type AdviceAdvicerBiz struct { - advicerImpl *impl.AdviceAdvicerImpl - adviceAdvicerVersionImpl *impl.AdviceAdvicerVersionImpl + advicerImpl *impl.AdviceAdvicerImpl + advicerVersionMongo *mongo_model.AdvicerVersionMongo + mongo *pkg.Mongo } func NewAdviceAdvicerBiz( advicerImpl *impl.AdviceAdvicerImpl, - adviceAdvicerVersionImpl *impl.AdviceAdvicerVersionImpl, + advicerVersionMongo *mongo_model.AdvicerVersionMongo, + mongo *pkg.Mongo, ) *AdviceAdvicerBiz { return &AdviceAdvicerBiz{ - advicerImpl: advicerImpl, - adviceAdvicerVersionImpl: adviceAdvicerVersionImpl, + advicerImpl: advicerImpl, + advicerVersionMongo: advicerVersionMongo, + mongo: mongo, } } @@ -33,6 +42,7 @@ func (a *AdviceAdvicerBiz) Update(ctx context.Context, data *entitys.AdvicerInit } param := &model.AiAdviceAdvicer{ AdvicerID: data.AdvicerID, + ProjectID: data.ProjectID, Name: data.Name, Birth: birth, Gender: data.Gender, @@ -56,21 +66,143 @@ func (a *AdviceAdvicerBiz) List(ctx context.Context, data *entitys.AdvicerListRe return list, err } -func (a *AdviceAdvicerBiz) VersionUpdate(ctx context.Context, param *entitys.AdvicerVersionInitReq) (err error) { - - if param.VersionID == 0 { - _, err = a.adviceAdvicerVersionImpl.Add(param) - } else { - cond := builder.NewCond() - cond = cond.And(builder.Eq{"version_id": param.VersionID}) - err = a.adviceAdvicerVersionImpl.UpdateByCond(&cond, param) +func (a *AdviceAdvicerBiz) VersionAdd(ctx context.Context, param *entitys.AdvicerVersionAddReq) (err error) { + cond := builder.NewCond() + cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID}) + _, err = a.advicerImpl.GetOneBySearch(&cond) + if err != nil { + return errors.New("顾问不存在") } + _, err = a.mongo.Co(a.advicerVersionMongo).InsertOne(ctx, &mongo_model.AdvicerVersionMongo{ + AdvicerId: param.AdvicerID, + VersionDesc: param.VersionDesc, + DialectFeatures: param.DialectFeatures, + SentencePatterns: param.SentencePatterns, + ToneTags: param.ToneTags, + PersonalityTags: param.PersonalityTags, + SignatureDialogues: param.SignatureDialogues, + LastUpdateTime: time.Now(), + }) + return err } -func (a *AdviceAdvicerBiz) VersionList(ctx context.Context, data *entitys.AdvicerVersionListReq) ([]map[string]interface{}, error) { - cond := builder.NewCond() - cond = cond.And(builder.Eq{"advicer_id": data.AdvicerID}) - list, err := a.adviceAdvicerVersionImpl.GetRange(&cond) +func (a *AdviceAdvicerBiz) VersionUpdate(ctx context.Context, param *entitys.AdvicerVersionUpdateReq) (err error) { + filter := bson.M{} + if len(param.Id) == 0 { + return errors.New("ID不能为空") + } + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + update := bson.M{ + "$set": &mongo_model.AdvicerVersionMongo{ + AdvicerId: param.AdvicerID, + VersionDesc: param.VersionDesc, + DialectFeatures: param.DialectFeatures, + SentencePatterns: param.SentencePatterns, + ToneTags: param.ToneTags, + PersonalityTags: param.PersonalityTags, + SignatureDialogues: param.SignatureDialogues, + LastUpdateTime: time.Now(), + }, + } + res := a.mongo.Co(a.advicerVersionMongo).FindOneAndUpdate(ctx, filter, update) + + return res.Err() +} + +func (a *AdviceAdvicerBiz) VersionList(ctx context.Context, param *entitys.AdvicerVersionListReq) (list []mongo_model.AdvicerVersionMongo, err error) { + filter := bson.M{} + // 1. advicer_id 条件 + if param.AdvicerId != 0 { + filter["AdvicerId"] = param.AdvicerId + } + + // 2. _id 条件 + if len(param.Id) != 0 { + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return nil, fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + } + + // 3. version_desc 模糊查询 + if len(param.VersionDesc) != 0 { + // 正确的方式:指定字段名 + filter["VersionDesc"] = bson.M{ + "$regex": primitive.Regex{ + Pattern: param.VersionDesc, + Options: "i", + }, + } + } + + cursor, err := a.mongo.Co(a.advicerVersionMongo).Find(ctx, filter) + if err != nil { + return nil, err + } + // 遍历结果 + for cursor.Next(ctx) { + var advicerVersion mongo_model.AdvicerVersionMongo + if err := cursor.Decode(&advicerVersion); err != nil { + return nil, err + } + list = append(list, advicerVersion) + } + + if err := cursor.Err(); err != nil { + return nil, err + } return list, err } + +func (a *AdviceAdvicerBiz) VersionDel(ctx context.Context, param *entitys.AdvicerVersionDelReq) (err error) { + filter := bson.M{} + // 1. advicer_id 条件 + + if len(param.Id) != 0 { + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + } + + _, err = a.mongo.Co(a.advicerVersionMongo).DeleteOne(ctx, filter) + + return err +} + +func (a *AdviceAdvicerBiz) VersionInfo(ctx context.Context, param *entitys.AdvicerVersionInfoReq) (info mongo_model.AdvicerVersionMongo, err error) { + filter := bson.M{} + if len(param.Id) != 0 { + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return info, fmt.Errorf("ID转换失败: %w", err) + + } + filter["_id"] = objectID + } + + res := a.mongo.Co(a.advicerVersionMongo).FindOne(ctx, filter) + if res.Err() != nil { + return info, res.Err() + } + + if err := res.Decode(&info); err != nil { + return info, err + } + return info, nil +} + +func (a *AdviceAdvicerBiz) AdvicerInfo(ctx context.Context, param *entitys.AdvicerInfoReq) (info model.AiAdviceAdvicer, err error) { + cond := builder.NewCond() + cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID}) + + err = a.advicerImpl.GetOneBySearchToStrut(&cond, &info) + return +} diff --git a/internal/biz/advice_chat.go b/internal/biz/advice_chat.go new file mode 100644 index 0000000..58c14fb --- /dev/null +++ b/internal/biz/advice_chat.go @@ -0,0 +1,124 @@ +package biz + +import ( + "ai_scheduler/internal/biz/llm_service/third_party" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + "ai_scheduler/utils" + "context" + "encoding/json" + "strings" + "time" + + "github.com/google/uuid" + "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" + "github.com/volcengine/volcengine-go-sdk/volcengine" +) + +type AdviceChatBiz struct { + hsyq *third_party.Hsyq + rdb *utils.Rdb +} + +func NewAdviceChatBiz( + hsyq *third_party.Hsyq, + rdb *utils.Rdb, +) *AdviceChatBiz { + return &AdviceChatBiz{ + hsyq: hsyq, + rdb: rdb, + } +} + +func (a *AdviceChatBiz) Regis(ctx context.Context, chatData *entitys.ChatData) (string, error) { + sessionId := uuid.New().String() + prompt, err := a.buildBasePrompt(ctx, chatData) + if err != nil { + return "", err + } + err = a.rdb.Rdb.SetEx(ctx, sessionId, pkg.JsonStringIgonErr(prompt), 3600*time.Second).Err() + + return sessionId, err +} + +func (a *AdviceChatBiz) Chat(ctx context.Context, chat *entitys.AdvicerChatReq) ([]string, error) { + if len(chat.Content) == 0 { + return nil, nil + } + basePromptJson, err := a.getChatDataFromStringSessionId(ctx, chat.SessionId) + if err != nil { + return nil, err + } + prompt, err := a.setContent(ctx, basePromptJson, chat.Content) + if err != nil { + return nil, err + } + resContent, err := a.callLlm(ctx, prompt, fileModel) + if err != nil { + return nil, err + } + resSlice := strings.Split(resContent, "\n") + return resSlice, nil +} + +func (a *AdviceChatBiz) buildBasePrompt(ctx context.Context, chatData *entitys.ChatData) ([]*model.ChatCompletionMessage, error) { + var message = make([]*model.ChatCompletionMessage, 3) + message[0] = &model.ChatCompletionMessage{ + Role: model.ChatMessageRoleSystem, + Content: &model.ChatCompletionMessageContent{ + StringValue: volcengine.String(a.sysPrompt(chatData)), + }, + } + + message[1] = &model.ChatCompletionMessage{ + Role: model.ChatMessageRoleUser, + Content: &model.ChatCompletionMessageContent{ + StringValue: volcengine.String("{{chat_content}}"), + }, + } + + message[2] = &model.ChatCompletionMessage{ + Role: model.ChatMessageRoleAssistant, + Content: &model.ChatCompletionMessageContent{ + StringValue: volcengine.String(a.assistantPrompt(chatData)), + }, + } + return message, nil +} + +func (a *AdviceChatBiz) setContent(ctx context.Context, basePromptJson string, content string) ([]*model.ChatCompletionMessage, error) { + promptJson := strings.ReplaceAll(basePromptJson, "{{chat_content}}", content) + var basePrompt []*model.ChatCompletionMessage + err := json.Unmarshal([]byte(promptJson), &basePrompt) + if err != nil { + return nil, err + } + + return basePrompt, nil +} + +func (a *AdviceChatBiz) sysPrompt(chatData *entitys.ChatData) string { + return "【场景】现在你是专业的顾问,我们在聊天软件里面聊天,当前时间是2026/01/30 16:13:30\n【要求】提供的json内容包含顾问,项目,对话技巧,客户信息,请根据json内容进行模仿真人进行对话,** 绝对不能让对方看出来你是ai!**\n【规则】\n1.必须模仿顾问的聊天方式,不能有markdown格式,聊天内容必须贴近聊天软件的正常聊天,非客户要求不要自顾自的介绍项目!开场白不要太长!\n2.不要有任何特殊格式,有比较长的对话内容注意提行\n3.尽量邀约对方来项目上来\n4.不能有任何画外音的文字格式出现!比如(等待对方回复)" +} + +func (a *AdviceChatBiz) assistantPrompt(chatData *entitys.ChatData) string { + return pkg.JsonStringIgonErr(chatData) +} + +func (a *AdviceChatBiz) getChatDataFromStringSessionId(ctx context.Context, sessionId string) (basePromptJson string, err error) { + cache := a.rdb.Rdb.Get(ctx, sessionId) + if cache.Err() != nil { + err = cache.Err() + return + } + + return cache.Val(), cache.Err() +} + +func (a *AdviceChatBiz) callLlm(ctx context.Context, prompt []*model.ChatCompletionMessage, modelName string) (string, error) { + res, err := a.hsyq.RequestHsyq(ctx, key, modelName, prompt) + if err != nil { + return "", err + } + return *res.Choices[0].Message.Content.StringValue, nil +} diff --git a/internal/biz/advice_client.go b/internal/biz/advice_client.go new file mode 100644 index 0000000..77fd54a --- /dev/null +++ b/internal/biz/advice_client.go @@ -0,0 +1,150 @@ +package biz + +import ( + "ai_scheduler/internal/data/mongo_model" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + "errors" + "fmt" + "time" + + "context" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AdviceClientBiz struct { + AdvicerClientMongo *mongo_model.AdvicerClientMongo + mongo *pkg.Mongo +} + +func NewAdviceClientBiz( + advicerClientMongo *mongo_model.AdvicerClientMongo, + mongo *pkg.Mongo, +) *AdviceClientBiz { + return &AdviceClientBiz{ + AdvicerClientMongo: advicerClientMongo, + mongo: mongo, + } +} + +func (a *AdviceClientBiz) Add(ctx context.Context, param *entitys.AdvicerClientAddReq) (err error) { + + _, err = a.mongo.Co(a.AdvicerClientMongo).InsertOne(ctx, &mongo_model.AdvicerClientMongo{ + ProjectId: param.ProjectId, + AdvicerId: param.AdvicerId, + PersonalInfo: param.PersonalInfo, + PurchasePurpose: param.PurchasePurpose, + CoreDemands: param.CoreDemands, + Concerns: param.Concerns, + DecisionProfile: param.DecisionProfile, + LastUpdateTime: time.Now(), + }) + + return err +} + +func (a *AdviceClientBiz) Update(ctx context.Context, param *entitys.AdvicerrClientUpdateReq) (err error) { + filter := bson.M{} + if len(param.Id) == 0 { + return errors.New("ID不能为空") + } + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + update := bson.M{ + "$set": &mongo_model.AdvicerClientMongo{ + ProjectId: param.ProjectId, + AdvicerId: param.AdvicerId, + PersonalInfo: param.PersonalInfo, + PurchasePurpose: param.PurchasePurpose, + CoreDemands: param.CoreDemands, + Concerns: param.Concerns, + DecisionProfile: param.DecisionProfile, + LastUpdateTime: time.Now(), + }, + } + res := a.mongo.Co(a.AdvicerClientMongo).FindOneAndUpdate(ctx, filter, update) + return res.Err() +} + +func (a *AdviceClientBiz) List(ctx context.Context, param *entitys.AdvicerClientListReq) (list []mongo_model.AdvicerClientMongo, err error) { + filter := bson.M{} + // 1. advicer_id 条件 + if param.AdvicerId != 0 { + filter["AdvicerId"] = param.AdvicerId + } + + if param.ProjectId != 0 { + filter["projectId"] = param.ProjectId + } + + // 2. _id 条件 + if len(param.Id) != 0 { + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return nil, fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + } + + cursor, err := a.mongo.Co(a.AdvicerClientMongo).Find(ctx, filter) + if err != nil { + return nil, err + } + // 遍历结果 + for cursor.Next(ctx) { + var advicerVersion mongo_model.AdvicerClientMongo + if err := cursor.Decode(&advicerVersion); err != nil { + return nil, err + } + list = append(list, advicerVersion) + } + + if err := cursor.Err(); err != nil { + return nil, err + } + return list, err +} + +func (a *AdviceClientBiz) Del(ctx context.Context, param *entitys.AdvicerClientDelReq) (err error) { + filter := bson.M{} + + if len(param.Id) != 0 { + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + } + + _, err = a.mongo.Co(a.AdvicerClientMongo).DeleteOne(ctx, filter) + + return err +} + +func (a *AdviceClientBiz) Info(ctx context.Context, param *entitys.AdvicerClientInfoReq) (info mongo_model.AdvicerClientMongo, err error) { + filter := bson.M{} + + if len(param.Id) != 0 { + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return info, fmt.Errorf("ID转换失败: %w", err) + + } + filter["_id"] = objectID + } + + res := a.mongo.Co(a.AdvicerClientMongo).FindOne(ctx, filter) + if res.Err() != nil { + return info, res.Err() + } + + if err = res.Decode(&info); err != nil { + return info, err + } + return +} diff --git a/internal/biz/advice_file.go b/internal/biz/advice_file.go index 897843b..65c8431 100644 --- a/internal/biz/advice_file.go +++ b/internal/biz/advice_file.go @@ -2,7 +2,7 @@ package biz import ( "ai_scheduler/internal/biz/llm_service/third_party" - "ai_scheduler/internal/entitys" + "ai_scheduler/internal/data/mongo_model" "ai_scheduler/internal/pkg" "context" "encoding/json" @@ -11,7 +11,6 @@ import ( "strings" "time" - "github.com/gofiber/fiber/v2/log" "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" "github.com/volcengine/volcengine-go-sdk/volcengine" ) @@ -32,26 +31,26 @@ const ( jsonModel = "doubao-seed-1-6-flash-250828" ) -var DataMap = map[string]entitys.AdviceData{ - "dialectFeatures": &entitys.DialectFeatures{}, - "sentencePatterns": &entitys.SentencePatterns{}, - "personalityTags": &entitys.PersonalityTags{}, - "toneTags": &entitys.ToneTags{}, - "signatureDialogues": &entitys.SignatureDialogues{}, - "regionValue": &entitys.RegionValue{}, - "competitionComparison": &entitys.CompetitionComparison{}, - "coreSellingPoints": &entitys.CoreSellingPoints{}, - "supportingFacilities": &entitys.SupportingFacilities{}, - "developerBacking": &entitys.DeveloperBacking{}, - "needsMining": &entitys.NeedsMining{}, - "painPointResponse": &entitys.PainPointResponse{}, - "valueBuilding": &entitys.ValueBuilding{}, - "closingTechniques": &entitys.ClosingTechniques{}, - "communicationRhythm": &entitys.CommunicationRhythm{}, - "customer": &entitys.Customer{}, +var DataMap = map[string]mongo_model.AdviceData{ + "dialectFeatures": &mongo_model.DialectFeatures{}, + "sentencePatterns": &mongo_model.SentencePatterns{}, + "personalityTags": &mongo_model.PersonalityTags{}, + "toneTags": &mongo_model.ToneTags{}, + "signatureDialogues": &mongo_model.SignatureDialogues{}, + "regionValue": &mongo_model.RegionValue{}, + "competitionComparison": &mongo_model.CompetitionComparison{}, + "coreSellingPoints": &mongo_model.CoreSellingPoints{}, + "supportingFacilities": &mongo_model.SupportingFacilities{}, + "developerBacking": &mongo_model.DeveloperBacking{}, + "needsMining": &mongo_model.NeedsMining{}, + "painPointResponse": &mongo_model.PainPointResponse{}, + "valueBuilding": &mongo_model.ValueBuilding{}, + "closingTechniques": &mongo_model.ClosingTechniques{}, + "communicationRhythm": &mongo_model.CommunicationRhythm{}, + "customer": &mongo_model.Customer{}, } -func (a *AdviceFileBiz) WordAna(ctx context.Context, wordContent string) (map[entitys.AdviceRole]map[string]entitys.AdviceData, error) { +func (a *AdviceFileBiz) WordAna(ctx context.Context, wordContent string) (map[mongo_model.AdviceRole]map[string]mongo_model.AdviceData, error) { timeSte := time.Now().Format("200601021504") dir := "./cache/" + timeSte os.Mkdir(dir, 0755) @@ -81,18 +80,18 @@ func (a *AdviceFileBiz) WordAna(ctx context.Context, wordContent string) (map[en return resData, err } -func (a *AdviceFileBiz) cateData(data map[string]entitys.AdviceData) map[entitys.AdviceRole]map[string]entitys.AdviceData { - var res = make(map[entitys.AdviceRole]map[string]entitys.AdviceData) +func (a *AdviceFileBiz) cateData(data map[string]mongo_model.AdviceData) map[mongo_model.AdviceRole]map[string]mongo_model.AdviceData { + var res = make(map[mongo_model.AdviceRole]map[string]mongo_model.AdviceData) for k, v := range data { if _, ok := res[v.Role()]; !ok { - res[v.Role()] = make(map[string]entitys.AdviceData) + res[v.Role()] = make(map[string]mongo_model.AdviceData) } res[v.Role()][k] = v } return res } -func (a *AdviceFileBiz) parseResponse(ctx context.Context, responseByte []byte) (resultOutPut map[string]entitys.AdviceData, err error) { +func (a *AdviceFileBiz) parseResponse(ctx context.Context, responseByte []byte) (resultOutPut map[string]mongo_model.AdviceData, err error) { //只尝试修复一次 if isValid := json.Valid(responseByte); !isValid { responseByte, err = a.fixJson(ctx, responseByte) @@ -108,7 +107,7 @@ func (a *AdviceFileBiz) parseResponse(ctx context.Context, responseByte []byte) result map[string]interface{} ) - resultOutPut = make(map[string]entitys.AdviceData) + resultOutPut = make(map[string]mongo_model.AdviceData) if err = json.Unmarshal(responseByte, &result); err != nil { return @@ -150,20 +149,19 @@ func (a *AdviceFileBiz) callLlm(ctx context.Context, prompt string, modelName st StringValue: volcengine.String(prompt), }, } - res, err := a.hsyq.RequestHsyq(ctx, key, modelName, message) if err != nil { return "", err } - log.Info("token用量:", res.Usage.TotalTokens) + return *res.Choices[0].Message.Content.StringValue, nil } -func (a *AdviceFileBiz) getAllExamples() map[string]entitys.AdviceData { +func (a *AdviceFileBiz) getAllExamples() map[string]mongo_model.AdviceData { return DataMap } -func (a *AdviceFileBiz) buildSimplePrompt(wordContent string, examples map[string]entitys.AdviceData) string { +func (a *AdviceFileBiz) buildSimplePrompt(wordContent string, examples map[string]mongo_model.AdviceData) string { // 最简单的提示词模板 template := `分析以下房地产销售对话,按指定格式提取信息: @@ -182,7 +180,7 @@ func (a *AdviceFileBiz) buildSimplePrompt(wordContent string, examples map[strin // 构建格式部分 var formats strings.Builder for name, example := range examples { - formats.WriteString(fmt.Sprintf("=== %s (%s:%s)===\n示例:%s\n\n", name, entitys.RoleDesc[example.Role()], example.Desc(), example.Example())) + formats.WriteString(fmt.Sprintf("=== %s (%s:%s)===\n示例:%s\n\n", name, mongo_model.RoleDesc[example.Role()], example.Desc(), example.Example())) } return fmt.Sprintf(template, wordContent, formats.String()) diff --git a/internal/biz/advice_project.go b/internal/biz/advice_project.go new file mode 100644 index 0000000..bd48d77 --- /dev/null +++ b/internal/biz/advice_project.go @@ -0,0 +1,100 @@ +package biz + +import ( + "ai_scheduler/internal/data/mongo_model" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + "errors" + "fmt" + "time" + + "context" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AdviceProjectBiz struct { + AdvicerProjectMongo *mongo_model.AdvicerProjectMongo + mongo *pkg.Mongo +} + +func NewAdviceProjectBiz( + advicerProjectMongo *mongo_model.AdvicerProjectMongo, + mongo *pkg.Mongo, +) *AdviceProjectBiz { + return &AdviceProjectBiz{ + AdvicerProjectMongo: advicerProjectMongo, + mongo: mongo, + } +} + +func (a *AdviceProjectBiz) Add(ctx context.Context, param *entitys.AdvicerProjectAddReq) (err error) { + + _, err = a.mongo.Co(a.AdvicerProjectMongo).InsertOne(ctx, &mongo_model.AdvicerProjectMongo{ + ProjectId: param.ProjectId, + ProjectInfo: param.ProjectInfo, + RegionValue: param.RegionValue, + CompetitionComparison: param.CompetitionComparison, + CoreSellingPoints: param.CoreSellingPoints, + SupportingFacilities: param.SupportingFacilities, + DeveloperBacking: param.DeveloperBacking, + LastUpdateTime: time.Now(), + }) + + return err +} + +func (a *AdviceProjectBiz) Update(ctx context.Context, param *entitys.AdvicerrProjectUpdateReq) (err error) { + filter := bson.M{} + if len(param.Id) == 0 { + return errors.New("ID不能为空") + } + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + update := bson.M{ + "$set": &mongo_model.AdvicerProjectMongo{ + ProjectId: param.ProjectId, + RegionValue: param.RegionValue, + CompetitionComparison: param.CompetitionComparison, + CoreSellingPoints: param.CoreSellingPoints, + SupportingFacilities: param.SupportingFacilities, + DeveloperBacking: param.DeveloperBacking, + LastUpdateTime: time.Now(), + }, + } + res := a.mongo.Co(a.AdvicerProjectMongo).FindOneAndUpdate(ctx, filter, update) + return res.Err() +} + +func (a *AdviceProjectBiz) Info(ctx context.Context, param *entitys.AdvicerProjectInfoReq) (info mongo_model.AdvicerProjectMongo, err error) { + filter := bson.M{} + + if param.ProjectId != 0 { + filter["projectId"] = param.ProjectId + } + + // 2. _id 条件 + if len(param.Id) != 0 { + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return info, fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + } + + res := a.mongo.Co(a.AdvicerProjectMongo).FindOne(ctx, filter) + if res.Err() != nil { + return info, res.Err() + } + // 遍历结果 + + if err := res.Decode(&info); err != nil { + return info, err + } + + return info, nil +} diff --git a/internal/biz/advice_skill.go b/internal/biz/advice_skill.go new file mode 100644 index 0000000..38e64c7 --- /dev/null +++ b/internal/biz/advice_skill.go @@ -0,0 +1,164 @@ +package biz + +import ( + "ai_scheduler/internal/data/mongo_model" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + "errors" + "fmt" + "time" + + "context" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AdviceSkillBiz struct { + AdvicerTalkSkillMongo *mongo_model.AdvicerTalkSkillMongo + mongo *pkg.Mongo +} + +func NewAdviceSkillBiz( + advicerTalkSkillMongo *mongo_model.AdvicerTalkSkillMongo, + mongo *pkg.Mongo, +) *AdviceSkillBiz { + return &AdviceSkillBiz{ + AdvicerTalkSkillMongo: advicerTalkSkillMongo, + mongo: mongo, + } +} + +func (a *AdviceSkillBiz) VersionAdd(ctx context.Context, param *entitys.AdvicerTalkSkillAddReq) (err error) { + + _, err = a.mongo.Co(a.AdvicerTalkSkillMongo).InsertOne(ctx, &mongo_model.AdvicerTalkSkillMongo{ + ProjectId: param.ProjectId, + AdvicerId: param.AdvicerId, + Desc: param.Desc, + NeedsMining: param.NeedsMining, + PainPointResponse: param.PainPointResponse, + ValueBuilding: param.ValueBuilding, + ClosingTechniques: param.ClosingTechniques, + CommunicationRhythm: param.CommunicationRhythm, + LastUpdateTime: time.Now(), + }) + + return err +} + +func (a *AdviceSkillBiz) VersionUpdate(ctx context.Context, param *entitys.AdvicerTalkSkillUpdateReq) (err error) { + filter := bson.M{} + if len(param.Id) == 0 { + return errors.New("ID不能为空") + } + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + update := bson.M{ + "$set": &mongo_model.AdvicerTalkSkillMongo{ + AdvicerId: param.AdvicerId, + ProjectId: param.ProjectId, + Desc: param.Desc, + NeedsMining: param.NeedsMining, + PainPointResponse: param.PainPointResponse, + ValueBuilding: param.ValueBuilding, + ClosingTechniques: param.ClosingTechniques, + CommunicationRhythm: param.CommunicationRhythm, + LastUpdateTime: time.Now(), + }, + } + res := a.mongo.Co(a.AdvicerTalkSkillMongo).FindOneAndUpdate(ctx, filter, update) + return res.Err() +} + +func (a *AdviceSkillBiz) VersionList(ctx context.Context, param *entitys.AdvicerTalkSkillListReq) (list []mongo_model.AdvicerTalkSkillMongo, err error) { + filter := bson.M{} + // 1. advicer_id 条件 + if param.AdvicerId != 0 { + filter["AdvicerId"] = param.AdvicerId + } + + if param.ProjectId != 0 { + filter["projectId"] = param.ProjectId + } + + // 2. _id 条件 + if len(param.Id) != 0 { + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return nil, fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + } + + // 3. version_desc 模糊查询 + if len(param.Desc) != 0 { + // 正确的方式:指定字段名 + filter["desc"] = bson.M{ + "$regex": primitive.Regex{ + Pattern: param.Desc, + Options: "i", + }, + } + } + + cursor, err := a.mongo.Co(a.AdvicerTalkSkillMongo).Find(ctx, filter) + if err != nil { + return nil, err + } + // 遍历结果 + for cursor.Next(ctx) { + var advicerVersion mongo_model.AdvicerTalkSkillMongo + if err := cursor.Decode(&advicerVersion); err != nil { + return nil, err + } + list = append(list, advicerVersion) + } + + if err := cursor.Err(); err != nil { + return nil, err + } + return list, err +} + +func (a *AdviceSkillBiz) VersionDel(ctx context.Context, param *entitys.AdvicerTalkSkillDelReq) (err error) { + filter := bson.M{} + + if len(param.Id) != 0 { + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + } + + _, err = a.mongo.Co(a.AdvicerTalkSkillMongo).DeleteOne(ctx, filter) + + return err +} + +func (a *AdviceSkillBiz) Info(ctx context.Context, param *entitys.AdvicerTalkSkillInfoReq) (info mongo_model.AdvicerTalkSkillMongo, err error) { + filter := bson.M{} + + if len(param.Id) != 0 { + objectID, err := primitive.ObjectIDFromHex(param.Id) + if err != nil { + return info, fmt.Errorf("ID转换失败: %w", err) + } + filter["_id"] = objectID + } + res := a.mongo.Co(a.AdvicerTalkSkillMongo).FindOne(ctx, filter) + if res.Err() != nil { + return info, err + } + // 遍历结果 + + if err = res.Decode(&info); err != nil { + return info, err + } + + return info, nil + +} diff --git a/internal/biz/llm_service/third_party/hsyq.go b/internal/biz/llm_service/third_party/hsyq.go index 5257774..6b74e0c 100644 --- a/internal/biz/llm_service/third_party/hsyq.go +++ b/internal/biz/llm_service/third_party/hsyq.go @@ -2,10 +2,13 @@ package third_party import ( "context" + "time" + "github.com/gofiber/fiber/v2/log" "github.com/volcengine/volcengine-go-sdk/service/arkruntime" "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" + "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model/responses" ) type Hsyq struct { @@ -47,6 +50,27 @@ func (h *Hsyq) RequestHsyq(ctx context.Context, key string, modelName string, pr if err != nil { return model.ChatCompletionResponse{ID: ""}, err } - + log.Info("token用量:", resp.Usage.TotalTokens, "输入:", resp.Usage.PromptTokens, "输出:", resp.Usage.CompletionTokens) + return resp, err +} + +func (h *Hsyq) RequestHsyqJson(ctx context.Context, key string, modelName string, prompt []*responses.InputItem) (*responses.ResponseObject, error) { + req := responses.ResponsesRequest{ + Model: modelName, + Input: &responses.ResponsesInput{ + Union: &responses.ResponsesInput_ListValue{ + ListValue: &responses.InputItemList{ListValue: prompt}, + }, + }, + Stream: new(bool), + Thinking: &responses.ResponsesThinking{Type: responses.ThinkingType_disabled.Enum()}, + Text: &responses.ResponsesText{Format: &responses.TextFormat{Type: responses.TextType_json_object}}, + } + + resp, err := h.getClient(key).CreateResponses(ctx, &req) + if err != nil { + return resp, err + } + log.Info("token用量:", resp.Usage.TotalTokens) return resp, err } diff --git a/internal/biz/provider_set.go b/internal/biz/provider_set.go index cb50b0e..4462609 100644 --- a/internal/biz/provider_set.go +++ b/internal/biz/provider_set.go @@ -25,4 +25,8 @@ var ProviderSetBiz = wire.NewSet( NewAdviceFileBiz, third_party.NewHsyq, NewAdviceAdvicerBiz, + NewAdviceSkillBiz, + NewAdviceProjectBiz, + NewAdviceClientBiz, + NewAdviceChatBiz, ) diff --git a/internal/config/config.go b/internal/config/config.go index d9201dd..232168f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -158,7 +158,8 @@ type Redis struct { } type DB struct { - Driver string `mapstructure:"driver"` + Driver string `mapstructure:"driver"` + Source string `mapstructure:"source"` MaxIdle int32 `mapstructure:"maxIdle"` MaxOpen int32 `mapstructure:"maxOpen"` @@ -168,6 +169,7 @@ type DB struct { type Mongo struct { Source string `mapstructure:"source"` + DataBase string `mapstructure:"dataBase"` MaxPoolSize uint64 `mapstructure:"maxPoolSize"` MinPoolSize uint64 `mapstructure:"minPoolSize"` MaxConnIdleTime int32 `mapstructure:"maxConnIdleTime"` @@ -319,7 +321,7 @@ func LoadConfigWithEnv() (*Config, error) { if err != nil { return nil, err } - viper.SetConfigFile(modularDir + "/config/config_env.yaml") + viper.SetConfigFile(modularDir + "/config/config_test.yaml") viper.SetConfigType("yaml") // 读取配置文件 if err := viper.ReadInConfig(); err != nil { diff --git a/internal/data/model/ai_advice_advicer.gen.go b/internal/data/model/ai_advice_advicer.gen.go index 67f7035..449547a 100644 --- a/internal/data/model/ai_advice_advicer.gen.go +++ b/internal/data/model/ai_advice_advicer.gen.go @@ -12,12 +12,13 @@ const TableNameAiAdviceAdvicer = "ai_advice_advicer" // AiAdviceAdvicer mapped from table type AiAdviceAdvicer struct { - AdvicerID int32 `gorm:"column:advicer_id;primaryKey;autoIncrement:true" json:"advicer_id"` - Name string `gorm:"column:name;not null;comment:姓名" json:"name"` // 姓名 - Birth time.Time `gorm:"column:birth;not null;comment:用户名称" json:"birth"` // 用户名称 - Gender int32 `gorm:"column:gender;not null;comment:1:男,2:女" json:"gender"` // 1:男,2:女 - WorkingYears int32 `gorm:"column:working_years;not null;default:1;comment:工作年限" json:"working_years"` // 工作年限 - CreateAt *time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` + AdvicerID int32 `gorm:"column:advicer_id;primaryKey;autoIncrement:true" json:"advicer_id"` + ProjectID int32 `gorm:"column:project_id;not null" json:"project_id"` + Name string `gorm:"column:name;not null;comment:姓名" json:"name"` // 姓名 + Birth time.Time `gorm:"column:birth;not null;comment:用户名称" json:"birth"` // 用户名称 + Gender int32 `gorm:"column:gender;not null;comment:1:男,2:女" json:"gender"` // 1:男,2:女 + WorkingYears int32 `gorm:"column:working_years;not null;default:1;comment:工作年限" json:"working_years"` // 工作年限 + CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` } // TableName AiAdviceAdvicer's table name diff --git a/internal/data/model/ai_advice_advicer_fn.gen.go b/internal/data/model/ai_advice_advicer_fn.gen.go new file mode 100644 index 0000000..adeb509 --- /dev/null +++ b/internal/data/model/ai_advice_advicer_fn.gen.go @@ -0,0 +1,37 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package model + +import ( + "fmt" + "time" +) + +type AiAdviceAdvicerEntity struct { + Name string `json:"name"` // 姓名 + Birth string `json:"birth"` // 用户名称 + Gender string `json:"gender"` // 1:男,2:女 + WorkingYears string `json:"working_years"` // 工作年限 +} + +func (a *AiAdviceAdvicer) Entity() *AiAdviceAdvicerEntity { + var ( + gender string + ) + switch a.Gender { + case 1: + gender = "男" + case 2: + gender = "女" + default: + gender = "未知" + } + return &AiAdviceAdvicerEntity{ + Name: a.Name, + Birth: a.Birth.Format(time.DateOnly), + Gender: gender, + WorkingYears: fmt.Sprintf("%d年", a.WorkingYears), + } +} \ No newline at end of file diff --git a/internal/data/mongo_model/advicer_client.go b/internal/data/mongo_model/advicer_client.go new file mode 100644 index 0000000..053ff42 --- /dev/null +++ b/internal/data/mongo_model/advicer_client.go @@ -0,0 +1,97 @@ +package mongo_model + +import ( + "time" +) + +type AdvicerClientMongo struct { + ProjectId int32 `json:"projectId" bson:"projectId"` + AdvicerId int32 `json:"advicerId" bson:"advicerId"` + PersonalInfo PersonalInfo `json:"personalInfo" bson:"personalInfo"` + PurchasePurpose PurchasePurpose `json:"purchasePurpose" bson:"purchasePurpose"` + CoreDemands CoreDemands `json:"coreDemands" bson:"coreDemands"` + Concerns []string `json:"concerns" bson:"concerns"` + DecisionProfile []string `json:"decisionProfile" bson:"decisionProfile"` + LastUpdateTime time.Time `json:"lastUpdateTime" bson:"lastUpdateTime"` +} + +func NewAdvicerClientMongo() *AdvicerClientMongo { + return &AdvicerClientMongo{} +} + +func (a *AdvicerClientMongo) MongoTableName() string { + return "advicer_client" +} + +type AdvicerClientMongoEntity struct { + PersonalInfo PersonalInfo `json:"personalInfo"` + PurchasePurpose PurchasePurpose `json:"purchasePurpose"` + CoreDemands CoreDemands `json:"coreDemands"` + Concerns []string `json:"concerns"` + DecisionProfile []string `json:"decisionProfile"` +} + +func (a *AdvicerClientMongo) Entity() *AdvicerClientMongoEntity { + return &AdvicerClientMongoEntity{ + PersonalInfo: a.PersonalInfo, + PurchasePurpose: a.PurchasePurpose, + CoreDemands: a.CoreDemands, + Concerns: a.Concerns, + DecisionProfile: a.DecisionProfile, + } +} + +// Customer 客户信息 +type Customer []ClientInfo +type ClientInfo struct { + // 个人信息 + PersonalInfo PersonalInfo `json:"personalInfo"` + + // 购房目的 + PurchasePurpose PurchasePurpose `json:"purchasePurpose"` + + // 核心需求 + CoreDemands CoreDemands `json:"coreDemands"` + + // 关注点与顾虑 + Concerns []string `json:"concerns"` + + // 决策建议 + DecisionProfile []string `json:"decisionProfile"` +} + +type PersonalInfo struct { + Name string `json:"name"` // 姓氏 + Gender string `json:"gender"` // 性别 + Location string `json:"location"` // 来源地/当前居住地 + IsFirstHome bool `json:"isFirstHome"` // 是否首套房 + FamilyOrganize string `json:"familyOrganize"` // 家庭人数 +} + +type PurchasePurpose struct { + PrimaryPurpose string `json:"primaryPurpose"` // 主要目的 + SecondaryPurpose string `json:"secondaryPurpose"` // 次要目的 + DecisionMakers string `json:"decisionMakers"` // 决策人 +} + +type CoreDemands struct { + TotalBudget string `json:"totalBudget"` // 预算范围 + PreferredLayout string `json:"preferredLayout"` // 偏好户型 + CoreAppeal string `json:"coreAppeal"` // 核心述求 +} + +func (e *Customer) Example() string { + return `[{"personalInfo":{"name":"唐","gender":"男","location":"成都北门","isFirstHome":true,"familyOrganize":"夫妻"},"purchasePurpose":{"primaryPurpose":"首次置业,解决自住","secondaryPurpose":"资产保值,未来可出租","decisionMakers":"夫妻双方"},"coreDemands":{"totalBudget":"350-400"","preferredLayout":"118㎡四房三卫双套房","coreAppeal":"在有限预算内满足家庭居住功能,确保房产保值"},"concerns":["总价超预算风险","板块保值能力"],"decisionProfile":["预算导向,严格控制总价","重点关注户型功能性和实用性"]},{"personalInfo":{"name":"冯女士","gender":"女","location":"","isFirstHome":false,"familyOrganize":"夫妻+1孩+父母同住"},"purchasePurpose":{"primaryPurpose":"改善居住条件","secondaryPurpose":"子女教育质量提升","decisionMakers":"夫妻双方需家庭共同商议]},"coreDemands":{"totalBudget":"400-500","preferredLayout":"118㎡四房三卫(非全景户型)","coreAppeal":"安静舒适、学区有保障的改善型住房"},"concerns":["临路噪音影响老人休息","学区质量和稳定性","社区小,绿化空间有限","得房率是否足够","二八板块学区对比"],"decisionProfile":["对噪音敏感,需要安静环境","重视教育资源配置","关注社区品质和舒适度","需要详细对比不同板块学区优势"]}]` +} + +func (e *Customer) Copy() AdviceData { + return new(Customer) +} + +func (e *Customer) Role() AdviceRole { + return RoleClient +} + +func (e *Customer) Desc() string { + return "客户信息" +} diff --git a/internal/data/mongo_model/advicer_project.go b/internal/data/mongo_model/advicer_project.go new file mode 100644 index 0000000..05f7d64 --- /dev/null +++ b/internal/data/mongo_model/advicer_project.go @@ -0,0 +1,150 @@ +package mongo_model + +import ( + "time" +) + +type AdvicerProjectMongo struct { + ProjectId int32 `json:"projectId" bson:"projectId"` + ProjectInfo ProjectInfo `json:"projectInfo" bson:"projectInfo"` + RegionValue RegionValue `json:"regionValue" bson:"regionValue"` + CompetitionComparison CompetitionComparison `json:"competitionComparison" bson:"competitionComparison"` + CoreSellingPoints CoreSellingPoints `json:"coreSellingPoints" bson:"coreSellingPoints"` + SupportingFacilities SupportingFacilities `json:"supportingFacilities" bson:"supportingFacilities"` + DeveloperBacking DeveloperBacking `json:"developerBacking" bson:"developerBacking"` + LastUpdateTime time.Time `json:"lastUpdateTime" bson:"lastUpdateTime"` +} + +func NewAdvicerProjectMongo() *AdvicerProjectMongo { + return &AdvicerProjectMongo{} +} + +func (a *AdvicerProjectMongo) MongoTableName() string { + return "advicer_project" +} + +type AdvicerProjectMongoEntity struct { + RegionValue RegionValue `json:"regionValue" bson:"regionValue"` + CompetitionComparison CompetitionComparison `json:"competitionComparison" bson:"competitionComparison"` + CoreSellingPoints CoreSellingPoints `json:"coreSellingPoints" bson:"coreSellingPoints"` + SupportingFacilities SupportingFacilities `json:"supportingFacilities" bson:"supportingFacilities"` + DeveloperBacking DeveloperBacking `json:"developerBacking" bson:"developerBacking"` +} + +func (a *AdvicerProjectMongo) Entity() *AdvicerProjectMongoEntity { + return &AdvicerProjectMongoEntity{ + RegionValue: a.RegionValue, + CompetitionComparison: a.CompetitionComparison, + CoreSellingPoints: a.CoreSellingPoints, + SupportingFacilities: a.SupportingFacilities, + DeveloperBacking: a.DeveloperBacking, + } +} + +type ProjectInfo struct { + Name string `json:"projectName" bson:"projectName"` + Address string `json:"projectAddress" bson:"projectAddress"` + Area string `json:"area" bson:"area"` + HouseTypes []HouseType `json:"houseTypes" bson:"houseTypes"` +} + +type HouseType struct { + Name string `json:"name" bson:"name"` + BuildArea string `json:"buildArea" bson:"buildArea"` + InnerArea string `json:"innerArea" bson:"innerArea"` + UnitPrice string `json:"unitPrice" bson:"unitPrice"` + TotalPrice string `json:"totalPrice" bson:"totalPrice"` +} + +// RegionValue 区域价值话术库 +type RegionValue map[string][]string + +func (e *RegionValue) Example() string { + return `{"区位层级":["成华区2.5环内侧,这个位置真的稀缺","槐树店板块现在是成华区的number one板块","北接三板桥商圈,西靠万象城,东临火车东站","属于淮舜板块,万象城东的核心位置"],"地价论证":["我们地价19500,华晨府20400,棕榈也是2万+","2.5环内现在地价没有低于19000的","面粉贵了,面包不可能便宜"],"板块热度":["从21年新希望锦麟一品开始,这边全是高端盘","龙湖最高端的滨江系列在这里,新希望的锦麟系列也在这里","各大品牌开发商争相恐后都在这边拿地"],"发展规划":["槐树店板块是棋盘成钢之后第二个富人区","整个板块都是300万到900万的总价段","未来全是改善型住宅,没有刚需盘"]}` +} +func (e *RegionValue) Copy() AdviceData { + return new(RegionValue) +} + +func (e *RegionValue) Role() AdviceRole { + return RoleProject +} + +func (e *RegionValue) Desc() string { + return "区域价值话术" +} + +// CompetitionComparison 竞品对比话术 +type CompetitionComparison map[string]map[string]string + +func (e *CompetitionComparison) Example() string { + return `{"龙湖滨江云河颂":{"优点承认":"龙湖位置确实好,看沙河公园","价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们得房率118平实得132平,套内单价才33000"},"邦泰云锦":{"定位相似":"邦泰也是首个项目,要打造口碑","价格参考":"他们当时12800拿地,现在卖34000","品质对比":"我们外立面全玻璃幕墙,比他们成本高30%"}}` +} + +func (e *CompetitionComparison) Copy() AdviceData { + return new(CompetitionComparison) +} + +func (e *CompetitionComparison) Role() AdviceRole { + return RoleProject +} + +func (e *CompetitionComparison) Desc() string { + return "竞品对比话术" +} + +// CoreSellingPoints 核心卖点 +type CoreSellingPoints map[string]string + +func (e *CoreSellingPoints) Example() string { + return `{"产品配置高端":"全玻璃幕墙+铝单板外立面,三层中空氩气玻璃,3.2米层高,方太Y9烟机灶具,高仪卫浴","地段稀缺性":"成华区2.5环内侧核心地段,槐树店板块是成华区number one板块,被三板桥、万象城、火车东站包围","得房率高":"118平实得132平,套内单价33000,比龙湖滨江云河颂套内单价低3000"}` +} +func (e *CoreSellingPoints) Copy() AdviceData { + return new(CoreSellingPoints) +} + +func (e *CoreSellingPoints) Role() AdviceRole { + return RoleProject +} + +func (e *CoreSellingPoints) Desc() string { + return "核心卖点" +} + +// SupportingFacilities 配套体系 +type SupportingFacilities map[string]map[string]string + +func (e *SupportingFacilities) Example() string { + return `{"交通配套":{"地铁":"双店路站350米(7号线),槐树店站550米(4号线),未来12号线","道路":"中环路、成洛大道,到春熙路5个站","通达性":"到火车东站2个站,到华西30分钟内"},"教育配套":{"幼儿园":"楼下公立幼儿园","小学":"城市附小锦汇东城(成华区生源最好的学校)","生源优势":"周边新盘都是300万+,生源纯粹"},"医疗配套":{"三甲医院":"市六医院、市二医院3公里内","顶尖医疗":"华西医院锦江院区30分钟车程","便利性":"到华西本部也是30分钟内"}}` +} + +func (e *SupportingFacilities) Copy() AdviceData { + return new(SupportingFacilities) +} + +func (e *SupportingFacilities) Role() AdviceRole { + return RoleProject +} + +func (e *SupportingFacilities) Desc() string { + return "配套体系" +} + +// DeveloperBacking 开发商背书 +type DeveloperBacking map[string]string + +func (e *DeveloperBacking) Example() string { + return `{"公司实力":"中信资产,多元化民营企业","资金安全":"在河南渑池有铝土矿,每年稳定收入10亿","开发经验":"宜宾有5个项目,贵州2个,成都是首个项目","合作方":"招商铂金物业,首次与外部企业合作"}` +} + +func (e *DeveloperBacking) Copy() AdviceData { + return new(DeveloperBacking) +} + +func (e *DeveloperBacking) Role() AdviceRole { + return RoleProject +} + +func (e *DeveloperBacking) Desc() string { + return "开发商背书" +} diff --git a/internal/data/mongo_model/advicer_talk_skill.go b/internal/data/mongo_model/advicer_talk_skill.go new file mode 100644 index 0000000..e3744fa --- /dev/null +++ b/internal/data/mongo_model/advicer_talk_skill.go @@ -0,0 +1,137 @@ +package mongo_model + +import ( + "time" +) + +type AdvicerTalkSkillMongo struct { + ProjectId int32 `json:"projectId" bson:"projectId"` + AdvicerId int32 `json:"advicerId" bson:"advicerId"` + Desc string `json:"desc" bson:"desc"` + NeedsMining NeedsMining `json:"needsMining" bson:"needsMining"` + PainPointResponse PainPointResponse `json:"painPointResponse" bson:"painPointResponse"` + ValueBuilding ValueBuilding `json:"valueBuilding" bson:"valueBuilding"` + ClosingTechniques ClosingTechniques `json:"closingTechniques" bson:"closingTechniques"` + CommunicationRhythm CommunicationRhythm `json:"communicationRhythm" bson:"communicationRhythm"` + LastUpdateTime time.Time `json:"lastUpdateTime" bson:"lastUpdateTime"` +} + +func NewAdvicerTalkSkillMongo() *AdvicerTalkSkillMongo { + return &AdvicerTalkSkillMongo{} +} + +func (a *AdvicerTalkSkillMongo) MongoTableName() string { + return "advicer_talk_skill" +} + +type AdvicerTalkSkillMongoEntity struct { + NeedsMining NeedsMining `json:"needsMining"` + PainPointResponse PainPointResponse `json:"painPointResponse"` + ValueBuilding ValueBuilding `json:"valueBuilding"` + ClosingTechniques ClosingTechniques `json:"closingTechniques"` + CommunicationRhythm CommunicationRhythm `json:"communicationRhythm"` +} + +func (a *AdvicerTalkSkillMongo) Entity() *AdvicerTalkSkillMongoEntity { + return &AdvicerTalkSkillMongoEntity{ + NeedsMining: a.NeedsMining, + PainPointResponse: a.PainPointResponse, + ValueBuilding: a.ValueBuilding, + ClosingTechniques: a.ClosingTechniques, + CommunicationRhythm: a.CommunicationRhythm, + } +} + +// NeedsMining 需求挖掘话术 +type NeedsMining map[string][]string + +func (e *NeedsMining) Example() string { + return `{"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"],"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"]}` +} + +func (e *NeedsMining) Copy() AdviceData { + return new(NeedsMining) +} + +func (e *NeedsMining) Role() AdviceRole { + return RoleSkill +} + +func (e *NeedsMining) Desc() string { + return "需求挖掘话术" +} + +// PainPointResponse 痛点应对策略 +type PainPointResponse map[string]map[string]string + +func (e *PainPointResponse) Example() string { + return `{"地块太小":{"承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩","转化优势":"但人少安静,楼间距反而更开阔","对比竞品":"339的邦泰才11亩,人家上千万豪宅"},"物业费高":{"理解感受":"我懂你,我们也觉得有点贵","价值分析":"但6块里3块是增值服务(保洁、送外卖)","价格补贴":"前三年补贴到5块,跟其他盘差不多"}}` +} +func (e *PainPointResponse) Copy() AdviceData { + return new(PainPointResponse) +} + +func (e *PainPointResponse) Role() AdviceRole { + return RoleSkill +} + +func (e *PainPointResponse) Desc() string { + return "痛点应对策略" +} + +// ValueBuilding 价值塑造技巧 +type ValueBuilding map[string][]string + +func (e *ValueBuilding) Example() string { + return `{"地段价值塑造":["买房最重要的是地段、地段、还是地段","核心地段的核心资产才保值增值","2.5环内的地卖一块少一块,不可再生"],"产品价值塑造":["我们是用改善的价格,买豪宅的标准","很多细节都是3000万豪宅才有的配置","外立面成本比竞品高30%,但单价差不多"]}` +} + +func (e *ValueBuilding) Copy() AdviceData { + return new(ValueBuilding) +} + +func (e *ValueBuilding) Role() AdviceRole { + return RoleSkill +} + +func (e *ValueBuilding) Desc() string { + return "价值塑造技巧" +} + +// ClosingTechniques 促单话术 +type ClosingTechniques map[string]map[string][]string + +func (e *ClosingTechniques) Example() string { + return `{"紧迫感营造":{"时间紧迫":["今天是月底最后一天,领导有压力价格可谈","我们刚刚开盘,还有额外优惠","月底冲业绩,价格最有弹性"],"房源稀缺":["118只剩20多套了,好楼层不多","这栋楼就60户,卖一套少一套","特价房只有这几套,今天不定可能就没了"]},"优惠策略":{"价格优惠":["今天定的话,我可以跟领导申请额外折扣","买车位的话,总价多给两个点优惠","一次性付款再优惠一个点"],"附加价值":["送一年物业费","送品牌家电礼包","优先选车位"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠优先通知你","今天不定的话,我帮你留意好楼层"]}}` +} + +func (e *ClosingTechniques) Copy() AdviceData { + return new(ClosingTechniques) +} + +func (e *ClosingTechniques) Role() AdviceRole { + return RoleSkill +} + +func (e *ClosingTechniques) Desc() string { + return "促单话术" +} + +// CommunicationRhythm 沟通节奏控制 +type CommunicationRhythm map[string]map[string]string + +func (e *CommunicationRhythm) Example() string { + return `{"开场阶段":{"时间占比":"5%","目标":"建立关系,了解需求","关键动作":"亲切称呼,简单寒暄,确认看房重点"},"沙盘讲解":{"时间占比":"30%","目标":"建立价值认知","关键动作":"板块价值→周边配套→项目亮点→开发商介绍"}}` +} + +func (e *CommunicationRhythm) Copy() AdviceData { + return new(CommunicationRhythm) +} + +func (e *CommunicationRhythm) Role() AdviceRole { + return RoleSkill +} + +func (e *CommunicationRhythm) Desc() string { + return "沟通节奏控制" +} diff --git a/internal/data/mongo_model/advicer_version.go b/internal/data/mongo_model/advicer_version.go new file mode 100644 index 0000000..1a66049 --- /dev/null +++ b/internal/data/mongo_model/advicer_version.go @@ -0,0 +1,155 @@ +package mongo_model + +import ( + "time" +) + +type AdvicerVersionMongo struct { + AdvicerId int32 `json:"advicerId" bson:"advicerId"` + VersionDesc string `json:"versionDesc" bson:"versionDesc"` + DialectFeatures DialectFeatures `json:"dialectFeatures" bson:"DialectFeatures"` + SentencePatterns SentencePatterns `json:"sentencePatterns" bson:"sentencePatterns"` + ToneTags ToneTags `json:"toneTags" bson:"toneTags"` + PersonalityTags PersonalityTags `json:"personalityTags" bson:"personalityTags"` + SignatureDialogues SignatureDialogues `json:"signatureDialogues" bson:"signatureDialogues"` + LastUpdateTime time.Time `json:"lastUpdateTime" bson:"lastUpdateTime"` +} + +func NewAdvicerVersionMongo() *AdvicerVersionMongo { + return &AdvicerVersionMongo{} +} + +func (a *AdvicerVersionMongo) MongoTableName() string { + return "advicer_version" +} + +type AdvicerVersionMongoEntity struct { + DialectFeatures DialectFeatures `json:"dialectFeatures"` + SentencePatterns SentencePatterns `json:"sentencePatterns"` + ToneTags ToneTags `json:"toneTags"` + PersonalityTags PersonalityTags `json:"personalityTags"` + SignatureDialogues SignatureDialogues `json:"signatureDialogues"` +} + +func (a *AdvicerVersionMongo) Entity() *AdvicerVersionMongoEntity { + return &AdvicerVersionMongoEntity{ + DialectFeatures: a.DialectFeatures, + SentencePatterns: a.SentencePatterns, + ToneTags: a.ToneTags, + PersonalityTags: a.PersonalityTags, + SignatureDialogues: a.SignatureDialogues, + } +} + +// SignatureDialogues 代表性对话示例 +type SignatureDialogues []struct { + Context string `json:"context"` + Dialogue string `json:"dialogue"` //解释 +} + +// DialectFeatures 方言特征 +type DialectFeatures struct { + Region string `json:"region"` //方言使用程度 + Intensity float64 `json:"intensity"` // 方言使用强度(0-1) + KeyWords []string `json:"KeyWords"` +} + +func (e *DialectFeatures) Example() string { + return `{"region":"四川成都话","intensity":0.4,"key_words":["噻","要得","没得","不晓得","是不是"]}` +} + +func (e *DialectFeatures) Copy() AdviceData { + return new(DialectFeatures) +} + +func (e *DialectFeatures) Role() AdviceRole { + return RoleAdvicer +} + +func (e *DialectFeatures) Desc() string { + return "方言特征" +} + +// SentencePatterns 句子模式 +type SentencePatterns struct { + OpeningMode []string `json:"openingMode"` //开场模式 + ExplanationMode []string `json:"explanationMode"` //解释模式 + ConfirmationMode []string `json:"confirmationMode"` //确认模式 + SummaryMode []string `json:"summaryMode"` //总结模式 + TransitionMode []string `json:"transitionMode"` //过渡模式 +} + +func (e *SentencePatterns) Example() string { + return `{"openingMode":["我给你介绍一下","我们先来看一下"],"explanationMode":["是这样的","我跟你讲","你发现没得"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?"],"summaryMode":["所以说","简单说就是"],"transitionMode":["然后的话","再其次","还有一点"]}` +} + +func (e *SentencePatterns) Copy() AdviceData { + return new(SentencePatterns) +} + +func (e *SentencePatterns) Role() AdviceRole { + return RoleAdvicer +} + +func (e *SentencePatterns) Desc() string { + return "句子模式" +} + +// PersonalityTags 个性标签 +type PersonalityTags []string + +func (e *PersonalityTags) Example() string { + return `["耐心细致","细节控"]` +} +func (e *PersonalityTags) Copy() AdviceData { + return new(PersonalityTags) +} + +func (e *PersonalityTags) Role() AdviceRole { + return RoleAdvicer +} + +func (e *PersonalityTags) Desc() string { + return "个性标签" +} + +// ToneTags 语气标签 +type ToneTags struct { + Enthusiasm float64 `json:"enthusiasm"` + Patience float64 `json:"patience"` + Confidence float64 `json:"confidence"` + Friendliness float64 `json:"friendliness"` + Persuasion float64 `json:"persuasion"` +} + +func (e *ToneTags) Example() string { + return `{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.75,"persuasion":0.7}` +} + +func (e *ToneTags) Copy() AdviceData { + return new(ToneTags) +} + +func (e *ToneTags) Role() AdviceRole { + return RoleAdvicer +} + +func (e *ToneTags) Desc() string { + return "语气标签" +} + +func (e *SignatureDialogues) Example() string { + return `[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都是2.5环内城买房,这种是个普遍存在的一个现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有管家送外卖、免费宠物喂养这些增值服务。你算一下,就算贵一块钱,十年也就多14000,但好物业让房子增值不止这点!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比28板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"}]` +} + +func (e *SignatureDialogues) Copy() AdviceData { + return new(SignatureDialogues) +} + +func (e *SignatureDialogues) Role() AdviceRole { + return RoleAdvicer +} + +func (e *SignatureDialogues) Desc() string { + return "代表性对话示例" +} diff --git a/internal/data/mongo_model/common.go b/internal/data/mongo_model/common.go new file mode 100644 index 0000000..3b20438 --- /dev/null +++ b/internal/data/mongo_model/common.go @@ -0,0 +1,24 @@ +package mongo_model + +type AdviceRole string + +const ( + RoleAdvicer AdviceRole = "advicer" + RoleProject AdviceRole = "project" + RoleSkill AdviceRole = "skill" + RoleClient AdviceRole = "client" +) + +var RoleDesc = map[AdviceRole]string{ + RoleAdvicer: "顾问", + RoleProject: "项目", + RoleSkill: "沟通技巧", + RoleClient: "客户", +} + +type AdviceData interface { + Example() string + Copy() AdviceData + Role() AdviceRole + Desc() string +} diff --git a/internal/data/mongo_model/provider_set.go b/internal/data/mongo_model/provider_set.go new file mode 100644 index 0000000..7520345 --- /dev/null +++ b/internal/data/mongo_model/provider_set.go @@ -0,0 +1,10 @@ +package mongo_model + +import "github.com/google/wire" + +var ProviderSetMongo = wire.NewSet( + NewAdvicerVersionMongo, + NewAdvicerTalkSkillMongo, + NewAdvicerProjectMongo, + NewAdvicerClientMongo, +) diff --git a/internal/entitys/advicer.go b/internal/entitys/advicer.go index 22968c0..eea6d85 100644 --- a/internal/entitys/advicer.go +++ b/internal/entitys/advicer.go @@ -1,387 +1,14 @@ package entitys -type AdviceData interface { - Example() string - Copy() AdviceData - Role() AdviceRole - Desc() string -} - -type AdviceRole string - -const ( - RoleAdvicer AdviceRole = "advicer" - RoleProject AdviceRole = "project" - RoleSkill AdviceRole = "skill" - RoleClient AdviceRole = "client" +import ( + "ai_scheduler/internal/data/model" + "ai_scheduler/internal/data/mongo_model" ) -var RoleDesc = map[AdviceRole]string{ - RoleAdvicer: "顾问", - RoleProject: "项目", - RoleSkill: "沟通技巧", - RoleClient: "客户", -} - -// -------顾问 - -// DialectFeatures 方言特征 -type DialectFeatures struct { - Region string `json:"region"` //方言使用程度 - Intensity float64 `json:"intensity"` // 方言使用强度(0-1) - KeyWords []string `json:"KeyWords"` -} - -func (e *DialectFeatures) Example() string { - return `{"region":"四川成都话","intensity":0.4,"key_words":["噻","要得","没得","不晓得","是不是"]}` -} - -func (e *DialectFeatures) Copy() AdviceData { - return new(DialectFeatures) -} - -func (e *DialectFeatures) Role() AdviceRole { - return RoleAdvicer -} - -func (e *DialectFeatures) Desc() string { - return "方言特征" -} - -// SentencePatterns 句子模式 -type SentencePatterns struct { - OpeningMode []string `json:"openingMode"` //开场模式 - ExplanationMode []string `json:"explanationMode"` //解释模式 - ConfirmationMode []string `json:"confirmationMode"` //确认模式 - SummaryMode []string `json:"summaryMode"` //总结模式 - TransitionMode []string `json:"transitionMode"` //过渡模式 -} - -func (e *SentencePatterns) Example() string { - return `{"openingMode":["我给你介绍一下","我们先来看一下"],"explanationMode":["是这样的","我跟你讲","你发现没得"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?"],"summaryMode":["所以说","简单说就是"],"transitionMode":["然后的话","再其次","还有一点"]}` -} - -func (e *SentencePatterns) Copy() AdviceData { - return new(SentencePatterns) -} - -func (e *SentencePatterns) Role() AdviceRole { - return RoleAdvicer -} - -func (e *SentencePatterns) Desc() string { - return "句子模式" -} - -// PersonalityTags 个性标签 -type PersonalityTags []string - -func (e *PersonalityTags) Example() string { - return `["耐心细致","细节控"]` -} -func (e *PersonalityTags) Copy() AdviceData { - return new(PersonalityTags) -} - -func (e *PersonalityTags) Role() AdviceRole { - return RoleAdvicer -} - -func (e *PersonalityTags) Desc() string { - return "个性标签" -} - -// ToneTags 语气标签 -type ToneTags struct { - Enthusiasm float64 `json:"enthusiasm"` - Patience float64 `json:"patience"` - Confidence float64 `json:"confidence"` - Friendliness float64 `json:"friendliness"` - Persuasion float64 `json:"persuasion"` -} - -func (e *ToneTags) Example() string { - return `{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.75,"persuasion":0.7}` -} - -func (e *ToneTags) Copy() AdviceData { - return new(ToneTags) -} - -func (e *ToneTags) Role() AdviceRole { - return RoleAdvicer -} - -func (e *ToneTags) Desc() string { - return "语气标签" -} - -// SignatureDialogues 代表性对话示例 -type SignatureDialogues []struct { - Context string `json:"context"` - Dialogue string `json:"dialogue"` //解释 -} - -func (e *SignatureDialogues) Example() string { - return `[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都是2.5环内城买房,这种是个普遍存在的一个现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有管家送外卖、免费宠物喂养这些增值服务。你算一下,就算贵一块钱,十年也就多14000,但好物业让房子增值不止这点!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比28板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"}]` -} - -func (e *SignatureDialogues) Copy() AdviceData { - return new(SignatureDialogues) -} - -func (e *SignatureDialogues) Role() AdviceRole { - return RoleAdvicer -} - -func (e *SignatureDialogues) Desc() string { - return "代表性对话示例" -} - -// -------项目 - -// RegionValue 区域价值话术库 -type RegionValue map[string][]string - -func (e *RegionValue) Example() string { - return `{"区位层级":["成华区2.5环内侧,这个位置真的稀缺","槐树店板块现在是成华区的number one板块","北接三板桥商圈,西靠万象城,东临火车东站","属于淮舜板块,万象城东的核心位置"],"地价论证":["我们地价19500,华晨府20400,棕榈也是2万+","2.5环内现在地价没有低于19000的","面粉贵了,面包不可能便宜"],"板块热度":["从21年新希望锦麟一品开始,这边全是高端盘","龙湖最高端的滨江系列在这里,新希望的锦麟系列也在这里","各大品牌开发商争相恐后都在这边拿地"],"发展规划":["槐树店板块是棋盘成钢之后第二个富人区","整个板块都是300万到900万的总价段","未来全是改善型住宅,没有刚需盘"]}` -} -func (e *RegionValue) Copy() AdviceData { - return new(RegionValue) -} - -func (e *RegionValue) Role() AdviceRole { - return RoleProject -} - -func (e *RegionValue) Desc() string { - return "区域价值话术" -} - -// CompetitionComparison 竞品对比话术 -type CompetitionComparison map[string]map[string]string - -func (e *CompetitionComparison) Example() string { - return `{"龙湖滨江云河颂":{"优点承认":"龙湖位置确实好,看沙河公园","价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们得房率118平实得132平,套内单价才33000"},"邦泰云锦":{"定位相似":"邦泰也是首个项目,要打造口碑","价格参考":"他们当时12800拿地,现在卖34000","品质对比":"我们外立面全玻璃幕墙,比他们成本高30%"}}` -} - -func (e *CompetitionComparison) Copy() AdviceData { - return new(CompetitionComparison) -} - -func (e *CompetitionComparison) Role() AdviceRole { - return RoleProject -} - -func (e *CompetitionComparison) Desc() string { - return "竞品对比话术" -} - -// CoreSellingPoints 核心卖点 -type CoreSellingPoints map[string]string - -func (e *CoreSellingPoints) Example() string { - return `{"产品配置高端":"全玻璃幕墙+铝单板外立面,三层中空氩气玻璃,3.2米层高,方太Y9烟机灶具,高仪卫浴","地段稀缺性":"成华区2.5环内侧核心地段,槐树店板块是成华区number one板块,被三板桥、万象城、火车东站包围","得房率高":"118平实得132平,套内单价33000,比龙湖滨江云河颂套内单价低3000"}` -} -func (e *CoreSellingPoints) Copy() AdviceData { - return new(CoreSellingPoints) -} - -func (e *CoreSellingPoints) Role() AdviceRole { - return RoleProject -} - -func (e *CoreSellingPoints) Desc() string { - return "核心卖点" -} - -// SupportingFacilities 配套体系 -type SupportingFacilities map[string]map[string]string - -func (e *SupportingFacilities) Example() string { - return `{"交通配套":{"地铁":"双店路站350米(7号线),槐树店站550米(4号线),未来12号线","道路":"中环路、成洛大道,到春熙路5个站","通达性":"到火车东站2个站,到华西30分钟内"},"教育配套":{"幼儿园":"楼下公立幼儿园","小学":"城市附小锦汇东城(成华区生源最好的学校)","生源优势":"周边新盘都是300万+,生源纯粹"},"医疗配套":{"三甲医院":"市六医院、市二医院3公里内","顶尖医疗":"华西医院锦江院区30分钟车程","便利性":"到华西本部也是30分钟内"}}` -} - -func (e *SupportingFacilities) Copy() AdviceData { - return new(SupportingFacilities) -} - -func (e *SupportingFacilities) Role() AdviceRole { - return RoleProject -} - -func (e *SupportingFacilities) Desc() string { - return "配套体系" -} - -// DeveloperBacking 开发商背书 -type DeveloperBacking map[string]string - -func (e *DeveloperBacking) Example() string { - return `{"公司实力":"中信资产,多元化民营企业","资金安全":"在河南渑池有铝土矿,每年稳定收入10亿","开发经验":"宜宾有5个项目,贵州2个,成都是首个项目","合作方":"招商铂金物业,首次与外部企业合作"}` -} - -func (e *DeveloperBacking) Copy() AdviceData { - return new(DeveloperBacking) -} - -func (e *DeveloperBacking) Role() AdviceRole { - return RoleProject -} - -func (e *DeveloperBacking) Desc() string { - return "开发商背书" -} - -// -------销售话术 - -// NeedsMining 需求挖掘话术 -type NeedsMining map[string][]string - -func (e *NeedsMining) Example() string { - return `{"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"],"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"]}` -} - -func (e *NeedsMining) Copy() AdviceData { - return new(NeedsMining) -} - -func (e *NeedsMining) Role() AdviceRole { - return RoleSkill -} - -func (e *NeedsMining) Desc() string { - return "需求挖掘话术" -} - -// PainPointResponse 痛点应对策略 -type PainPointResponse map[string]map[string]string - -func (e *PainPointResponse) Example() string { - return `{"地块太小":{"承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩","转化优势":"但人少安静,楼间距反而更开阔","对比竞品":"339的邦泰才11亩,人家上千万豪宅"},"物业费高":{"理解感受":"我懂你,我们也觉得有点贵","价值分析":"但6块里3块是增值服务(保洁、送外卖)","价格补贴":"前三年补贴到5块,跟其他盘差不多"}}` -} -func (e *PainPointResponse) Copy() AdviceData { - return new(PainPointResponse) -} - -func (e *PainPointResponse) Role() AdviceRole { - return RoleSkill -} - -func (e *PainPointResponse) Desc() string { - return "痛点应对策略" -} - -// ValueBuilding 价值塑造技巧 -type ValueBuilding map[string][]string - -func (e *ValueBuilding) Example() string { - return `{"地段价值塑造":["买房最重要的是地段、地段、还是地段","核心地段的核心资产才保值增值","2.5环内的地卖一块少一块,不可再生"],"产品价值塑造":["我们是用改善的价格,买豪宅的标准","很多细节都是3000万豪宅才有的配置","外立面成本比竞品高30%,但单价差不多"]}` -} - -func (e *ValueBuilding) Copy() AdviceData { - return new(ValueBuilding) -} - -func (e *ValueBuilding) Role() AdviceRole { - return RoleSkill -} - -func (e *ValueBuilding) Desc() string { - return "价值塑造技巧" -} - -// ClosingTechniques 促单话术 -type ClosingTechniques map[string]map[string][]string - -func (e *ClosingTechniques) Example() string { - return `{"紧迫感营造":{"时间紧迫":["今天是月底最后一天,领导有压力价格可谈","我们刚刚开盘,还有额外优惠","月底冲业绩,价格最有弹性"],"房源稀缺":["118只剩20多套了,好楼层不多","这栋楼就60户,卖一套少一套","特价房只有这几套,今天不定可能就没了"]},"优惠策略":{"价格优惠":["今天定的话,我可以跟领导申请额外折扣","买车位的话,总价多给两个点优惠","一次性付款再优惠一个点"],"附加价值":["送一年物业费","送品牌家电礼包","优先选车位"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠优先通知你","今天不定的话,我帮你留意好楼层"]}}` -} - -func (e *ClosingTechniques) Copy() AdviceData { - return new(ClosingTechniques) -} - -func (e *ClosingTechniques) Role() AdviceRole { - return RoleSkill -} - -func (e *ClosingTechniques) Desc() string { - return "促单话术" -} - -// CommunicationRhythm 沟通节奏控制 -type CommunicationRhythm map[string]map[string]string - -func (e *CommunicationRhythm) Example() string { - return `{"开场阶段":{"时间占比":"5%","目标":"建立关系,了解需求","关键动作":"亲切称呼,简单寒暄,确认看房重点"},"沙盘讲解":{"时间占比":"30%","目标":"建立价值认知","关键动作":"板块价值→周边配套→项目亮点→开发商介绍"}}` -} - -func (e *CommunicationRhythm) Copy() AdviceData { - return new(CommunicationRhythm) -} - -func (e *CommunicationRhythm) Role() AdviceRole { - return RoleSkill -} - -func (e *CommunicationRhythm) Desc() string { - return "沟通节奏控制" -} - -//----------客户 - -// Customer 客户信息 -type Customer []ClientInfo -type ClientInfo struct { - // 个人信息 - PersonalInfo PersonalInfo `json:"personalInfo"` - - // 购房目的 - PurchasePurpose PurchasePurpose `json:"purchasePurpose"` - - // 核心需求 - CoreDemands CoreDemands `json:"coreDemands"` - - // 关注点与顾虑 - Concerns []string `json:"concerns"` - - // 决策建议 - DecisionProfile []string `json:"decisionProfile"` -} - -type PersonalInfo struct { - Name string `json:"name"` // 姓氏 - Gender string `json:"gender"` // 性别 - Location string `json:"location"` // 来源地/当前居住地 - IsFirstHome bool `json:"isFirstHome"` // 是否首套房 - FamilyOrganize string `json:"familyOrganize"` // 家庭人数 -} - -type PurchasePurpose struct { - PrimaryPurpose string `json:"primaryPurpose"` // 主要目的 - SecondaryPurpose string `json:"secondaryPurpose"` // 次要目的 - DecisionMakers string `json:"decisionMakers"` // 决策人 -} - -type CoreDemands struct { - TotalBudget string `json:"totalBudget"` // 预算范围 - PreferredLayout string `json:"preferredLayout"` // 偏好户型 - CoreAppeal string `json:"coreAppeal"` // 核心述求 -} - -func (e *Customer) Example() string { - return `[{"personalInfo":{"name":"唐","gender":"男","location":"成都北门","isFirstHome":true,"familyOrganize":"夫妻+1孩+父母同住"},"purchasePurpose":{"primaryPurpose":"首次置业,解决自住","secondaryPurpose":"资产保值,未来可出租","decisionMakers":"夫妻双方"},"coreDemands":{"totalBudget":"350-400"","preferredLayout":"118㎡四房三卫双套房","coreAppeal":"在有限预算内满足家庭居住功能,确保房产保值"},"concerns":["总价超预算风险","板块保值能力","未来租金回报率","开发商资金实力"],"decisionProfile":["预算导向,严格控制总价","重点关注户型功能性和实用性","需要对比板块发展潜力","对开发商交付能力有顾虑"]},{"personalInfo":{"name":"冯女士","gender":"女","location":"","isFirstHome":false,"familyOrganize":"夫妻+1孩+父母同住"},"purchasePurpose":{"primaryPurpose":"改善居住条件","secondaryPurpose":"子女教育质量提升","decisionMakers":"夫妻双方需家庭共同商议]},"coreDemands":{"totalBudget":"400-500","preferredLayout":"118㎡四房三卫(非全景户型)","coreAppeal":"安静舒适、学区有保障的改善型住房"},"concerns":["临路噪音影响老人休息","学区质量和稳定性","社区小,绿化空间有限","得房率是否足够","二八板块学区对比"],"decisionProfile":["对噪音敏感,需要安静环境","重视教育资源配置","关注社区品质和舒适度","需要详细对比不同板块学区优势"]}]` -} - -func (e *Customer) Copy() AdviceData { - return new(Customer) -} - -func (e *Customer) Role() AdviceRole { - return RoleClient -} - -func (e *Customer) Desc() string { - return "客户信息" +type ChatData struct { + ClientInfo *mongo_model.AdvicerClientMongoEntity `json:"clientInfo"` + TalkSkill *mongo_model.AdvicerTalkSkillMongoEntity `json:"talkSkill"` + ProjectInfo *mongo_model.AdvicerProjectMongoEntity `json:"projectInfo"` + AdvicerInfo *model.AiAdviceAdvicerEntity `json:"advicerInfo"` + AdvicerVersion *mongo_model.AdvicerVersionMongoEntity `json:"advicerVersion"` } diff --git a/internal/entitys/advicer_data.go b/internal/entitys/advicer_data.go index ed72545..4a22a67 100644 --- a/internal/entitys/advicer_data.go +++ b/internal/entitys/advicer_data.go @@ -1,29 +1,173 @@ package entitys +import "ai_scheduler/internal/data/mongo_model" + type AdvicerInitReq struct { - AdvicerID int32 `json:"advicer_id"` - ProjectID int32 `json:"project_id"` - Name string `json:"name"` // 姓名 - Birth string `json:"birth"` // 用户名称 - Gender int32 `json:"gender"` // 1:男,2:女 - WorkingYears int32 `json:"working_years"` // 工作年限 + AdvicerID int32 `json:"AdvicerId"` + ProjectID int32 `json:"ProjectId"` + Name string `json:"name"` // 姓名 + Birth string `json:"birth"` // 用户名称 + Gender int32 `json:"gender"` // 1:男,2:女 + WorkingYears int32 `json:"WorkingYears"` // 工作年限 +} + +type AdvicerInfoReq struct { + AdvicerID int32 `json:"AdvicerId"` } type AdvicerListReq struct { - ProjectId int32 `json:"project_id"` + ProjectId int32 `json:"ProjectId"` } -type AdvicerVersionInitReq struct { - VersionID int32 `json:"version_id"` - AdvicerID int32 `json:"advicer_id"` - VersionDesc string `json:"version_desc"` // 版本名称 - DialectFeatures string `json:"dialect_features"` // 语言风格 - SentencePatterns string `json:"sentence_patterns"` // 句子模式 - ToneTags string `json:"tone_tags"` // 语气标签 - PersonalityTags string `json:"personality_tags"` // 个性标签 - SignatureDialogues string `json:"signature_dialogues"` // 代表性对话示例 +type AdvicerVersionAddReq struct { + AdvicerID int32 `json:"advicerId"` + VersionDesc string `json:"versionDesc"` + DialectFeatures mongo_model.DialectFeatures `json:"dialectFeatures"` + PersonalityTags mongo_model.PersonalityTags `json:"personalityTags"` + SentencePatterns mongo_model.SentencePatterns `json:"sentencePatterns"` + SignatureDialogues mongo_model.SignatureDialogues `json:"signatureDialogues"` + ToneTags mongo_model.ToneTags `json:"toneTags"` +} + +type AdvicerVersionUpdateReq struct { + Id string `json:"id"` + AdvicerID int32 `json:"advicerId"` + VersionDesc string `json:"versionDesc"` + DialectFeatures mongo_model.DialectFeatures `json:"dialectFeatures"` + PersonalityTags mongo_model.PersonalityTags `json:"personalityTags"` + SentencePatterns mongo_model.SentencePatterns `json:"sentencePatterns"` + SignatureDialogues mongo_model.SignatureDialogues `json:"signatureDialogues"` + ToneTags mongo_model.ToneTags `json:"toneTags"` } type AdvicerVersionListReq struct { - AdvicerID int32 `json:"advicer_id"` + Id string `json:"id"` + AdvicerId int32 `json:"advicerId"` + VersionDesc string `json:"versionDesc"` +} + +type AdvicerVersionDelReq struct { + Id string `json:"id"` +} + +type AdvicerVersionInfoReq struct { + Id string `json:"id"` +} + +type AdvicerTalkSkillAddReq struct { + ProjectId int32 `json:"projectId" bson:"projectId"` + AdvicerId int32 `json:"advicerId" bson:"advicerId"` + Desc string `json:"desc" bson:"desc"` + NeedsMining mongo_model.NeedsMining `json:"needsMining" bson:"needsMining"` + PainPointResponse mongo_model.PainPointResponse `json:"painPointResponse" bson:"painPointResponse"` + ValueBuilding mongo_model.ValueBuilding `json:"valueBuilding" bson:"valueBuilding"` + ClosingTechniques mongo_model.ClosingTechniques `json:"closingTechniques" bson:"closingTechniques"` + CommunicationRhythm mongo_model.CommunicationRhythm `json:"communicationRhythm" bson:"communicationRhythm"` +} + +type AdvicerTalkSkillUpdateReq struct { + Id string `json:"id"` + ProjectId int32 `json:"projectId" bson:"projectId"` + AdvicerId int32 `json:"advicerId" bson:"advicerId" :"advicer-id"` + Desc string `json:"desc" bson:"desc" :"desc"` + NeedsMining mongo_model.NeedsMining `json:"needsMining" bson:"needsMining" :"needs-mining"` + PainPointResponse mongo_model.PainPointResponse `json:"painPointResponse" bson:"painPointResponse" :"pain-point-response"` + ValueBuilding mongo_model.ValueBuilding `json:"valueBuilding" bson:"valueBuilding" :"value-building"` + ClosingTechniques mongo_model.ClosingTechniques `json:"closingTechniques" bson:"closingTechniques" :"closing-techniques"` + CommunicationRhythm mongo_model.CommunicationRhythm `json:"communicationRhythm" bson:"communicationRhythm" :"communication-rhythm"` +} + +type AdvicerTalkSkillListReq struct { + Id string `json:"id"` + ProjectId int32 `json:"projectId" bson:"projectId"` + AdvicerId int32 `json:"advicerId" bson:"advicerId"` + Desc string `json:"desc" bson:"desc"` +} + +type AdvicerTalkSkillDelReq struct { + Id string `json:"id"` +} + +type AdvicerTalkSkillInfoReq struct { + Id string `json:"id"` +} + +type AdvicerProjectAddReq struct { + ProjectId int32 `json:"projectId" bson:"projectId"` + ProjectInfo mongo_model.ProjectInfo `json:"projectInfo" bson:"projectInfo"` + RegionValue mongo_model.RegionValue `json:"regionValue" bson:"regionValue"` + CompetitionComparison mongo_model.CompetitionComparison `json:"competitionComparison" bson:"competitionComparison"` + CoreSellingPoints mongo_model.CoreSellingPoints `json:"coreSellingPoints" bson:"coreSellingPoints"` + SupportingFacilities mongo_model.SupportingFacilities `json:"supportingFacilities" bson:"supportingFacilities"` + DeveloperBacking mongo_model.DeveloperBacking `json:"developerBacking" bson:"developerBacking"` +} + +type AdvicerrProjectUpdateReq struct { + Id string `json:"id"` + ProjectId int32 `json:"projectId" bson:"projectId"` + ProjectInfo mongo_model.ProjectInfo `json:"projectInfo" bson:"projectInfo"` + RegionValue mongo_model.RegionValue `json:"regionValue" bson:"regionValue"` + CompetitionComparison mongo_model.CompetitionComparison `json:"competitionComparison" bson:"competitionComparison"` + CoreSellingPoints mongo_model.CoreSellingPoints `json:"coreSellingPoints" bson:"coreSellingPoints"` + SupportingFacilities mongo_model.SupportingFacilities `json:"supportingFacilities" bson:"supportingFacilities"` + DeveloperBacking mongo_model.DeveloperBacking `json:"developerBacking" bson:"developerBacking"` +} + +type AdvicerProjectInfoReq struct { + Id string `json:"id"` + ProjectId int32 `json:"projectId" bson:"projectId"` +} + +type AdvicerClientAddReq struct { + ProjectId int32 `json:"projectId" bson:"projectId"` + AdvicerId int32 `json:"advicerId" bson:"advicerId"` + PersonalInfo mongo_model.PersonalInfo `json:"personalInfo" bson:"personalInfo"` + PurchasePurpose mongo_model.PurchasePurpose `json:"purchasePurpose" bson:"purchasePurpose"` + CoreDemands mongo_model.CoreDemands `json:"coreDemands" bson:"coreDemands"` + Concerns []string `json:"concerns" bson:"concerns"` + DecisionProfile []string `json:"decisionProfile" bson:"decisionProfile"` +} + +type AdvicerrClientUpdateReq struct { + Id string `json:"id"` + ProjectId int32 `json:"projectId" bson:"projectId"` + AdvicerId int32 `json:"advicerId" bson:"advicerId"` + PersonalInfo mongo_model.PersonalInfo `json:"personalInfo" bson:"personalInfo"` + PurchasePurpose mongo_model.PurchasePurpose `json:"purchasePurpose" bson:"purchasePurpose"` + CoreDemands mongo_model.CoreDemands `json:"coreDemands" bson:"coreDemands"` + Concerns []string `json:"concerns" bson:"concerns"` + DecisionProfile []string `json:"decisionProfile" bson:"decisionProfile"` +} + +type AdvicerClientListReq struct { + Id string `json:"id"` + ProjectId int32 `json:"projectId" bson:"projectId"` + AdvicerId int32 `json:"advicerId" bson:"advicerId"` +} + +type AdvicerClientDelReq struct { + Id string `json:"id"` +} + +type AdvicerClientInfoReq struct { + Id string `json:"id"` +} + +type AdvicerChatRegistReq struct { + AdvicerVersionId string `json:"advicerVersionId"` + ClientId string `json:"clientId"` + TalkSkillId string `json:"talkSkillId"` +} + +type AdvicerChatRegistRes struct { + SessionId string `json:"sessionId"` +} + +type AdvicerChatReq struct { + SessionId string `json:"sessionId"` + Content string `json:"content"` +} + +type AdvicerChatRes struct { + Content int32 `json:"content"` } diff --git a/internal/pkg/mongo.go b/internal/pkg/mongo.go index c6dfe76..5642c32 100644 --- a/internal/pkg/mongo.go +++ b/internal/pkg/mongo.go @@ -12,6 +12,7 @@ import ( type Mongo struct { Client *mongo.Client + c *config.Config } func NewMongoDb(ctx context.Context, c *config.Config) (*Mongo, func()) { @@ -23,13 +24,25 @@ func NewMongoDb(ctx context.Context, c *config.Config) (*Mongo, func()) { ConnectTimeout: time.Duration(c.Mongo.ConnectTimeout) * time.Second, SocketTimeout: time.Duration(c.Mongo.SocketTimeout) * time.Second, }) - if err != nil { panic(fmt.Sprintf("mongo数据库错误: %v", err)) } + + if err = transDBClient.Ping(ctx, nil); err != nil { + panic(fmt.Sprintf("mongo链接失败: %v", err)) + } return &Mongo{ Client: transDBClient, + c: c, }, func() { transDBClient.Disconnect(ctx) } } + +type MongoModel interface { + MongoTableName() string +} + +func (m *Mongo) Co(mongoModel MongoModel) *mongo.Collection { + return m.Client.Database(m.c.Mongo.DataBase).Collection(mongoModel.MongoTableName()) +} diff --git a/internal/server/http.go b/internal/server/http.go index 53446c8..91191a1 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -4,22 +4,13 @@ import ( "ai_scheduler/internal/gateway" "ai_scheduler/internal/server/router" "ai_scheduler/internal/services" + "ai_scheduler/internal/services/advice" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" ) -type HTTPServer struct { - app *fiber.App - service *services.ChatService - session *services.SessionService - gateway *gateway.Gateway - callback *services.CallbackService - chatHis *services.HistoryService - capabilityService *services.CapabilityService -} - func NewHTTPServer( service *services.ChatService, session *services.SessionService, @@ -28,10 +19,16 @@ func NewHTTPServer( callback *services.CallbackService, chatHis *services.HistoryService, capabilityService *services.CapabilityService, + adviceFile *advice.FileService, + adviceData *advice.AdvicerService, + adviceChat *advice.ChatService, + adviceProject *advice.ProjectService, + adviceTalkSkill *advice.TalkSkillService, + adviceClient *advice.ClientService, ) *fiber.App { //构建 server app := initRoute() - router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService) + router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService, adviceFile, adviceData, adviceChat, adviceProject, adviceTalkSkill, adviceClient) return app } diff --git a/internal/server/router/router.go b/internal/server/router/router.go index b7a6131..2b66d20 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -1,6 +1,7 @@ package router import ( + errorcode "ai_scheduler/internal/data/error" errors "ai_scheduler/internal/data/error" "ai_scheduler/internal/gateway" "ai_scheduler/internal/services" @@ -15,19 +16,11 @@ import ( "github.com/gofiber/websocket/v2" ) -type RouterServer struct { - app *fiber.App - service *services.ChatService - session *services.SessionService - gateway *gateway.Gateway - chatHist *services.HistoryService - capabilityService *services.CapabilityService -} - // SetupRoutes 设置路由 func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionService *services.SessionService, task *services.TaskService, gateway *gateway.Gateway, callbackService *services.CallbackService, chatHist *services.HistoryService, - capabilityService *services.CapabilityService, adviceFile *advice.FileService, adviceData *advice.DataService, + capabilityService *services.CapabilityService, adviceFile *advice.FileService, adviceData *advice.AdvicerService, + adviceChat *advice.ChatService, adviceProject *advice.ProjectService, adviceTalkSkill *advice.TalkSkillService, adviceClient *advice.ClientService, ) { app.Use(func(c *fiber.Ctx) error { // 设置 CORS 头 @@ -103,28 +96,34 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi advicer := r.Group("advice/") advicer.Post("file/word/ana", adviceFile.WordAna) //顾问 - advicer.Post("file/advicer/add", adviceData.AdvicerAdd) - advicer.Post("file/advicer/update", adviceData.AdvicerUpdate) - advicer.Post("file/advicer/list", adviceData.AdvicerList) - advicer.Post("file/advicer/version/add", adviceData.AdvicerVersionAdd) - advicer.Post("file/advicer/version/update", adviceFile.WordAna) - advicer.Post("file/advicer/version/del", adviceFile.WordAna) - advicer.Post("file/advicer/version/list", adviceFile.WordAna) + advicer.Post("advicer/add", adviceData.AdvicerUpdate) + advicer.Post("advicer/update", adviceData.AdvicerUpdate) + advicer.Post("advicer/list", adviceData.AdvicerList) + advicer.Post("advicer/version/add", adviceData.AdvicerVersionAdd) + advicer.Post("advicer/version/update", adviceData.AdvicerVersionUpdate) + advicer.Post("advicer/version/del", adviceData.AdvicerVersionDel) + advicer.Post("advicer/version/list", adviceData.AdvicerVersionList) //聊天技巧 - advicer.Post("file/skill/list", adviceFile.WordAna) - advicer.Post("file/skill/init", adviceFile.WordAna) - advicer.Post("file/skill/add", adviceFile.WordAna) - advicer.Post("file/skill/update", adviceFile.WordAna) - advicer.Post("file/skill/del", adviceFile.WordAna) - advicer.Post("file/skill/list", adviceFile.WordAna) + advicer.Post("skill/list", adviceTalkSkill.TalkSkillList) + advicer.Post("skill/add", adviceTalkSkill.TalkSkillAdd) + advicer.Post("skill/update", adviceTalkSkill.TalkSkillUpdate) + advicer.Post("skill/del", adviceTalkSkill.TalkSkillUpdate) + //项目 - advicer.Post("file/project/init", adviceFile.WordAna) - advicer.Post("file/project/add", adviceFile.WordAna) - advicer.Post("file/project/update", adviceFile.WordAna) + advicer.Post("project/add", adviceProject.Add) + advicer.Post("project/update", adviceProject.Update) + advicer.Post("project/info", adviceProject.Info) + //客户 - advicer.Post("file/client/init", adviceFile.WordAna) - advicer.Post("file/client/add", adviceFile.WordAna) - advicer.Post("file/client/update", adviceFile.WordAna) + advicer.Post("client/add", adviceClient.Add) + advicer.Post("client/update", adviceClient.Update) + advicer.Post("client/list", adviceClient.List) + advicer.Post("client/del", adviceClient.Del) + + //客户 + advicer.Post("chat/regis", adviceChat.Regis) + advicer.Post("chat/chat", adviceChat.Chat) + } func routerSocket(app *fiber.App, chatService *services.ChatService) { @@ -161,12 +160,13 @@ func registerCommon(c *fiber.Ctx, err error) error { if c.Path() == "/api/v1/qywx/callback" { return nil } - bsErr, ok := err.(*errors.BusinessErr) - if !ok { - bsErr = errors.SystemError - } + // 如果有错误发生 if err != nil { + bsErr, ok := err.(*errors.BusinessErr) + if !ok { + bsErr = errorcode.SysErr(err.Error()) + } // 返回自定义错误响应 return c.JSON(fiber.Map{ "message": bsErr.Error(), @@ -179,10 +179,17 @@ func registerCommon(c *fiber.Ctx, err error) error { // 是 SSE 请求 return c.SendString("这是 SSE 请求") } - var data interface{} - json.Unmarshal(c.Response().Body(), &data) + + body := c.Response().Body() + var rawData json.RawMessage + if len(body) > 0 { + if err := json.Unmarshal(body, &rawData); err != nil { + // 解析失败,作为字符串包装成JSON + rawData = json.RawMessage(`"` + strings.ReplaceAll(string(body), `"`, `\"`) + `"`) + } + } return c.JSON(fiber.Map{ - "data": data, + "data": rawData, "message": errors.Success.Error(), "code": errors.Success.Code(), }) diff --git a/internal/services/advice/advicer.go b/internal/services/advice/advicer.go new file mode 100644 index 0000000..51d1d86 --- /dev/null +++ b/internal/services/advice/advicer.go @@ -0,0 +1,78 @@ +package advice + +import ( + "ai_scheduler/internal/biz" + "ai_scheduler/internal/config" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + + "github.com/gofiber/fiber/v2" +) + +// AdvicerService 数据处理 +type AdvicerService struct { + adviceBiz *biz.AdviceAdvicerBiz + cfg *config.Config +} + +// NewDataService +func NewAdvicerService( + adviceBiz *biz.AdviceAdvicerBiz, + cfg *config.Config, +) *AdvicerService { + return &AdvicerService{ + adviceBiz: adviceBiz, + cfg: cfg, + } +} + +func (d *AdvicerService) AdvicerUpdate(c *fiber.Ctx) error { + req := &entitys.AdvicerInitReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.adviceBiz.Update(c.UserContext(), req) +} + +func (d *AdvicerService) AdvicerList(c *fiber.Ctx) error { + req := &entitys.AdvicerListReq{} + if err := c.BodyParser(req); err != nil { + return err + } + list, err := d.adviceBiz.List(c.UserContext(), req) + return pkg.HandleResponse(c, list, err) +} + +func (d *AdvicerService) AdvicerVersionAdd(c *fiber.Ctx) error { + req := &entitys.AdvicerVersionAddReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.adviceBiz.VersionAdd(c.UserContext(), req) +} + +func (d *AdvicerService) AdvicerVersionUpdate(c *fiber.Ctx) error { + req := &entitys.AdvicerVersionUpdateReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.adviceBiz.VersionUpdate(c.UserContext(), req) +} + +func (d *AdvicerService) AdvicerVersionList(c *fiber.Ctx) error { + req := &entitys.AdvicerVersionListReq{} + if err := c.BodyParser(req); err != nil { + return err + } + list, err := d.adviceBiz.VersionList(c.UserContext(), req) + return pkg.HandleResponse(c, list, err) +} + +func (d *AdvicerService) AdvicerVersionDel(c *fiber.Ctx) error { + req := &entitys.AdvicerVersionDelReq{} + if err := c.BodyParser(req); err != nil { + return err + } + + return d.adviceBiz.VersionDel(c.UserContext(), req) +} diff --git a/internal/services/advice/advicer_test.go b/internal/services/advice/advicer_test.go index 3ced2d2..056ac9f 100644 --- a/internal/services/advice/advicer_test.go +++ b/internal/services/advice/advicer_test.go @@ -5,8 +5,10 @@ import ( "ai_scheduler/internal/biz/llm_service/third_party" "ai_scheduler/internal/config" "ai_scheduler/internal/data/impl" - "ai_scheduler/internal/entitys" + "ai_scheduler/internal/data/mongo_model" + "ai_scheduler/internal/pkg" "ai_scheduler/utils" + "context" "encoding/json" @@ -18,19 +20,47 @@ import ( ) func Test_WordAna(t *testing.T) { - Run(nil) - ana, err := file.WordAnat("https://attachment-public.oss-cn-hangzhou.aliyuncs.com/ai-scheduler/data-analytics/word/content3.docx") + Run(context.Background(), nil) + ana, err := file.WordAnat("https://attachment-public.oss-cn-hangzhou.aliyuncs.com/ai-scheduler/data-analytics/word/content2.docx") t.Log(ana, err) } func Test_AdvicerInit(t *testing.T) { reqBody := `{"advicer_id": 124, "name": "张三111", "birth": "1990-01-01", "gender": 1, "working_years": 10}` - Run([]byte(reqBody)) + Run(context.Background(), []byte(reqBody)) err := advicer.AdvicerUpdate(fiberCtx) t.Log(err) } +func Test_AdvicerVersionAdd(t *testing.T) { + reqBody := `{"advicerId":124,"versionDesc":"第三个版本","dialectFeatures":{"region":"四川成都话","intensity":0.6,"KeyWords":null},"personalityTags":["耐心细致","专业务实","经验丰富","善于引导"],"sentencePatterns":{"openingMode":["我给你介绍一下","我们先来看一下","这边请"],"explanationMode":["是这样的","我跟你讲","你发现没得","说白了"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?","对不对?"],"summaryMode":["所以说","简单说就是","其实"],"transitionMode":["然后的话","再其次","还有一点","另外"]},"signatureDialogues":[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都2.5环内城买房,这种是普遍现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!而且小小区人少安静,圈层更纯粹!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有夜间送外卖、免费宠物喂养、年度保洁这些增值服务。而且前三年开发商补贴一块钱,只需要交5块,跟其他盘差不多!好物业能让房子后期保值增值更多!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比二八板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"},{"context":"客户担心小区小不保值","dialogue":"哥,你不用担心小地块不保值,东大街的九龙仓擎天半岛只有两栋楼,现在二手房还能卖3万左右,是当年的豪宅项目。还有望江名门、仁和春天29号院,都是小地块但照样是高端保值盘。核心还是地段,我们在槐树店这个成华区最贵的板块,保值根本没问题!"}],"toneTags":{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.8,"persuasion":0.75}}` + Run(context.Background(), []byte(reqBody)) + err := advicer.AdvicerVersionAdd(fiberCtx) + t.Log(err) +} + +func Test_AdvicerVersionUpdate(t *testing.T) { + reqBody := `{"id":"69804b5a6532131383aeda3a","advicerId":124,"versionDesc":"第三个版本","dialectFeatures":{"region":"四川成都话","intensity":0.6,"KeyWords":null},"personalityTags":["耐心细致","专业务实","经验丰富","善于引导"],"sentencePatterns":{"openingMode":["我给你介绍一下","我们先来看一下","这边请"],"explanationMode":["是这样的","我跟你讲","你发现没得","说白了"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?","对不对?"],"summaryMode":["所以说","简单说就是","其实"],"transitionMode":["然后的话","再其次","还有一点","另外"]},"signatureDialogues":[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都2.5环内城买房,这种是普遍现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!而且小小区人少安静,圈层更纯粹!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有夜间送外卖、免费宠物喂养、年度保洁这些增值服务。而且前三年开发商补贴一块钱,只需要交5块,跟其他盘差不多!好物业能让房子后期保值增值更多!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比二八板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"},{"context":"客户担心小区小不保值","dialogue":"哥,你不用担心小地块不保值,东大街的九龙仓擎天半岛只有两栋楼,现在二手房还能卖3万左右,是当年的豪宅项目。还有望江名门、仁和春天29号院,都是小地块但照样是高端保值盘。核心还是地段,我们在槐树店这个成华区最贵的板块,保值根本没问题!"}],"toneTags":{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.8,"persuasion":0.75}}` + Run(context.Background(), []byte(reqBody)) + err := advicer.AdvicerVersionUpdate(fiberCtx) + t.Log(err) +} + +func Test_VersionList(t *testing.T) { + reqBody := `{"id":"69804060c17976e5e21858a8"}` + Run(context.Background(), []byte(reqBody)) + err := advicer.AdvicerVersionList(fiberCtx) + t.Log(err) +} + +func Test_AdvicerVersionDel(t *testing.T) { + reqBody := `{"id":"698056073059550befc4f0da"}` + Run(context.Background(), []byte(reqBody)) + err := advicer.AdvicerVersionDel(fiberCtx) + t.Log(err) +} + func Test_Json(t *testing.T) { responseByte, err := os.ReadFile("./res.json") if err != nil { @@ -38,7 +68,7 @@ func Test_Json(t *testing.T) { } var ( result map[string]interface{} - res = make(map[string]entitys.AdviceData) + res = make(map[string]mongo_model.AdviceData) ) if err = json.Unmarshal(responseByte, &result); err != nil { @@ -64,13 +94,13 @@ func Test_Json(t *testing.T) { var ( file *FileService - advicer *DataService + advicer *AdvicerService configConfig *config.Config fiberCtx *fiber.Ctx ) // run 函数是程序的入口函数,负责初始化和配置各个组件 -func Run(reqBody []byte) { +func Run(ctx context.Context, reqBody []byte) { if reqBody != nil { app := fiber.New() fctx := &fasthttp.RequestCtx{} @@ -82,29 +112,42 @@ func Run(reqBody []byte) { configConfig, _ = config.LoadConfigWithEnv() // 初始化数据库连接 db, _ := utils.NewGormDb(configConfig) + rdb := utils.NewRdb(configConfig) advicerImpl := impl.NewAdviceAdvicerImpl(db) - advicerVersionImpl := impl.NewAdviceAdvicerVersionImpl(db) + advicerVersionMongo := mongo_model.NewAdvicerVersionMongo() + advicerTalkSkillMongo := mongo_model.NewAdvicerTalkSkillMongo() + advicerClientMongo := mongo_model.NewAdvicerClientMongo() + advicerProjectMongo := mongo_model.NewAdvicerProjectMongo() hsyq := third_party.NewHsyq() advicerfilebiz := biz.NewAdviceFileBiz(hsyq) - advicerbiz := biz.NewAdviceAdvicerBiz(advicerImpl, advicerVersionImpl) + mongo, _ := pkg.NewMongoDb(ctx, configConfig) + adviceAdvicerBiz := biz.NewAdviceAdvicerBiz(advicerImpl, advicerVersionMongo, mongo) + skillBiz := biz.NewAdviceSkillBiz(advicerTalkSkillMongo, mongo) + clientBiz := biz.NewAdviceClientBiz(advicerClientMongo, mongo) + projectBiz := biz.NewAdviceProjectBiz(advicerProjectMongo, mongo) + chatBiz := biz.NewAdviceChatBiz(hsyq, rdb) file = NewFileService(advicerfilebiz, configConfig) - advicer = NewDataService(advicerbiz, configConfig) + advicer = NewAdvicerService(adviceAdvicerBiz, configConfig) + skill = NewTalkSkillService(skillBiz, configConfig) + client = NewClientService(clientBiz, configConfig) + project = NewProjectService(projectBiz, configConfig) + chat = NewChatService(chatBiz, clientBiz, adviceAdvicerBiz, projectBiz, skillBiz, configConfig) } -var dataMap = map[string]entitys.AdviceData{ - "DialectFeatures": &entitys.DialectFeatures{}, - "SentencePatterns": &entitys.SentencePatterns{}, - "PersonalityTags": &entitys.PersonalityTags{}, - "ToneTags": &entitys.ToneTags{}, - "SignatureDialogues": &entitys.SignatureDialogues{}, - "RegionValue": &entitys.RegionValue{}, - "CompetitionComparison": &entitys.CompetitionComparison{}, - "CoreSellingPoints": &entitys.CoreSellingPoints{}, - "SupportingFacilities": &entitys.SupportingFacilities{}, - "DeveloperBacking": &entitys.DeveloperBacking{}, - "NeedsMining": &entitys.NeedsMining{}, - "PainPointResponse": &entitys.PainPointResponse{}, - "ValueBuilding": &entitys.ValueBuilding{}, - "ClosingTechniques": &entitys.ClosingTechniques{}, - "CommunicationRhythm": &entitys.CommunicationRhythm{}, +var dataMap = map[string]mongo_model.AdviceData{ + "DialectFeatures": &mongo_model.DialectFeatures{}, + "SentencePatterns": &mongo_model.SentencePatterns{}, + "PersonalityTags": &mongo_model.PersonalityTags{}, + "ToneTags": &mongo_model.ToneTags{}, + "SignatureDialogues": &mongo_model.SignatureDialogues{}, + "RegionValue": &mongo_model.RegionValue{}, + "CompetitionComparison": &mongo_model.CompetitionComparison{}, + "CoreSellingPoints": &mongo_model.CoreSellingPoints{}, + "SupportingFacilities": &mongo_model.SupportingFacilities{}, + "DeveloperBacking": &mongo_model.DeveloperBacking{}, + "NeedsMining": &mongo_model.NeedsMining{}, + "PainPointResponse": &mongo_model.PainPointResponse{}, + "ValueBuilding": &mongo_model.ValueBuilding{}, + "ClosingTechniques": &mongo_model.ClosingTechniques{}, + "CommunicationRhythm": &mongo_model.CommunicationRhythm{}, } diff --git a/internal/services/advice/chat.go b/internal/services/advice/chat.go new file mode 100644 index 0000000..4759b7b --- /dev/null +++ b/internal/services/advice/chat.go @@ -0,0 +1,117 @@ +package advice + +import ( + "ai_scheduler/internal/biz" + "ai_scheduler/internal/config" + errorcode "ai_scheduler/internal/data/error" + "ai_scheduler/internal/pkg" + + "ai_scheduler/internal/data/mongo_model" + "ai_scheduler/internal/entitys" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// FileService 文件处理 +type ChatService struct { + adviceChatBiz *biz.AdviceChatBiz + adviceClientBiz *biz.AdviceClientBiz + adviceAdvicerBiz *biz.AdviceAdvicerBiz + adviceProjectBiz *biz.AdviceProjectBiz + adviceSkillBiz *biz.AdviceSkillBiz + cfg *config.Config +} + +// NewFileService +func NewChatService( + adviceChatBiz *biz.AdviceChatBiz, + adviceClientBiz *biz.AdviceClientBiz, + adviceAdvicerBiz *biz.AdviceAdvicerBiz, + adviceProjectBiz *biz.AdviceProjectBiz, + adviceSkillBiz *biz.AdviceSkillBiz, + cfg *config.Config, +) *ChatService { + return &ChatService{ + adviceChatBiz: adviceChatBiz, + cfg: cfg, + adviceClientBiz: adviceClientBiz, + adviceAdvicerBiz: adviceAdvicerBiz, + adviceProjectBiz: adviceProjectBiz, + adviceSkillBiz: adviceSkillBiz, + } +} + +func (a *ChatService) Regis(c *fiber.Ctx) error { + req := &entitys.AdvicerChatRegistReq{} + if err := c.BodyParser(req); err != nil { + return err + } + if len(req.AdvicerVersionId) == 0 { + return errorcode.ParamErr("AdvicerVersionId is empty") + } + if len(req.TalkSkillId) == 0 { + return errorcode.ParamErr("talkSkillId is empty") + } + //顾问版本信息 + versionInfo, err := a.adviceAdvicerBiz.VersionInfo(c.UserContext(), &entitys.AdvicerVersionInfoReq{ + Id: req.AdvicerVersionId, + }) + if err != nil { + return err + } + + //顾问信息 + advicerInfo, err := a.adviceAdvicerBiz.AdvicerInfo(c.UserContext(), &entitys.AdvicerInfoReq{ + AdvicerID: versionInfo.AdvicerId, + }) + if err != nil { + return err + } + //项目信息 + projectInfo, err := a.adviceProjectBiz.Info(c.UserContext(), &entitys.AdvicerProjectInfoReq{ + ProjectId: advicerInfo.ProjectID, + }) + if err != nil { + return err + } + //销售技巧 + talkSkill, err := a.adviceSkillBiz.Info(c.UserContext(), &entitys.AdvicerTalkSkillInfoReq{ + Id: req.TalkSkillId, + }) + if err != nil { + return err + } + //客户信息 + var clientInfo mongo_model.AdvicerClientMongo + if len(req.ClientId) != 0 { + + clientInfo, err = a.adviceClientBiz.Info(c.UserContext(), &entitys.AdvicerClientInfoReq{ + Id: req.ClientId, + }) + if err != nil { + return err + } + } + chat := entitys.ChatData{ + ClientInfo: clientInfo.Entity(), + TalkSkill: talkSkill.Entity(), + ProjectInfo: projectInfo.Entity(), + AdvicerInfo: advicerInfo.Entity(), + AdvicerVersion: versionInfo.Entity(), + } + sessionId, err := a.adviceChatBiz.Regis(c.UserContext(), &chat) + log.Info(sessionId) + return pkg.HandleResponse(c, sessionId, err) +} + +func (a *ChatService) Chat(c *fiber.Ctx) error { + req := &entitys.AdvicerChatReq{} + if err := c.BodyParser(req); err != nil { + return err + } + + res, err := a.adviceChatBiz.Chat(c.UserContext(), req) + log.Info(res) + return pkg.HandleResponse(c, res, err) +} diff --git a/internal/services/advice/chat_test.go b/internal/services/advice/chat_test.go new file mode 100644 index 0000000..d60eb91 --- /dev/null +++ b/internal/services/advice/chat_test.go @@ -0,0 +1,37 @@ +package advice + +import ( + "ai_scheduler/internal/entitys" + "context" + "encoding/json" + + "testing" +) + +func Test_Regis(t *testing.T) { + reqBody := `{"advicerVersionId":"698315725d0077330399f704","clientId":"698199fa0c5f4ae098e009ab","talkSkillId":"698063ff5215bdb9c6344e88"}` + Run(context.Background(), []byte(reqBody)) + err := chat.Regis(fiberCtx) + t.Log(err) +} + +func Test_Chat(t *testing.T) { + reqBody := `{"sessionId":"075a0ec7-2163-4cfa-ae88-c8b8e60e1c66","content":"说话!"}` + Run(context.Background(), []byte(reqBody)) + err := chat.Chat(fiberCtx) + t.Log(err) +} + +func Test_Cache(t *testing.T) { + var chatData entitys.ChatData + chatDataJson := `{"clientInfo":{"personalInfo":{"name":"唐先生1","gender":"男","location":"成都北门","isFirstHome":false,"familyOrganize":"夫妻+1孩+父母同住"},"purchasePurpose":{"primaryPurpose":"改善居住条件","secondaryPurpose":"资产保值,方便子女上学","decisionMakers":"夫妻双方"},"coreDemands":{"totalBudget":"350-400万","preferredLayout":"118㎡四房三卫双套房","coreAppeal":"在预算内满足家庭居住功能,确保房产保值,临近学校"},"concerns":["总价超预算风险","板块保值能力","小区小是否影响居住体验","开发商资金实力"],"decisionProfile":["预算导向,严格控制总价","重点关注户型功能性和实用性","需要对比板块发展潜力","对开发商交付能力有顾虑"]},"talkSkill":{"needsMining":{"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?","对房间数量、卫生间数量有要求吗?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"],"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"]},"painPointResponse":{"地块太小":{"对比竞品":"339的邦泰才11亩,人家是千万级豪宅","承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩","转化优势":"但人少安静,圈层更纯粹,楼间距反而更开阔"},"客户质疑开发商实力":{"合作背书":"招商物业首次外部合作,品牌物业认可开发商实力","实力展示":"公司有6000万吨铝矿,年稳定收入10亿,现金流雄厚","开发经验":"做房地产14年,在宜宾、贵州开发超500万平米项目"},"担心南侧住宅用地遮挡阳光":{"澄清方向":"我们主采光面朝南,南侧住宅用地规划会错开楼间距,不会遮挡","竞品类比":"南侧用地会做高端大户型,开发商会考虑业主采光,不会影响我们的日照"},"担心新小区不保值":{"产品稀缺":"新规产品得房率高,未来政策限制赠送,产品竞争力强","地段支撑":"槐树店是成华区地价最高的板块,周边都是高端项目,地价和高端项目带动房价保值","需求保障":"未来大量业主会置换新规产品,该板块是首选,供需决定价值"},"物业费高":{"价值分析":"但6块里1块是增值服务(保洁、送外卖、宠物服务)","价格补贴":"前三年补贴到5块,跟其他盘差不多","未来可协商":"后期业主委员会可以协商调整物业费,仁恒滨河湾就从7.9谈到5块","理解感受":"我懂你,我们也觉得有点贵"}},"valueBuilding":{"产品价值塑造":["我们是用改善的价格,买豪宅的标准","很多细节都是千万级豪宅才有的配置","外立面成本比竞品高,单价却相当","3.2米层高、无机磨石车库这些都是高端配置"],"地段价值塑造":["买房最重要的是地段、地段、还是地段","核心地段的核心资产才保值增值","2.5环内的地卖一块少一块,不可再生","槐树店是成华区地价最高的板块,地价高对应房价支撑强"]},"closingTechniques":{"优惠策略":{"价格优惠":["双十一特价,118㎡优惠后360-400万,140㎡优惠后450-500万","渠道客户可额外申请优惠,相当于多一个点左右的优惠"],"附加价值":["车位双十一特惠,5.3米长车位9.8万,5.1米长车位8.8万"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠优先通知你","今天不定的话,我帮你留意好楼层"]},"紧迫感营造":{"房源稀缺":["118㎡只剩部分楼层,140㎡只有二十多套公园景观房","好楼层卖一套少一套,性价比高的楼层不多了"],"时间紧迫":["现在是双十一/年底冲刺,有特价优惠","优惠是阶段性的,错过就没有了"]}},"communicationRhythm":{"开场阶段":{"关键动作":"亲切称呼,简单寒暄,确认看房重点","时间占比":"5%","目标":"建立关系,了解需求"},"样板间带看":{"关键动作":"细节讲解→户型优势→空间体验→竞品对比","时间占比":"40%","目标":"强化产品感知"},"沙盘讲解":{"关键动作":"板块价值→周边配套→项目亮点→开发商介绍","时间占比":"30%","目标":"建立价值认知"},"洽谈阶段":{"关键动作":"需求匹配→痛点应对→优惠释放→决策推动","时间占比":"25%","目标":"解决顾虑,促进成交"}}},"projectInfo":{"regionValue":{"区位层级":["成华区2.5环内侧,槐树店板块是成华区number one板块","北接339商圈,西靠万象城,东临火车东站","属于槐树店崔怀板块,成华区目前最好的开发板块"],"发展规划":["槐树店是成华区未来的富人区,板块还有大量待开发土地","未来这个区域会形成连片高端居住区,城市界面会越来越好"],"地价论证":["我们地价19500,比28板块贵5000多","华润华城府地价20400,我们和它同属一个板块,地价差距小","面粉都这么贵,面包不可能便宜"],"板块热度":["从2021年新希望锦麟一品开始,这边全是高端盘","龙湖最高端的滨江云河颂在这里,卖得特别火","各大品牌开发商都在这边拿地,未来全是改善盘"]},"competitionComparison":{"华润置地云上":{"价格对比":"他们单价35000左右,还靠近安置小区","优势突出":"我们小区纯粹,没有安置小区,居住环境更安静","优点承认":"华润品牌影响力大"},"招商璟宸序":{"价格对比":"他们单价32000左右,但地段在28板块,地价比我们便宜5000","优势突出":"我们地段在槐树店,是成华区核心板块,未来增值空间更大","优点承认":"招商品牌大,物业也是自己的"},"龙湖滨江云河颂":{"价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们118平实得132平,套内单价才33000,还做四房三卫,他们143平才双卫","优点承认":"龙湖位置确实好,能看沙河公园"}},"coreSellingPoints":{"产品配置高端":"3.2米层高、全玻璃幕墙+三层中空玻璃、无机磨石车库、方太Y9烟机、高仪卫浴、国千木作柜体","地段稀缺性":"成华区2.5环内侧槐树店板块,成华区房价天花板区域,被万象城、339、火车东站包围","得房率高":"118平实得132平,得房率超110%,四房三卫双套房设计,市面上同面积段没有竞品","物业优质":"招商局铂金物业,有夜间送外卖、免费宠物喂养、全屋保洁等增值服务"},"supportingFacilities":{"交通配套":{"地铁":"7号线双店路站350米,4号线槐树店站550米,未来还有12号线","通达性":"到万象城2个站,到华西锦江院区30分钟车程","道路":"中环路、成洛大道,到春熙路5个站,到火车东站2个站"},"医疗配套":{"三甲医院":"市六医院、市二医院3公里内","顶尖医疗":"华西医院锦江院区、华西本部30分钟车程"},"商业配套":{"便利性":"到万象城2个站,到339商圈3个站","核心商圈":"万象城商圈、339商圈","社区商业":"和悦广场、东方希望上东里"},"教育配套":{"小学":"成华小学,1-3年级在项目附近,4-6年级在二环内","生源优势":"周边新盘都是300万+,生源纯粹"}},"developerBacking":{"公司实力":"中信资产,多元化民营企业,涉及矿产、有色金属、生态农业、地产开发","合作方":"招商局物业,百年央企,首次与外部企业合作提供铂金服务","开发经验":"2011年开始做地产,在宜宾、贵州开发超过500万平米,成都是首个项目","资金安全":"在河南渑池有两座优质铝矿,每年稳定收入10亿,现金流充足"}},"advicerInfo":{"name":"张三111","birth":"1990-01-01","gender":"男","working_years":"10年"},"advicerVersion":{"dialectFeatures":{"region":"四川成都话","intensity":0.6,"KeyWords":null},"sentencePatterns":{"openingMode":["我给你介绍一下","我们先来看一下","这边请"],"explanationMode":["是这样的","我跟你讲","你发现没得","说白了"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?","对不对?"],"summaryMode":["所以说","简单说就是","其实"],"transitionMode":["然后的话","再其次","还有一点","另外"]},"toneTags":{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.8,"persuasion":0.75},"personalityTags":["耐心细致","专业务实","经验丰富","善于引导"],"signatureDialogues":[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都2.5环内城买房,这种是普遍现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!而且小小区人少安静,圈层更纯粹!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有夜间送外卖、免费宠物喂养、年度保洁这些增值服务。而且前三年开发商补贴一块钱,只需要交5块,跟其他盘差不多!好物业能让房子后期保值增值更多!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比二八板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"},{"context":"客户担心小区小不保值","dialogue":"哥,你不用担心小地块不保值,东大街的九龙仓擎天半岛只有两栋楼,现在二手房还能卖3万左右,是当年的豪宅项目。还有望江名门、仁和春天29号院,都是小地块但照样是高端保值盘。核心还是地段,我们在槐树店这个成华区最贵的板块,保值根本没问题!"}]}}` + + err := json.Unmarshal([]byte(chatDataJson), &chatData) + t.Log(err) +} + +var ( + chat *ChatService +) + +// run 函数是程序的入口函数,负责初始化和配置各个组件 diff --git a/internal/services/advice/client.go b/internal/services/advice/client.go new file mode 100644 index 0000000..d884414 --- /dev/null +++ b/internal/services/advice/client.go @@ -0,0 +1,61 @@ +package advice + +import ( + "ai_scheduler/internal/biz" + "ai_scheduler/internal/config" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + + "github.com/gofiber/fiber/v2" +) + +// ClientService 数据处理 +type ClientService struct { + AdviceClientBiz *biz.AdviceClientBiz + cfg *config.Config +} + +// NewDataService +func NewClientService( + AdviceClientBiz *biz.AdviceClientBiz, + cfg *config.Config, +) *ClientService { + return &ClientService{ + AdviceClientBiz: AdviceClientBiz, + cfg: cfg, + } +} + +func (d *ClientService) Add(c *fiber.Ctx) error { + req := &entitys.AdvicerClientAddReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.AdviceClientBiz.Add(c.UserContext(), req) +} + +func (d *ClientService) Update(c *fiber.Ctx) error { + req := &entitys.AdvicerrClientUpdateReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.AdviceClientBiz.Update(c.UserContext(), req) +} + +func (d *ClientService) List(c *fiber.Ctx) error { + req := &entitys.AdvicerClientListReq{} + if err := c.BodyParser(req); err != nil { + return err + } + list, err := d.AdviceClientBiz.List(c.UserContext(), req) + return pkg.HandleResponse(c, list, err) +} + +func (d *ClientService) Del(c *fiber.Ctx) error { + req := &entitys.AdvicerClientDelReq{} + if err := c.BodyParser(req); err != nil { + return err + } + + return d.AdviceClientBiz.Del(c.UserContext(), req) +} diff --git a/internal/services/advice/client_test.go b/internal/services/advice/client_test.go new file mode 100644 index 0000000..f94e2cb --- /dev/null +++ b/internal/services/advice/client_test.go @@ -0,0 +1,39 @@ +package advice + +import ( + "context" + + "testing" +) + +func Test_ClientAdd(t *testing.T) { + reqBody := `{"projectId":1,"AdvicerId":1,"personalInfo":{"name":"杜先生","gender":"男","location":"","isFirstHome":false,"familyOrganize":"夫妻+2孩"},"purchasePurpose":{"primaryPurpose":"改善居住条件","secondaryPurpose":"资产保值","decisionMakers":"夫妻双方"},"coreDemands":{"totalBudget":"450-500万","preferredLayout":"140㎡四房三卫","coreAppeal":"户型实用、景观好、社区品质高"},"concerns":["小区小是否保值","价格是否有优惠","开发商交付能力"],"decisionProfile":["注重户型实用性和景观","关注社区品质和后期保值","对价格敏感,希望拿到优惠"]}` + Run(context.Background(), []byte(reqBody)) + err := client.Add(fiberCtx) + t.Log(err) +} + +func Test_ClientUpdate(t *testing.T) { + reqBody := `{"id":"698199fa0c5f4ae098e009ab","projectId":1,"AdvicerId":1,"personalInfo":{"name":"唐先生1","gender":"男","location":"成都北门","isFirstHome":false,"familyOrganize":"夫妻+1孩+父母同住"},"purchasePurpose":{"primaryPurpose":"改善居住条件","secondaryPurpose":"资产保值,方便子女上学","decisionMakers":"夫妻双方"},"coreDemands":{"totalBudget":"350-400万","preferredLayout":"118㎡四房三卫双套房","coreAppeal":"在预算内满足家庭居住功能,确保房产保值,临近学校"},"concerns":["总价超预算风险","板块保值能力","小区小是否影响居住体验","开发商资金实力"],"decisionProfile":["预算导向,严格控制总价","重点关注户型功能性和实用性","需要对比板块发展潜力","对开发商交付能力有顾虑"]}` + Run(context.Background(), []byte(reqBody)) + err := client.Update(fiberCtx) + t.Log(err) +} + +func Test_ClientList(t *testing.T) { + reqBody := `{"projectId":1}` + Run(context.Background(), []byte(reqBody)) + err := client.List(fiberCtx) + t.Log(err) +} + +func Test_ClientDel(t *testing.T) { + reqBody := `{"id":"698056073059550befc4f0da"}` + Run(context.Background(), []byte(reqBody)) + err := advicer.AdvicerVersionDel(fiberCtx) + t.Log(err) +} + +var ( + client *ClientService +) diff --git a/internal/services/advice/data.go b/internal/services/advice/data.go deleted file mode 100644 index fc12207..0000000 --- a/internal/services/advice/data.go +++ /dev/null @@ -1,60 +0,0 @@ -package advice - -import ( - "ai_scheduler/internal/biz" - "ai_scheduler/internal/config" - "ai_scheduler/internal/entitys" - "ai_scheduler/internal/pkg" - - "github.com/gofiber/fiber/v2" -) - -// DataService 数据处理 -type DataService struct { - adviceBiz *biz.AdviceAdvicerBiz - cfg *config.Config -} - -// NewDataService -func NewDataService( - adviceBiz *biz.AdviceAdvicerBiz, - cfg *config.Config, -) *DataService { - return &DataService{ - adviceBiz: adviceBiz, - cfg: cfg, - } -} - -func (d *DataService) AdvicerAdd(c *fiber.Ctx) error { - req := &entitys.AdvicerInitReq{} - if err := c.BodyParser(req); err != nil { - return err - } - return d.adviceBiz.Update(c.UserContext(), req) -} - -func (d *DataService) AdvicerUpdate(c *fiber.Ctx) error { - req := &entitys.AdvicerInitReq{} - if err := c.BodyParser(req); err != nil { - return err - } - return d.adviceBiz.Update(c.UserContext(), req) -} - -func (d *DataService) AdvicerList(c *fiber.Ctx) error { - req := &entitys.AdvicerListReq{} - if err := c.BodyParser(req); err != nil { - return err - } - list, err := d.adviceBiz.List(c.UserContext(), req) - return pkg.HandleResponse(c, list, err) -} - -func (d *DataService) AdvicerVersionAdd(c *fiber.Ctx) error { - req := &entitys.AdvicerVersionInitReq{} - if err := c.BodyParser(req); err != nil { - return err - } - return d.adviceBiz.VersionUpdate(c.UserContext(), req) -} diff --git a/internal/services/advice/project.go b/internal/services/advice/project.go new file mode 100644 index 0000000..22cd036 --- /dev/null +++ b/internal/services/advice/project.go @@ -0,0 +1,52 @@ +package advice + +import ( + "ai_scheduler/internal/biz" + "ai_scheduler/internal/config" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + + "github.com/gofiber/fiber/v2" +) + +// ProjectService 数据处理 +type ProjectService struct { + adviceProjectBiz *biz.AdviceProjectBiz + cfg *config.Config +} + +// NewProjectService +func NewProjectService( + adviceProjectBiz *biz.AdviceProjectBiz, + cfg *config.Config, +) *ProjectService { + return &ProjectService{ + adviceProjectBiz: adviceProjectBiz, + cfg: cfg, + } +} + +func (d *ProjectService) Add(c *fiber.Ctx) error { + req := &entitys.AdvicerProjectAddReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.adviceProjectBiz.Add(c.UserContext(), req) +} + +func (d *ProjectService) Update(c *fiber.Ctx) error { + req := &entitys.AdvicerrProjectUpdateReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.adviceProjectBiz.Update(c.UserContext(), req) +} + +func (d *ProjectService) Info(c *fiber.Ctx) error { + req := &entitys.AdvicerProjectInfoReq{} + if err := c.BodyParser(req); err != nil { + return err + } + list, err := d.adviceProjectBiz.Info(c.UserContext(), req) + return pkg.HandleResponse(c, list, err) +} diff --git a/internal/services/advice/project_test.go b/internal/services/advice/project_test.go new file mode 100644 index 0000000..6375353 --- /dev/null +++ b/internal/services/advice/project_test.go @@ -0,0 +1,30 @@ +package advice + +import ( + "context" + + "testing" +) + +func Test_ProjectAdd(t *testing.T) { + reqBody := `{"projectId":1,"projectInfo":{"projectName":"中信资产项目 *","projectAddress":"成华区槐树店板块,2.5环内侧 *","area":"成华区","houseTypes":[{"name":"118平户型","buildArea":"118㎡","innerArea":"132㎡ *","unitPrice":"约33000元/㎡ * (套内单价)","totalPrice":"约389万元 *"},{"name":"核心主力户型 *","buildArea":"120-140㎡ *","innerArea":"高得房率,约132-156㎡ *","unitPrice":"约32000-35000元/㎡ *","totalPrice":"约384-490万元 *"},{"name":"大平层户型 *","buildArea":"140㎡+ *","innerArea":"实得约156㎡+ * (得房率110%+)","unitPrice":"约35000元/㎡ *","totalPrice":"约490万元+ *"}]},"competitionComparison":{"华润置地云上":{"价格对比":"他们单价35000左右,还靠近安置小区","优势突出":"我们小区纯粹,没有安置小区,居住环境更安静","优点承认":"华润品牌影响力大"},"招商璟宸序":{"价格对比":"他们单价32000左右,但地段在28板块,地价比我们便宜5000","优势突出":"我们地段在槐树店,是成华区核心板块,未来增值空间更大","优点承认":"招商品牌大,物业也是自己的"},"龙湖滨江云河颂":{"价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们118平实得132平,套内单价才33000,还做四房三卫,他们143平才双卫","优点承认":"龙湖位置确实好,能看沙河公园"}},"coreSellingPoints":{"产品配置高端":"3.2米层高、全玻璃幕墙+三层中空玻璃、无机磨石车库、方太Y9烟机、高仪卫浴、国千木作柜体","地段稀缺性":"成华区2.5环内侧槐树店板块,成华区房价天花板区域,被万象城、339、火车东站包围","得房率高":"118平实得132平,得房率超110%,四房三卫双套房设计,市面上同面积段没有竞品","物业优质":"招商局铂金物业,有夜间送外卖、免费宠物喂养、全屋保洁等增值服务"},"developerBacking":{"公司实力":"中信资产,多元化民营企业,涉及矿产、有色金属、生态农业、地产开发","合作方":"招商局物业,百年央企,首次与外部企业合作提供铂金服务","开发经验":"2011年开始做地产,在宜宾、贵州开发超过500万平米,成都是首个项目","资金安全":"在河南渑池有两座优质铝矿,每年稳定收入10亿,现金流充足"},"regionValue":{"区位层级":["成华区2.5环内侧,槐树店板块是成华区number one板块","北接339商圈,西靠万象城,东临火车东站","属于槐树店崔怀板块,成华区目前最好的开发板块"],"发展规划":["槐树店是成华区未来的富人区,板块还有大量待开发土地","未来这个区域会形成连片高端居住区,城市界面会越来越好"],"地价论证":["我们地价19500,比28板块贵5000多","华润华城府地价20400,我们和它同属一个板块,地价差距小","面粉都这么贵,面包不可能便宜"],"板块热度":["从2021年新希望锦麟一品开始,这边全是高端盘","龙湖最高端的滨江云河颂在这里,卖得特别火","各大品牌开发商都在这边拿地,未来全是改善盘"]},"supportingFacilities":{"交通配套":{"地铁":"7号线双店路站350米,4号线槐树店站550米,未来还有12号线","通达性":"到万象城2个站,到华西锦江院区30分钟车程","道路":"中环路、成洛大道,到春熙路5个站,到火车东站2个站"},"医疗配套":{"三甲医院":"市六医院、市二医院3公里内","顶尖医疗":"华西医院锦江院区、华西本部30分钟车程"},"商业配套":{"便利性":"到万象城2个站,到339商圈3个站","核心商圈":"万象城商圈、339商圈","社区商业":"和悦广场、东方希望上东里"},"教育配套":{"小学":"成华小学,1-3年级在项目附近,4-6年级在二环内","生源优势":"周边新盘都是300万+,生源纯粹"}}}` + Run(context.Background(), []byte(reqBody)) + err := project.Add(fiberCtx) + t.Log(err) +} + +func Test_ProjectUpdate(t *testing.T) { + reqBody := `{"id":"69804b5a6532131383aeda3a","advicerId":124,"versionDesc":"第三个版本","dialectFeatures":{"region":"四川成都话","intensity":0.6,"KeyWords":null},"personalityTags":["耐心细致","专业务实","经验丰富","善于引导"],"sentencePatterns":{"openingMode":["我给你介绍一下","我们先来看一下","这边请"],"explanationMode":["是这样的","我跟你讲","你发现没得","说白了"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?","对不对?"],"summaryMode":["所以说","简单说就是","其实"],"transitionMode":["然后的话","再其次","还有一点","另外"]},"signatureDialogues":[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都2.5环内城买房,这种是普遍现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!而且小小区人少安静,圈层更纯粹!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有夜间送外卖、免费宠物喂养、年度保洁这些增值服务。而且前三年开发商补贴一块钱,只需要交5块,跟其他盘差不多!好物业能让房子后期保值增值更多!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比二八板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"},{"context":"客户担心小区小不保值","dialogue":"哥,你不用担心小地块不保值,东大街的九龙仓擎天半岛只有两栋楼,现在二手房还能卖3万左右,是当年的豪宅项目。还有望江名门、仁和春天29号院,都是小地块但照样是高端保值盘。核心还是地段,我们在槐树店这个成华区最贵的板块,保值根本没问题!"}],"toneTags":{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.8,"persuasion":0.75}}` + Run(context.Background(), []byte(reqBody)) + err := project.Update(fiberCtx) + t.Log(err) +} + +func Test_ProjectInfo(t *testing.T) { + reqBody := `{"id":"69804b5a6532131383aeda3a","advicerId":124,"versionDesc":"第三个版本","dialectFeatures":{"region":"四川成都话","intensity":0.6,"KeyWords":null},"personalityTags":["耐心细致","专业务实","经验丰富","善于引导"],"sentencePatterns":{"openingMode":["我给你介绍一下","我们先来看一下","这边请"],"explanationMode":["是这样的","我跟你讲","你发现没得","说白了"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?","对不对?"],"summaryMode":["所以说","简单说就是","其实"],"transitionMode":["然后的话","再其次","还有一点","另外"]},"signatureDialogues":[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都2.5环内城买房,这种是普遍现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!而且小小区人少安静,圈层更纯粹!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有夜间送外卖、免费宠物喂养、年度保洁这些增值服务。而且前三年开发商补贴一块钱,只需要交5块,跟其他盘差不多!好物业能让房子后期保值增值更多!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比二八板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"},{"context":"客户担心小区小不保值","dialogue":"哥,你不用担心小地块不保值,东大街的九龙仓擎天半岛只有两栋楼,现在二手房还能卖3万左右,是当年的豪宅项目。还有望江名门、仁和春天29号院,都是小地块但照样是高端保值盘。核心还是地段,我们在槐树店这个成华区最贵的板块,保值根本没问题!"}],"toneTags":{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.8,"persuasion":0.75}}` + Run(context.Background(), []byte(reqBody)) + err := project.Info(fiberCtx) + t.Log(err) +} + +var project *ProjectService diff --git a/internal/services/advice/talk_skill.go b/internal/services/advice/talk_skill.go new file mode 100644 index 0000000..d47ccec --- /dev/null +++ b/internal/services/advice/talk_skill.go @@ -0,0 +1,61 @@ +package advice + +import ( + "ai_scheduler/internal/biz" + "ai_scheduler/internal/config" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + + "github.com/gofiber/fiber/v2" +) + +// TalkSkillService 数据处理 +type TalkSkillService struct { + adviceSkillBiz *biz.AdviceSkillBiz + cfg *config.Config +} + +// NewTalkSkillService +func NewTalkSkillService( + adviceSkillBiz *biz.AdviceSkillBiz, + cfg *config.Config, +) *TalkSkillService { + return &TalkSkillService{ + adviceSkillBiz: adviceSkillBiz, + cfg: cfg, + } +} + +func (d *TalkSkillService) TalkSkillAdd(c *fiber.Ctx) error { + req := &entitys.AdvicerTalkSkillAddReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.adviceSkillBiz.VersionAdd(c.UserContext(), req) +} + +func (d *TalkSkillService) TalkSkillUpdate(c *fiber.Ctx) error { + req := &entitys.AdvicerTalkSkillUpdateReq{} + if err := c.BodyParser(req); err != nil { + return err + } + return d.adviceSkillBiz.VersionUpdate(c.UserContext(), req) +} + +func (d *TalkSkillService) TalkSkillList(c *fiber.Ctx) error { + req := &entitys.AdvicerTalkSkillListReq{} + if err := c.BodyParser(req); err != nil { + return err + } + list, err := d.adviceSkillBiz.VersionList(c.UserContext(), req) + return pkg.HandleResponse(c, list, err) +} + +func (d *TalkSkillService) TalkSkillDel(c *fiber.Ctx) error { + req := &entitys.AdvicerTalkSkillDelReq{} + if err := c.BodyParser(req); err != nil { + return err + } + + return d.adviceSkillBiz.VersionDel(c.UserContext(), req) +} diff --git a/internal/services/advice/talk_skill_test.go b/internal/services/advice/talk_skill_test.go new file mode 100644 index 0000000..9de7e30 --- /dev/null +++ b/internal/services/advice/talk_skill_test.go @@ -0,0 +1,41 @@ +package advice + +import ( + "context" + + "testing" +) + +func Test_TalkSkillAdd(t *testing.T) { + reqBody := `{"advicerId":124,"projectId":1,"desc":"第一版本","closingTechniques":{"优惠策略":{"价格优惠":["今天定的话,我可以跟领导申请额外1个点的折扣","买车位的话,总价再给你优惠2万块","一次性付款的话,还能再降1个点"],"附加价值":["送一年物业费","送品牌家电礼包","优先选车位"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠我第一时间通知你","今天不定的话,我帮你留意着这个好楼层"]},"紧迫感营造":{"房源稀缺":["118的户型只剩20多套了,好楼层只有这几套了","这栋楼一共就60户,卖一套少一套,现在不订可能就没了"],"时间紧迫":["今天是周末活动最后一天,这个价格只有今天能申请","月底冲业绩,领导给的权限最大,过了今天就没这个优惠了"]}},"communicationRhythm":{"开场阶段":{"关键动作":"亲切称呼,简单寒暄,确认客户关注点","时间占比":"5%","目标":"建立关系,了解需求"},"样板间带看":{"关键动作":"讲解户型功能→展示装修标准→强调细节品质","时间占比":"40%","目标":"体验产品优势"},"沙盘讲解":{"关键动作":"板块价值→周边配套→项目亮点→开发商介绍","时间占比":"30%","目标":"建立价值认知"},"洽谈阶段":{"关键动作":"算价格→对比竞品→解决顾虑→逼定成交","时间占比":"25%","目标":"促单成交"}},"needsMining":{"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?"],"教育需求":["小孩在哪里上学?对学校距离有要求吗?","看重学校的哪些方面?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"],"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"]},"painPointResponse":{"小区太小":{"对比竞品":"仁和春天29号院才29亩,照样是千万级豪宅","承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩,339的邦泰才11亩","转化优势":"但小区人少安静,楼间距开阔,200多米的楼间距比很多大楼盘还宽"},"担心保值":{"举例论证":"你看九龙仓擎天半岛,就两栋楼,现在二手房还是卖3万多;望江名门一栋楼,照样是千万级豪宅","承认顾虑":"我理解你担心小小区不保值","核心逻辑":"保值看的是地段,我们槐树店是成华区核心板块,地价19500,未来只会涨不会跌"},"物业费高":{"价值分析":"但6块里有2块是增值服务,招商物业是铂金服务,这些服务外面花钱都买不来","价格补贴":"前三年开发商补贴1块,你只需要交5块,和其他改善盘差不多","理解感受":"我懂你觉得6块有点贵"}},"valueBuilding":{"产品价值塑造":["我们是用改善的价格,买豪宅的配置","3.2米层高、全落地窗、无机磨石车库,这些都是千万级豪宅的标配","118平实得132平,得房率超过110%,市面上找不到第二家"],"地段价值塑造":["买房最重要的是地段、地段、还是地段","2.5环内的核心地段卖一块少一块,不可再生","槐树店是成华区房价天花板,买这里的房子保值有保障"]}}` + Run(context.Background(), []byte(reqBody)) + err := skill.TalkSkillAdd(fiberCtx) + t.Log(err) +} + +func Test_TalkSkillUpdate(t *testing.T) { + reqBody := `{"id":"698063ff5215bdb9c6344e88","advicerId":124,"projectId":3,"desc":"第0版本","closingTechniques":{"优惠策略":{"价格优惠":["双十一特价,118㎡优惠后360-400万,140㎡优惠后450-500万","渠道客户可额外申请优惠,相当于多一个点左右的优惠"],"附加价值":["车位双十一特惠,5.3米长车位9.8万,5.1米长车位8.8万"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠优先通知你","今天不定的话,我帮你留意好楼层"]},"紧迫感营造":{"房源稀缺":["118㎡只剩部分楼层,140㎡只有二十多套公园景观房","好楼层卖一套少一套,性价比高的楼层不多了"],"时间紧迫":["现在是双十一/年底冲刺,有特价优惠","优惠是阶段性的,错过就没有了"]}},"communicationRhythm":{"开场阶段":{"关键动作":"亲切称呼,简单寒暄,确认看房重点","时间占比":"5%","目标":"建立关系,了解需求"},"样板间带看":{"关键动作":"细节讲解→户型优势→空间体验→竞品对比","时间占比":"40%","目标":"强化产品感知"},"沙盘讲解":{"关键动作":"板块价值→周边配套→项目亮点→开发商介绍","时间占比":"30%","目标":"建立价值认知"},"洽谈阶段":{"关键动作":"需求匹配→痛点应对→优惠释放→决策推动","时间占比":"25%","目标":"解决顾虑,促进成交"}},"needsMining":{"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?","对房间数量、卫生间数量有要求吗?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"],"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"]},"painPointResponse":{"地块太小":{"对比竞品":"339的邦泰才11亩,人家是千万级豪宅","承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩","转化优势":"但人少安静,圈层更纯粹,楼间距反而更开阔"},"客户质疑开发商实力":{"合作背书":"招商物业首次外部合作,品牌物业认可开发商实力","实力展示":"公司有6000万吨铝矿,年稳定收入10亿,现金流雄厚","开发经验":"做房地产14年,在宜宾、贵州开发超500万平米项目"},"担心南侧住宅用地遮挡阳光":{"澄清方向":"我们主采光面朝南,南侧住宅用地规划会错开楼间距,不会遮挡","竞品类比":"南侧用地会做高端大户型,开发商会考虑业主采光,不会影响我们的日照"},"担心新小区不保值":{"产品稀缺":"新规产品得房率高,未来政策限制赠送,产品竞争力强","地段支撑":"槐树店是成华区地价最高的板块,周边都是高端项目,地价和高端项目带动房价保值","需求保障":"未来大量业主会置换新规产品,该板块是首选,供需决定价值"},"物业费高":{"价值分析":"但6块里1块是增值服务(保洁、送外卖、宠物服务)","价格补贴":"前三年补贴到5块,跟其他盘差不多","未来可协商":"后期业主委员会可以协商调整物业费,仁恒滨河湾就从7.9谈到5块","理解感受":"我懂你,我们也觉得有点贵"}},"valueBuilding":{"产品价值塑造":["我们是用改善的价格,买豪宅的标准","很多细节都是千万级豪宅才有的配置","外立面成本比竞品高,单价却相当","3.2米层高、无机磨石车库这些都是高端配置"],"地段价值塑造":["买房最重要的是地段、地段、还是地段","核心地段的核心资产才保值增值","2.5环内的地卖一块少一块,不可再生","槐树店是成华区地价最高的板块,地价高对应房价支撑强"]}}` + Run(context.Background(), []byte(reqBody)) + err := skill.TalkSkillUpdate(fiberCtx) + t.Log(err) +} + +func Test_TalkSkillList(t *testing.T) { + reqBody := `{"projectId":1}` + Run(context.Background(), []byte(reqBody)) + err := skill.TalkSkillList(fiberCtx) + t.Log(err) +} + +func Test_TalkSkillDel(t *testing.T) { + reqBody := `{"id":"698056073059550befc4f0da"}` + Run(context.Background(), []byte(reqBody)) + err := skill.TalkSkillDel(fiberCtx) + t.Log(err) +} + +var ( + skill *TalkSkillService +) + +// run 函数是程序的入口函数,负责初始化和配置各个组件 diff --git a/internal/services/provider_set.go b/internal/services/provider_set.go index dfce67a..75178d5 100644 --- a/internal/services/provider_set.go +++ b/internal/services/provider_set.go @@ -16,5 +16,10 @@ var ProviderSetServices = wire.NewSet( NewHistoryService, NewCapabilityService, NewCronService, - advice.NewAdviceService, + advice.NewFileService, + advice.NewAdvicerService, + advice.NewTalkSkillService, + advice.NewProjectService, + advice.NewClientService, + advice.NewChatService, ) From 6394715bfe22ab3ac0b65446dc065d1b0691f813 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Thu, 5 Feb 2026 13:39:44 +0800 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84advice?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8F=8A=E6=B7=BB=E5=8A=A0MongoDB=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index fbcd581..e9a2b97 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -19,7 +19,6 @@ func main() { if err != nil { log.Fatalf("加载配置失败: %v", err) } - app, cleanup, err := InitializeApp(ctx, bc, log.DefaultLogger()) if err != nil { log.Fatalf("项目初始化失败: %v", err)