458 lines
15 KiB
Go
458 lines
15 KiB
Go
package biz
|
||
|
||
import (
|
||
errorcode "ai_scheduler/internal/data/error"
|
||
"ai_scheduler/internal/data/impl"
|
||
"ai_scheduler/internal/data/model"
|
||
"ai_scheduler/internal/data/mongo_model"
|
||
"ai_scheduler/internal/entitys"
|
||
"ai_scheduler/internal/pkg"
|
||
"ai_scheduler/tmpl/dataTemp"
|
||
"errors"
|
||
"fmt"
|
||
"time"
|
||
|
||
"context"
|
||
|
||
"go.mongodb.org/mongo-driver/bson"
|
||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||
"go.mongodb.org/mongo-driver/mongo"
|
||
"go.mongodb.org/mongo-driver/mongo/options"
|
||
"xorm.io/builder"
|
||
|
||
"ai_scheduler/internal/biz/llm_service/third_party"
|
||
"ai_scheduler/internal/data/constants"
|
||
"encoding/json"
|
||
"strings"
|
||
)
|
||
|
||
type AdviceProjectBiz struct {
|
||
AdvicerProjectMongo *mongo_model.AdvicerProjectMongo
|
||
AdvicerProjectDataMongo *mongo_model.AdvicerProjectDataMongo
|
||
adviceProjectImpl *impl.AdviceProjectImpl
|
||
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl
|
||
industryImpl *impl.AdviceIndustryImpl
|
||
mongo *pkg.Mongo
|
||
}
|
||
|
||
func NewAdviceProjectBiz(
|
||
advicerProjectMongo *mongo_model.AdvicerProjectMongo,
|
||
advicerProjectDataMongo *mongo_model.AdvicerProjectDataMongo,
|
||
adviceProjectImpl *impl.AdviceProjectImpl,
|
||
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl,
|
||
industryImpl *impl.AdviceIndustryImpl,
|
||
mongo *pkg.Mongo,
|
||
) *AdviceProjectBiz {
|
||
return &AdviceProjectBiz{
|
||
AdvicerProjectMongo: advicerProjectMongo,
|
||
AdvicerProjectDataMongo: advicerProjectDataMongo,
|
||
mongo: mongo,
|
||
adviceProjectImpl: adviceProjectImpl,
|
||
aiAdviceModelSupImpl: aiAdviceModelSupImpl,
|
||
industryImpl: industryImpl,
|
||
}
|
||
}
|
||
|
||
// BaseAdd 新建项目基础信息;IndustryId>0 时自动复制行业模板的维度内容
|
||
func (a *AdviceProjectBiz) BaseAdd(ctx context.Context, param *entitys.AdvicerProjectBaseAddReq) (res *entitys.AdvicerProjectBaseAddRes, err error) {
|
||
add := &model.AiAdviceProject{
|
||
Name: param.Name,
|
||
ModelSupID: param.ModelSupId,
|
||
}
|
||
if param.IndustryId > 0 {
|
||
if err = a.fillTemplateFromIndustry(ctx, add, param.IndustryId); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
err = a.adviceProjectImpl.AddWithData(add)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &entitys.AdvicerProjectBaseAddRes{
|
||
ProjectId: add.ProjectID,
|
||
}, err
|
||
}
|
||
|
||
// BaseUpdate 更新项目基础信息与项目级模板字段(仅更新传入非空字段)
|
||
func (a *AdviceProjectBiz) BaseUpdate(ctx context.Context, param *entitys.AdvicerProjectBaseUpdateReq) (err error) {
|
||
if param.ProjectId == 0 {
|
||
return errorcode.ParamErr("projectId is empty")
|
||
}
|
||
updates := make(map[string]interface{})
|
||
if param.Name != "" {
|
||
updates["name"] = param.Name
|
||
}
|
||
if param.ModelSupId != 0 {
|
||
updates["model_sup_id"] = param.ModelSupId
|
||
}
|
||
if param.Desc != "" {
|
||
updates["template_desc"] = param.Desc
|
||
}
|
||
if param.AdvicerDesc != "" {
|
||
updates["template_advicer_desc"] = param.AdvicerDesc
|
||
}
|
||
if param.ClientDimension != "" {
|
||
updates["client_dimension"] = param.ClientDimension
|
||
}
|
||
if param.ProjectDimension != "" {
|
||
updates["project_dimension"] = param.ProjectDimension
|
||
}
|
||
if param.AdvicerDimension != "" {
|
||
updates["advicer_dimension"] = param.AdvicerDimension
|
||
}
|
||
if param.TalkSkillDimension != "" {
|
||
updates["talk_skill_dimension"] = param.TalkSkillDimension
|
||
}
|
||
if param.RuleDimension != "" {
|
||
updates["rule_dimension"] = param.RuleDimension
|
||
}
|
||
if param.WxToken != "" {
|
||
updates["wx_token"] = param.WxToken
|
||
}
|
||
// IndustryId>0 时重新从行业模板复制(覆盖模板字段)
|
||
if param.IndustryId > 0 {
|
||
var industry model.AiAdviceIndustryTemp
|
||
if err = a.industryImpl.GetByKey(ctx, "industry_id", param.IndustryId, &industry); err != nil {
|
||
return err
|
||
}
|
||
if industry.IndustryId == 0 {
|
||
return errorcode.ParamErr("行业模板不存在")
|
||
}
|
||
a.mergeIndustryTemplate(updates, &industry)
|
||
}
|
||
if len(updates) == 0 {
|
||
return nil
|
||
}
|
||
cond := builder.NewCond()
|
||
cond = cond.And(builder.Eq{"project_id": param.ProjectId})
|
||
err = a.adviceProjectImpl.UpdateByCond(&cond, updates)
|
||
return err
|
||
}
|
||
|
||
// TemplateCopy 重新应用行业模板(全量覆盖项目模板字段)
|
||
func (a *AdviceProjectBiz) TemplateCopy(ctx context.Context, param *entitys.AdvicerProjectTemplateCopyReq) (err error) {
|
||
if param.ProjectId == 0 {
|
||
return errorcode.ParamErr("projectId is empty")
|
||
}
|
||
if param.IndustryId == 0 {
|
||
return errorcode.ParamErr("industryId is empty")
|
||
}
|
||
var industry model.AiAdviceIndustryTemp
|
||
if err = a.industryImpl.GetByKey(ctx, "industry_id", param.IndustryId, &industry); err != nil {
|
||
return err
|
||
}
|
||
if industry.IndustryId == 0 {
|
||
return errorcode.ParamErr("行业模板不存在")
|
||
}
|
||
updates := make(map[string]interface{})
|
||
a.mergeIndustryTemplate(updates, &industry)
|
||
cond := builder.NewCond()
|
||
cond = cond.And(builder.Eq{"project_id": param.ProjectId})
|
||
return a.adviceProjectImpl.UpdateByCond(&cond, updates)
|
||
}
|
||
|
||
// fillTemplateFromIndustry 从行业模板复制维度内容到项目实体
|
||
func (a *AdviceProjectBiz) fillTemplateFromIndustry(ctx context.Context, project *model.AiAdviceProject, industryId int32) error {
|
||
var industry model.AiAdviceIndustryTemp
|
||
if err := a.industryImpl.GetByKey(ctx, "industry_id", industryId, &industry); err != nil {
|
||
return err
|
||
}
|
||
if industry.IndustryId == 0 {
|
||
return errorcode.ParamErr("行业模板不存在")
|
||
}
|
||
project.IndustryID = industry.IndustryId
|
||
project.TemplateDesc = industry.Desc
|
||
project.TemplateAdvicerDesc = industry.AdvicerDesc
|
||
project.ClientDimension = industry.ClientDimension
|
||
project.ProjectDimension = industry.ProjectDimension
|
||
project.AdvicerDimension = industry.AdvicerDimension
|
||
project.TalkSkillDimension = industry.TalkSkillDimension
|
||
project.RuleDimension = industry.RuleDimension
|
||
return nil
|
||
}
|
||
|
||
// mergeIndustryTemplate 将行业模板内容合并到更新 map(全量覆盖语义)
|
||
func (a *AdviceProjectBiz) mergeIndustryTemplate(updates map[string]interface{}, industry *model.AiAdviceIndustryTemp) {
|
||
updates["industry_id"] = industry.IndustryId
|
||
updates["template_desc"] = industry.Desc
|
||
updates["template_advicer_desc"] = industry.AdvicerDesc
|
||
updates["client_dimension"] = industry.ClientDimension
|
||
updates["project_dimension"] = industry.ProjectDimension
|
||
updates["advicer_dimension"] = industry.AdvicerDimension
|
||
updates["talk_skill_dimension"] = industry.TalkSkillDimension
|
||
updates["rule_dimension"] = industry.RuleDimension
|
||
}
|
||
|
||
// List 分页查询项目列表
|
||
func (a *AdviceProjectBiz) List(ctx context.Context, param *entitys.AdvicerProjectListReq) (list []model.AiAdviceProject, total int64, err error) {
|
||
list = make([]model.AiAdviceProject, 0)
|
||
cond := builder.NewCond()
|
||
if param.Name != "" {
|
||
cond = cond.And(builder.Like{"name", "%" + param.Name + "%"})
|
||
}
|
||
page, err := a.adviceProjectImpl.GetListToStruct(ctx, &cond, &dataTemp.ReqPageBo{Page: param.Page, Limit: param.PageSize}, &list, "")
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return list, page.Total, nil
|
||
}
|
||
|
||
func (a *AdviceProjectBiz) Add(ctx context.Context, param *entitys.AdvicerProjectAddReq) (id interface{}, err error) {
|
||
|
||
res, 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 res.InsertedID, err
|
||
}
|
||
|
||
func (a *AdviceProjectBiz) Update(ctx context.Context, param *entitys.AdvicerrProjectUpdateReq) (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
|
||
}
|
||
if param.ProjectId != 0 {
|
||
|
||
filter["projectId"] = param.ProjectId
|
||
}
|
||
|
||
update := bson.M{
|
||
"$set": &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(),
|
||
},
|
||
}
|
||
res := a.mongo.Co(a.AdvicerProjectMongo).FindOneAndUpdate(ctx, filter, update)
|
||
return res.Err()
|
||
}
|
||
|
||
func (a *AdviceProjectBiz) Info(ctx context.Context, param *entitys.AdvicerProjectInfoReq) (info *entitys.AdvicerProjectInfoRes, err error) {
|
||
configInfo, err := a.ConfigInfo(ctx, param)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
baseProjectId := configInfo.ProjectId
|
||
// mongo 中暂无配置记录时,回退用请求参数中的 projectId 查询基础信息
|
||
if baseProjectId == 0 {
|
||
baseProjectId = param.ProjectId
|
||
}
|
||
baseInfo, err := a.BaseInfo(baseProjectId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
supInfo, err := a.ModelInfo(baseInfo.ModelSupID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &entitys.AdvicerProjectInfoRes{
|
||
ConfigInfo: configInfo,
|
||
Base: baseInfo,
|
||
ModelInfo: supInfo,
|
||
}, nil
|
||
}
|
||
|
||
func (a *AdviceProjectBiz) BaseInfo(projectId int32) (baseInfo model.AiAdviceProject, err error) {
|
||
if projectId == 0 {
|
||
return
|
||
}
|
||
cond := builder.NewCond()
|
||
cond = cond.And(builder.Eq{"project_id": projectId})
|
||
err = a.adviceProjectImpl.GetOneBySearchToStrut(&cond, &baseInfo)
|
||
if err != nil {
|
||
return baseInfo, err
|
||
}
|
||
return baseInfo, nil
|
||
}
|
||
|
||
func (a *AdviceProjectBiz) ModelInfo(supId int32) (supInfo model.AiAdviceModelSup, err error) {
|
||
if supId == 0 {
|
||
return
|
||
}
|
||
cond := builder.NewCond()
|
||
cond = cond.And(builder.Eq{"sup_id": supId})
|
||
err = a.aiAdviceModelSupImpl.GetOneBySearchToStrut(&cond, &supInfo)
|
||
if err != nil {
|
||
return supInfo, err
|
||
}
|
||
return supInfo, nil
|
||
}
|
||
|
||
func (a *AdviceProjectBiz) ConfigInfo(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 && !errors.Is(res.Err(), mongo.ErrNoDocuments) {
|
||
return info, res.Err()
|
||
}
|
||
// 遍历结果
|
||
|
||
if err := res.Decode(&info); err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
|
||
return info, err
|
||
}
|
||
|
||
return info, nil
|
||
}
|
||
|
||
// ProjectDataLoad 加载项目资料(新表)
|
||
// 文档中除 projectId / lastUpdateTime 外的字段均为栏目数据
|
||
func (a *AdviceProjectBiz) ProjectDataLoad(ctx context.Context, projectId int32) (info mongo_model.AdvicerProjectDataMongo, err error) {
|
||
filter := bson.M{"projectId": projectId}
|
||
res := a.mongo.Co(a.AdvicerProjectDataMongo).FindOne(ctx, filter)
|
||
if res.Err() != nil && !errors.Is(res.Err(), mongo.ErrNoDocuments) {
|
||
return info, res.Err()
|
||
}
|
||
var raw bson.M
|
||
if err := res.Decode(&raw); err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
|
||
return info, err
|
||
}
|
||
if raw == nil {
|
||
return info, nil
|
||
}
|
||
info.ProjectId = projectId
|
||
if t, ok := raw["lastUpdateTime"].(primitive.DateTime); ok {
|
||
info.LastUpdateTime = t.Time()
|
||
}
|
||
// 除已知字段外,其余均为栏目数据(过滤掉可能残留的无效字段)
|
||
info.Data = make(map[string]interface{})
|
||
for k, v := range raw {
|
||
if k == "_id" || k == "projectId" || k == "lastUpdateTime" || k == "sections" || k == "sectionsMeta" {
|
||
continue
|
||
}
|
||
info.Data[k] = v
|
||
}
|
||
return info, nil
|
||
}
|
||
|
||
// ProjectDataSave 保存项目资料(新表,全量替换动态字段)
|
||
// 先读取现有文档清除旧动态字段,再写入新数据,保证保存与加载一致
|
||
func (a *AdviceProjectBiz) ProjectDataSave(ctx context.Context, param *entitys.AdvicerProjectDataSaveReq) error {
|
||
filter := bson.M{"projectId": param.ProjectId}
|
||
// 1. 读取现有文档,找出所有动态字段(非系统字段)
|
||
var existing bson.M
|
||
err := a.mongo.Co(a.AdvicerProjectDataMongo).FindOne(ctx, filter).Decode(&existing)
|
||
systemKeys := map[string]bool{"_id": true, "projectId": 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{
|
||
"projectId": param.ProjectId,
|
||
"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
|
||
opts := options.Update().SetUpsert(true)
|
||
_, err = a.mongo.Co(a.AdvicerProjectDataMongo).UpdateOne(ctx, filter, update, opts)
|
||
return err
|
||
}
|
||
|
||
// ProjectDataGenerate AI 预设项目资料:根据项目名称和描述,调用 LLM 生成 2~4 个栏目
|
||
func (a *AdviceProjectBiz) ProjectDataGenerate(ctx context.Context, req *entitys.AdvicerProjectDataGenerateReq) (map[string]interface{}, error) {
|
||
// 1. 查询项目基础信息
|
||
baseInfo, err := a.BaseInfo(req.ProjectId)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("查询项目信息失败: %w", err)
|
||
}
|
||
if baseInfo.ProjectID == 0 {
|
||
return nil, errorcode.ParamErr("项目不存在")
|
||
}
|
||
|
||
// 2. 确定模型配置(优先用请求指定的 supId,否则用项目配置的模型)
|
||
var sup model.AiAdviceModelSup
|
||
if req.SupId > 0 {
|
||
cond := builder.NewCond().And(builder.Eq{"sup_id": req.SupId})
|
||
if err = a.aiAdviceModelSupImpl.GetOneBySearchToStrut(&cond, &sup); err != nil || sup.SupID == 0 {
|
||
return nil, fmt.Errorf("指定的模型配置不存在")
|
||
}
|
||
} else if baseInfo.ModelSupID > 0 {
|
||
cond := builder.NewCond().And(builder.Eq{"sup_id": baseInfo.ModelSupID})
|
||
if err = a.aiAdviceModelSupImpl.GetOneBySearchToStrut(&cond, &sup); err != nil || sup.SupID == 0 {
|
||
return nil, fmt.Errorf("项目配置的模型配置不存在")
|
||
}
|
||
} else {
|
||
return nil, errorcode.ParamErr("项目未配置模型,请手动选择模型配置")
|
||
}
|
||
|
||
// 3. 构建提示词
|
||
prompt := constants.PromptProjectDataGenerate
|
||
prompt = strings.ReplaceAll(prompt, "{{项目名称}}", baseInfo.Name)
|
||
desc := baseInfo.TemplateDesc
|
||
if desc == "" {
|
||
desc = baseInfo.TemplateAdvicerDesc
|
||
}
|
||
prompt = strings.ReplaceAll(prompt, "{{项目描述}}", desc)
|
||
|
||
// 4. 调用 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)
|
||
}
|
||
|
||
// 5. 解析返回的 JSON,提取 sections
|
||
var parsed struct {
|
||
Sections []struct {
|
||
Name string `json:"name"`
|
||
Type string `json:"type"`
|
||
Content interface{} `json:"content"`
|
||
} `json:"sections"`
|
||
}
|
||
if err := json.Unmarshal([]byte(result), &parsed); err != nil {
|
||
return nil, fmt.Errorf("解析 AI 返回结果失败: %w", err)
|
||
}
|
||
if len(parsed.Sections) == 0 {
|
||
return nil, fmt.Errorf("AI 未返回有效栏目数据")
|
||
}
|
||
|
||
// 6. 转为展平 map(key=栏目名,value=内容)
|
||
data := make(map[string]interface{})
|
||
for _, s := range parsed.Sections {
|
||
if s.Name == "" {
|
||
continue
|
||
}
|
||
data[s.Name] = s.Content
|
||
}
|
||
return data, nil
|
||
}
|