ai_scheduler/internal/biz/advice_project.go

458 lines
15 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 (
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
}