ai_scheduler/internal/biz/advice_advicer.go

420 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package biz
import (
"ai_scheduler/internal/biz/llm_service/third_party"
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/data/mongo_model"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"context"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"xorm.io/builder"
)
type AdviceAdvicerBiz struct {
advicerImpl *impl.AdviceAdvicerImpl
advicerVersionMongo *mongo_model.AdvicerVersionMongo
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl
adviceProjectImpl *impl.AdviceProjectImpl
mongo *pkg.Mongo
}
func NewAdviceAdvicerBiz(
advicerImpl *impl.AdviceAdvicerImpl,
advicerVersionMongo *mongo_model.AdvicerVersionMongo,
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl,
adviceProjectImpl *impl.AdviceProjectImpl,
mongo *pkg.Mongo,
) *AdviceAdvicerBiz {
return &AdviceAdvicerBiz{
advicerImpl: advicerImpl,
advicerVersionMongo: advicerVersionMongo,
aiAdviceModelSupImpl: aiAdviceModelSupImpl,
adviceProjectImpl: adviceProjectImpl,
mongo: mongo,
}
}
func (a *AdviceAdvicerBiz) Update(ctx context.Context, data *entitys.AdvicerInitReq) (int32, error) {
birth, err := time.Parse("2006-01-02", data.Birth)
if err != nil {
return 0, err
}
param := &model.AiAdviceAdvicer{
AdvicerID: data.AdvicerID,
ProjectID: data.ProjectID,
Name: data.Name,
Birth: birth,
Gender: data.Gender,
WorkingYears: data.WorkingYears,
}
// 入职时间(可选,留空则不更新)
if data.JoinDate != "" {
joinDate, err := time.Parse("2006-01-02", data.JoinDate)
if err != nil {
return 0, err
}
param.JoinDate = joinDate
}
if param.AdvicerID == 0 {
err = a.advicerImpl.AddWithData(param)
} else {
cond := builder.NewCond()
cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID})
err = a.advicerImpl.UpdateByCond(&cond, param)
}
return param.AdvicerID, 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
}
// Info 根据 advicerId 获取单个销售详情
func (a *AdviceAdvicerBiz) Info(ctx context.Context, data *entitys.AdvicerInfoReq) (map[string]interface{}, error) {
if data.AdvicerID == 0 {
return nil, errors.New("销售ID不能为空")
}
cond := builder.NewCond()
cond = cond.And(builder.Eq{"advicer_id": data.AdvicerID})
return a.advicerImpl.GetOneBySearch(&cond)
}
// UpdateWxDeviceId 更新销售的微信设备 ID
func (a *AdviceAdvicerBiz) UpdateWxDeviceId(ctx context.Context, advicerId int32, wxDeviceId string) error {
updates := map[string]interface{}{"wx_device_id": wxDeviceId}
cond := builder.NewCond()
cond = cond.And(builder.Eq{"advicer_id": advicerId})
return a.advicerImpl.UpdateByCond(&cond, updates)
}
// UpdateWxId 更新销售的登录微信 wxid
func (a *AdviceAdvicerBiz) UpdateWxId(ctx context.Context, advicerId int32, wxId string) error {
updates := map[string]interface{}{"wx_id": wxId}
cond := builder.NewCond()
cond = cond.And(builder.Eq{"advicer_id": advicerId})
return a.advicerImpl.UpdateByCond(&cond, updates)
}
// HostingUpdate 更新销售的托管配置
func (a *AdviceAdvicerBiz) HostingUpdate(ctx context.Context, req *entitys.AdvicerHostingUpdateReq) error {
updates := map[string]interface{}{
"hosting_enabled": req.HostingEnabled,
"hosting_version_id": req.HostingVersionId,
"hosting_skill_id": req.HostingSkillId,
}
// reply_filehelper 为可选字段,仅当显式传入时才更新
if req.ReplyFilehelper != nil {
updates["reply_filehelper"] = *req.ReplyFilehelper
}
cond := builder.NewCond()
cond = cond.And(builder.Eq{"advicer_id": req.AdvicerId})
return a.advicerImpl.UpdateByCond(&cond, updates)
}
// FindByWxDeviceId 通过 wx_device_id 查销售记录
func (a *AdviceAdvicerBiz) FindByWxDeviceId(ctx context.Context, wxDeviceId string) (model.AiAdviceAdvicer, error) {
return a.advicerImpl.FindByWxDeviceId(ctx, wxDeviceId)
}
// GetLatestVersion 获取某销售的最新版本(按 lastUpdateTime 倒序取第一条)
func (a *AdviceAdvicerBiz) GetLatestVersion(ctx context.Context, advicerId int32) (versionId string, version mongo_model.AdvicerVersionMongo, err error) {
list, err := a.VersionList(ctx, &entitys.AdvicerVersionListReq{AdvicerId: advicerId})
if err != nil {
return "", mongo_model.AdvicerVersionMongo{}, err
}
if len(list) == 0 {
return "", mongo_model.AdvicerVersionMongo{}, fmt.Errorf("未找到该销售的版本")
}
// 按 LastUpdateTime 倒序找最新
latest := list[0]
for _, item := range list[1:] {
if item.LastUpdateTime.After(latest.LastUpdateTime) {
latest = item
}
}
return latest.Id.Hex(), mongo_model.AdvicerVersionMongo{
AdvicerId: latest.AdvicerId,
VersionDesc: latest.VersionDesc,
Data: latest.Data,
LastUpdateTime: latest.LastUpdateTime,
}, nil
}
func (a *AdviceAdvicerBiz) VersionAdd(ctx context.Context, param *entitys.AdvicerVersionAddReq) (id interface{}, err error) {
cond := builder.NewCond()
cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID})
_, err = a.advicerImpl.GetOneBySearch(&cond)
if err != nil {
return 0, errors.New("顾问不存在")
}
// 构建文档:系统字段 + 维度数据展平到顶层
doc := bson.M{
"advicerId": param.AdvicerID,
"versionDesc": param.VersionDesc,
"lastUpdateTime": time.Now(),
}
for k, v := range param.Data {
doc[k] = v
}
res, err := a.mongo.Co(a.advicerVersionMongo).InsertOne(ctx, doc)
if err != nil {
return nil, err
}
return res.InsertedID, err
}
func (a *AdviceAdvicerBiz) VersionUpdate(ctx context.Context, param *entitys.AdvicerVersionUpdateReq) (err error) {
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 := bson.M{"_id": objectID}
// 1. 读取现有文档,找出所有旧维度字段
var existing bson.M
err = a.mongo.Co(a.advicerVersionMongo).FindOne(ctx, filter).Decode(&existing)
systemKeys := map[string]bool{"_id": true, "advicerId": true, "versionDesc": true, "lastUpdateTime": true}
unsetFields := bson.M{}
if err == nil && existing != nil {
for k := range existing {
if !systemKeys[k] {
unsetFields[k] = 1
}
}
}
// 2. 构建新数据
setFields := bson.M{
"advicerId": param.AdvicerID,
"versionDesc": param.VersionDesc,
"lastUpdateTime": time.Now(),
}
for k, v := range param.Data {
setFields[k] = v
}
// 3. 先清除旧维度字段,再写入新数据
update := bson.M{}
if len(unsetFields) > 0 {
update["$unset"] = unsetFields
}
update["$set"] = setFields
_, err = a.mongo.Co(a.advicerVersionMongo).UpdateOne(ctx, filter, update)
return err
}
func (a *AdviceAdvicerBiz) VersionList(ctx context.Context, param *entitys.AdvicerVersionListReq) (list []mongo_model.AdvicerVersionItem, 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
}
// 遍历结果:读取原始 BSON,将非系统字段展平到 Data
systemKeys := map[string]bool{"_id": true, "advicerId": true, "versionDesc": true, "lastUpdateTime": true}
for cursor.Next(ctx) {
var raw bson.M
if err := cursor.Decode(&raw); err != nil {
return nil, err
}
item := mongo_model.AdvicerVersionItem{}
if id, ok := raw["_id"].(primitive.ObjectID); ok {
item.Id = id
}
item.AdvicerId = int32(raw["advicerId"].(int32))
if s, ok := raw["versionDesc"].(string); ok {
item.VersionDesc = s
}
if t, ok := raw["lastUpdateTime"].(primitive.DateTime); ok {
item.LastUpdateTime = t.Time()
}
item.Data = make(map[string]interface{})
for k, v := range raw {
if !systemKeys[k] {
item.Data[k] = v
}
}
list = append(list, item)
}
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
}
var raw bson.M
res := a.mongo.Co(a.advicerVersionMongo).FindOne(ctx, filter)
if res.Err() != nil {
return info, res.Err()
}
if err := res.Decode(&raw); err != nil {
return info, err
}
systemKeys := map[string]bool{"_id": true, "advicerId": true, "versionDesc": true, "lastUpdateTime": true}
if v, ok := raw["advicerId"].(int32); ok {
info.AdvicerId = v
}
if v, ok := raw["versionDesc"].(string); ok {
info.VersionDesc = v
}
if t, ok := raw["lastUpdateTime"].(primitive.DateTime); ok {
info.LastUpdateTime = t.Time()
}
info.Data = make(map[string]interface{})
for k, v := range raw {
if !systemKeys[k] {
info.Data[k] = v
}
}
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
}
// VersionStyleGenerate AI 生成销售风格维度
func (a *AdviceAdvicerBiz) VersionStyleGenerate(ctx context.Context, req *entitys.AdvicerVersionStyleGenerateReq) (map[string]interface{}, error) {
// 1. 查询销售信息
cond := builder.NewCond().And(builder.Eq{"advicer_id": req.AdvicerId})
var advicer model.AiAdviceAdvicer
if err := a.advicerImpl.GetOneBySearchToStrut(&cond, &advicer); err != nil || advicer.AdvicerID == 0 {
return nil, fmt.Errorf("销售不存在")
}
// 2. 查询项目信息
condP := builder.NewCond().And(builder.Eq{"project_id": advicer.ProjectID})
var project model.AiAdviceProject
if err := a.adviceProjectImpl.GetOneBySearchToStrut(&condP, &project); err != nil || project.ProjectID == 0 {
return nil, fmt.Errorf("项目不存在")
}
// 3. 确定模型配置
var sup model.AiAdviceModelSup
if req.SupId > 0 {
condS := builder.NewCond().And(builder.Eq{"sup_id": req.SupId})
if err := a.aiAdviceModelSupImpl.GetOneBySearchToStrut(&condS, &sup); err != nil || sup.SupID == 0 {
return nil, fmt.Errorf("指定的模型配置不存在")
}
} else if project.ModelSupID > 0 {
condS := builder.NewCond().And(builder.Eq{"sup_id": project.ModelSupID})
if err := a.aiAdviceModelSupImpl.GetOneBySearchToStrut(&condS, &sup); err != nil || sup.SupID == 0 {
return nil, fmt.Errorf("项目配置的模型配置不存在")
}
} else {
return nil, fmt.Errorf("项目未配置模型,请手动选择模型配置")
}
// 4. 构建提示词
prompt := constants.PromptVersionStyleGenerate
prompt = strings.ReplaceAll(prompt, "{{项目名称}}", project.Name)
desc := project.TemplateDesc
if desc == "" {
desc = project.TemplateAdvicerDesc
}
prompt = strings.ReplaceAll(prompt, "{{项目描述}}", desc)
prompt = strings.ReplaceAll(prompt, "{{销售姓名}}", advicer.Name)
prompt = strings.ReplaceAll(prompt, "{{工作年限}}", fmt.Sprintf("%d", advicer.WorkingYears))
// 用户对该销售的一句话描述
userDesc := ""
if req.Desc != "" {
userDesc = "\n【销售特点描述】\n" + req.Desc + "\n"
}
prompt = strings.ReplaceAll(prompt, "{{用户描述}}", userDesc)
// 5. 调用 LLM
result, err := third_party.NewOpenAi().RequestResponsesJson(ctx, sup.Key, sup.URL, sup.JSONModel, prompt)
if err != nil {
return nil, fmt.Errorf("AI 生成失败: %w", err)
}
// 6. 解析返回的 JSON
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(result), &parsed); err != nil {
return nil, fmt.Errorf("解析 AI 返回结果失败: %w", err)
}
// 7. 提取需要的字段(7 个维度,中文 key)
resp := make(map[string]interface{})
for _, key := range []string{"方言/口音特征", "词汇偏好", "句式模式", "标点与排版习惯", "个性标签", "语气标签", "标志性对话"} {
if v, ok := parsed[key]; ok {
resp[key] = v
}
}
if len(resp) == 0 {
return nil, fmt.Errorf("AI 未返回有效的风格维度数据")
}
return resp, nil
}