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] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84advice=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=8F=8A=E6=B7=BB=E5=8A=A0MongoDB=E6=94=AF=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, )