Compare commits

...

11 Commits
master ... v4

28 changed files with 952 additions and 173 deletions

View File

@ -27,6 +27,7 @@ lsxd:
login_url: "https://api.user.1688sup.com/v1/login/phone" login_url: "https://api.user.1688sup.com/v1/login/phone"
phone: "ORlviZN7N06W2+WKLe76xg==" phone: "ORlviZN7N06W2+WKLe76xg=="
password: "V5Uh8C4bamEM6UQZh4TCeQ==" password: "V5Uh8C4bamEM6UQZh4TCeQ=="
code: "456789"
check_token_url: "https://api.user.1688sup.com/v1/user/welcome" check_token_url: "https://api.user.1688sup.com/v1/user/welcome"

View File

@ -26,10 +26,14 @@ coze:
lsxd: lsxd:
# 统一登录 # 统一登录
login_url: "https://api.user.1688sup.com/v1/login/phone" login_url: "http://api.test.user.1688sup.com/v1/login/phone"
phone: "ORlviZN7N06W2+WKLe76xg==" phone: "OFJ8UpqOlI7+w3Qklf36ZA=="
password: "V5Uh8C4bamEM6UQZh4TCeQ==" password: "tEbFegH/DRRh6LutFb7o3g=="
check_token_url: "https://api.user.1688sup.com/v1/user/welcome" code: "123456"
check_token_url: "http://api.test.user.1688sup.com/v1/user/welcome"
zltx:
req_url: "https://gateway.dev.cdlsxd.cn/zltx_api"
sys: sys:
session_len: 6 session_len: 6
@ -41,7 +45,7 @@ redis:
host: 47.97.27.195:6379 host: 47.97.27.195:6379
type: node type: node
pass: lansexiongdi@666 pass: lansexiongdi@666
key: report-api-test key: ai_scheduler-test
pollSize: 5 #连接池大小不配置或配置为0表示不启用连接池 pollSize: 5 #连接池大小不配置或配置为0表示不启用连接池
minIdleConns: 2 #最小空闲连接数 minIdleConns: 2 #最小空闲连接数
maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭 maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭

View File

@ -8,6 +8,7 @@ import (
"ai_scheduler/internal/pkg/util" "ai_scheduler/internal/pkg/util"
"context" "context"
"encoding/json" "encoding/json"
"xorm.io/builder" "xorm.io/builder"
) )
@ -30,7 +31,8 @@ func (s *ChatHistoryBiz) List(ctx context.Context, query *entitys.ChatHistQuery)
con := []impl.CondFunc{ con := []impl.CondFunc{
s.chatHiRepo.WithSessionId(query.SessionID), s.chatHiRepo.WithSessionId(query.SessionID),
s.chatHiRepo.PaginateScope(query.Page, query.PageSize), s.chatHiRepo.PaginateScope(query.Page, query.PageSize),
s.chatHiRepo.OrderByDesc("his_id"), // s.chatHiRepo.OrderByDesc("his_id"),
s.chatHiRepo.OrderByAsc("his_id"),
} }
if query.HisID > 0 { if query.HisID > 0 {
con = append(con, s.chatHiRepo.WithHisId(query.HisID)) con = append(con, s.chatHiRepo.WithHisId(query.HisID))
@ -127,5 +129,5 @@ func (c *ChatHistoryBiz) UpdateContent(ctx context.Context, chat *entitys.Update
func (s *ChatHistoryBiz) Update(ctx context.Context, chat *entitys.UseFulRequest) error { func (s *ChatHistoryBiz) Update(ctx context.Context, chat *entitys.UseFulRequest) error {
cond := builder.NewCond() cond := builder.NewCond()
cond = cond.And(builder.Eq{"his_id": chat.HisId}) cond = cond.And(builder.Eq{"his_id": chat.HisId})
return s.chatHiRepo.UpdateByCond(&cond, &model.AiChatHi{HisID: chat.HisId, Useful: chat.Useful}) return s.chatHiRepo.UpdateByCond(&cond, &model.AiChatHi{Useful: chat.Useful})
} }

View File

@ -29,21 +29,22 @@ import (
// AiRouterBiz 智能路由服务 // AiRouterBiz 智能路由服务
type DingTalkBotBiz struct { type DingTalkBotBiz struct {
do *do.Do do *do.Do
handle *do.Handle handle *do.Handle
botConfigImpl *impl.BotConfigImpl botConfigImpl *impl.BotConfigImpl
replier *chatbot.ChatbotReplier replier *chatbot.ChatbotReplier
log log.Logger log log.Logger
dingTalkUser *dingtalk.User dingTalkUser *dingtalk.User
botGroupImpl *impl.BotGroupImpl botGroupImpl *impl.BotGroupImpl
botGroupConfigImpl *impl.BotGroupConfigImpl botGroupConfigImpl *impl.BotGroupConfigImpl
botGroupQywxImpl *impl.BotGroupQywxImpl botGroupQywxImpl *impl.BotGroupQywxImpl
toolManager *tools.Manager toolManager *tools.Manager
chatHis *impl.BotChatHisImpl chatHis *impl.BotChatHisImpl
conf *config.Config conf *config.Config
cardSend *dingtalk.SendCardClient cardSend *dingtalk.SendCardClient
qywxGroupHandle *qywx.Group qywxGroupHandle *qywx.Group
groupConfigBiz *GroupConfigBiz groupConfigBiz *GroupConfigBiz
reportDailyCacheImpl *impl.ReportDailyCacheImpl
} }
// NewDingTalkBotBiz // NewDingTalkBotBiz
@ -54,23 +55,25 @@ func NewDingTalkBotBiz(
botGroupImpl *impl.BotGroupImpl, botGroupImpl *impl.BotGroupImpl,
dingTalkUser *dingtalk.User, dingTalkUser *dingtalk.User,
chatHis *impl.BotChatHisImpl, chatHis *impl.BotChatHisImpl,
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
toolManager *tools.Manager, toolManager *tools.Manager,
conf *config.Config, conf *config.Config,
cardSend *dingtalk.SendCardClient, cardSend *dingtalk.SendCardClient,
groupConfigBiz *GroupConfigBiz, groupConfigBiz *GroupConfigBiz,
) *DingTalkBotBiz { ) *DingTalkBotBiz {
return &DingTalkBotBiz{ return &DingTalkBotBiz{
do: do, do: do,
handle: handle, handle: handle,
botConfigImpl: botConfigImpl, botConfigImpl: botConfigImpl,
replier: chatbot.NewChatbotReplier(), replier: chatbot.NewChatbotReplier(),
dingTalkUser: dingTalkUser, dingTalkUser: dingTalkUser,
groupConfigBiz: groupConfigBiz, groupConfigBiz: groupConfigBiz,
botGroupImpl: botGroupImpl, botGroupImpl: botGroupImpl,
toolManager: toolManager, toolManager: toolManager,
chatHis: chatHis, chatHis: chatHis,
conf: conf, conf: conf,
cardSend: cardSend, cardSend: cardSend,
reportDailyCacheImpl: reportDailyCacheImpl,
} }
} }
@ -197,6 +200,49 @@ func (d *DingTalkBotBiz) Macro(ctx context.Context, requireData *entitys.Require
return return
} }
if strings.Contains(content, "[负利润分析]获取") {
var (
data model.AiReportDailyCache
value map[int32]*bbxt.ResellerLossSumProductRelation
)
cond := builder.NewCond()
cond = cond.And(builder.Eq{"`index`": bbxt.IndexLossSumDetail})
cond = cond.And(builder.Eq{"`key`": time.Now().Format(time.DateOnly)})
err = d.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &data)
if err != nil {
entitys.ResText(requireData.Ch, "", "获取失败")
return
}
err = json.Unmarshal([]byte(data.Value), &value)
if err != nil {
entitys.ResText(requireData.Ch, "", "获取失败,格式解析错误")
return
}
return
}
if strings.Contains(content, "[负利润分析]更新") {
// 提取冒号后的内容
if len(groupConfig.ProductName) == 0 {
entitys.ResText(requireData.Ch, "", "暂未设置")
} else {
entitys.ResText(requireData.Ch, "", groupConfig.ProductName)
isFinish = true
}
return
}
if strings.Contains(content, "[负利润分析]同步") {
// 提取冒号后的内容
if len(groupConfig.ProductName) == 0 {
entitys.ResText(requireData.Ch, "", "暂未设置")
} else {
entitys.ResText(requireData.Ch, "", groupConfig.ProductName)
isFinish = true
}
return
}
return return
} }

View File

@ -2,6 +2,7 @@ package do
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
errors "ai_scheduler/internal/data/error" errors "ai_scheduler/internal/data/error"
"ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
@ -32,16 +33,18 @@ type Do struct {
} }
func NewDo( func NewDo(
sessionImpl *impl.SessionImpl,
sysImpl *impl.SysImpl, sysImpl *impl.SysImpl,
taskImpl *impl.TaskImpl, taskImpl *impl.TaskImpl,
hisImpl *impl.ChatHisImpl, hisImpl *impl.ChatHisImpl,
conf *config.Config, conf *config.Config,
) *Do { ) *Do {
return &Do{ return &Do{
conf: conf, conf: conf,
sysImpl: sysImpl, sessionImpl: sessionImpl,
hisImpl: hisImpl, sysImpl: sysImpl,
taskImpl: taskImpl, hisImpl: hisImpl,
taskImpl: taskImpl,
} }
} }
@ -265,16 +268,27 @@ func (d *Do) startMessageHandler(
if len(chat) > 0 { if len(chat) > 0 {
// 合并所有回答-转json字符串 // 合并所有回答-转json字符串
ans, _ := json.Marshal(chat) ans, _ := json.Marshal(chat)
// 通过 chat 获取 task_id
taskId := d.getTaskIdByChat(chat)
AiRes := &model.AiChatHi{ AiRes := &model.AiChatHi{
SessionID: requireData.Session, SessionID: requireData.Session,
Ques: requireData.Req.Text, Ques: requireData.Req.Text,
Ans: string(ans), Ans: string(ans),
Files: requireData.Req.Img, Files: requireData.Req.Img,
TaskID: requireData.Task.TaskID, TaskID: taskId,
} }
d.hisImpl.AddWithData(AiRes) d.hisImpl.AddWithData(AiRes)
hisLog.HisId = AiRes.HisID hisLog.HisId = AiRes.HisID
// 查询当前session
cond := builder.NewCond().And(builder.Eq{"session_id": requireData.Session})
sessionMap, _ := d.sessionImpl.GetOneBySearch(&cond)
requireData.SessionInfo.Title = sessionMap["title"].(string)
// 当前 session title为空 ,更新为用户输入
if requireData.SessionInfo.Title == "" {
d.sessionImpl.UpdateByCond(&cond, &model.AiSession{Title: requireData.Req.Text})
}
} }
_ = entitys.MsgSend(client, entitys.Response{ _ = entitys.MsgSend(client, entitys.Response{
@ -399,3 +413,28 @@ func (d *Do) LoadUserPermission(client *gateway.Client, requireData *entitys.Req
return respBody.Codes, nil return respBody.Codes, nil
} }
// getTaskIdByChat 从 chat 中获取 task_id
func (d *Do) getTaskIdByChat(chat []entitys.Response) (taskId int32) {
if len(chat) == 0 {
return
}
// 解析 taskIndex
taskIndex := chat[0].Index
if _, ok := constants.IrregularTaskToolIndexMap[taskIndex]; ok {
taskIndex = constants.IrregularTaskToolIndexMap[taskIndex]
}
// 通过 taskIndex 获取 taskId
cond := builder.NewCond().And(builder.Eq{"`index`": taskIndex})
taskMap, _ := d.taskImpl.GetOneBySearch(&cond)
if taskMap == nil || taskMap["task_id"] == nil {
return
}
taskId = taskMap["task_id"].(int32)
return
}

View File

@ -9,11 +9,15 @@ import (
"ai_scheduler/internal/domain/workflow/recharge" "ai_scheduler/internal/domain/workflow/recharge"
"ai_scheduler/internal/domain/workflow/runtime" "ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/l_request" "ai_scheduler/internal/pkg/l_request"
"ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_oss" "ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/internal/tools" "ai_scheduler/internal/tools"
"ai_scheduler/internal/tools/bbxt" "ai_scheduler/internal/tools/bbxt"
"ai_scheduler/utils"
"context" "context"
"database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -30,12 +34,14 @@ import (
// AiRouterBiz 智能路由服务 // AiRouterBiz 智能路由服务
type GroupConfigBiz struct { type GroupConfigBiz struct {
botGroupConfigImpl *impl.BotGroupConfigImpl botGroupConfigImpl *impl.BotGroupConfigImpl
ossClient *utils_oss.Client reportDailyCacheImpl *impl.ReportDailyCacheImpl
workflowManager *runtime.Registry ossClient *utils_oss.Client
botTools []model.AiBotTool workflowManager *runtime.Registry
toolManager *tools.Manager botTools []model.AiBotTool
conf *config.Config toolManager *tools.Manager
conf *config.Config
rdb *utils.Rdb
} }
// NewDingTalkBotBiz // NewDingTalkBotBiz
@ -45,13 +51,17 @@ func NewGroupConfigBiz(
botGroupConfigImpl *impl.BotGroupConfigImpl, botGroupConfigImpl *impl.BotGroupConfigImpl,
workflowManager *runtime.Registry, workflowManager *runtime.Registry,
conf *config.Config, conf *config.Config,
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
rdb *utils.Rdb,
) *GroupConfigBiz { ) *GroupConfigBiz {
return &GroupConfigBiz{ return &GroupConfigBiz{
botTools: tools.BootTools, botTools: tools.BootTools,
ossClient: ossClient, ossClient: ossClient,
botGroupConfigImpl: botGroupConfigImpl, botGroupConfigImpl: botGroupConfigImpl,
workflowManager: workflowManager, workflowManager: workflowManager,
conf: conf, conf: conf,
reportDailyCacheImpl: reportDailyCacheImpl,
rdb: rdb,
} }
} }
@ -72,12 +82,12 @@ func (g *GroupConfigBiz) GetReportLists(ctx context.Context, groupConfig *model.
product = strings.Split(groupConfig.ProductName, ",") product = strings.Split(groupConfig.ProductName, ",")
} }
reportList, err := bbxt.NewBbxtTools(g.conf) reportList, err := bbxt.NewBbxtTools(g.conf, lsxd.NewLogin(g.conf, g.rdb))
if err != nil { if err != nil {
return return
} }
reports, err = reportList.DailyReport(time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, g.ossClient) reports, err = reportList.DailyReport(ctx, time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, g.ossClient, g.GetReportCache)
if err != nil { if err != nil {
return return
} }
@ -145,7 +155,8 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
} }
} }
} }
rep, err := bbxt.NewBbxtTools(g.conf)
rep, err := bbxt.NewBbxtTools(g.conf, lsxd.NewLogin(g.conf, g.rdb))
uploader := bbxt.NewUploader(g.ossClient, g.conf) uploader := bbxt.NewUploader(g.ossClient, g.conf)
if err != nil { if err != nil {
return err return err
@ -153,7 +164,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
var reports []*bbxt.ReportRes var reports []*bbxt.ReportRes
switch rec.Match.Index { switch rec.Match.Index {
case "report_loss_analysis": case "report_loss_analysis":
repo, _err := rep.StatisOursProductLossSum(t) repo, _err := rep.StatisOursProductLossSum(ctx, t, g.GetReportCache)
if _err != nil { if _err != nil {
return _err return _err
} }
@ -174,7 +185,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
reports = append(reports, repo) reports = append(reports, repo)
case "report_daily": case "report_daily":
product := strings.Split(groupConfig.ProductName, ",") product := strings.Split(groupConfig.ProductName, ",")
repo, _err := rep.DailyReport(t, bbxt.DownWardValue, product, bbxt.SumFilter, nil) repo, _err := rep.DailyReport(ctx, t, bbxt.DownWardValue, product, bbxt.SumFilter, nil, g.GetReportCache)
if _err != nil { if _err != nil {
return _err return _err
} }
@ -404,3 +415,47 @@ func (g *GroupConfigBiz) otherTask(ctx context.Context, rec *entitys.Recognize)
entitys.ResText(rec.Ch, "", rec.Match.Reasoning) entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
return return
} }
func (g *GroupConfigBiz) GetReportCache(ctx context.Context, day time.Time, totalDetail []*bbxt.ResellerLoss, bbxtObj *bbxt.BbxtTools) error {
var ResellerProductRelation map[int32]*bbxt.ResellerLossSumProductRelation
dayDate := day.Format(time.DateOnly)
cond := builder.NewCond()
cond = cond.And(builder.Eq{"index": bbxt.IndexLossSumDetail})
cond = cond.And(builder.Eq{"key": dayDate})
var cache model.AiReportDailyCache
err := g.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache)
if err != nil {
if errors.Is(sql.ErrNoRows, err) {
ResellerProductRelation, err = bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail)
if err != nil {
return err
}
cache = model.AiReportDailyCache{
Key: dayDate,
Index: bbxt.IndexLossSumDetail,
Value: pkg.JsonStringIgonErr(ResellerProductRelation),
}
_, err = g.reportDailyCacheImpl.Add(&cache)
}
} else {
err = json.Unmarshal([]byte(cache.Value), &ResellerProductRelation)
}
if err != nil {
return err
}
for _, v := range totalDetail {
if _, ex := ResellerProductRelation[v.ResellerId]; !ex {
continue
}
v.Manager = ResellerProductRelation[v.ResellerId].AfterSaleName
for _, vv := range v.ProductLoss {
if _, ex := ResellerProductRelation[v.ResellerId].Products[vv.ProductId]; !ex {
continue
}
vv.LossReason = ResellerProductRelation[v.ResellerId].Products[vv.ProductId].LossReason
}
}
return nil
}

View File

@ -35,41 +35,31 @@ func NewSessionBiz(conf *config.Config, sessionImpl *impl.SessionImpl, sysImpl *
func (s *SessionBiz) SessionInit(ctx context.Context, req *entitys.SessionInitRequest) (result *entitys.SessionInitResponse, err error) { func (s *SessionBiz) SessionInit(ctx context.Context, req *entitys.SessionInitRequest) (result *entitys.SessionInitResponse, err error) {
// 获取系统配置 // 获取系统配置
sysConfig, has, err := s.sysRepo.FindOne(s.sysRepo.WithSysId(req.SysId)) sysConfig, err := s.GetSysConfig(req.SysId)
if err != nil { if err != nil {
return return
} else if !has {
err = errorcode.SysNotFound
return
} }
result = &entitys.SessionInitResponse{ result = &entitys.SessionInitResponse{
Chat: make([]entitys.ChatHistory, 0), Chat: make([]entitys.ChatHistory, 0),
} }
// 获取 当天的session // 获取 当天的最新session
t := time.Now().Truncate(24 * time.Hour) t := time.Now().Truncate(24 * time.Hour)
session, has, err := s.sessionRepo.FindOne( session, has, err := s.sessionRepo.Take(
s.sessionRepo.WithUserId(req.UserId), // 条件用户ID s.sessionRepo.WithUserId(req.UserId), // 条件用户ID
s.sessionRepo.WithStartTime(t), // 条件:会话开始时间 s.sessionRepo.WithStartTime(t), // 条件:会话开始时间
s.sysRepo.WithSysId(sysConfig.SysID), // 条件系统ID s.sysRepo.WithSysId(sysConfig.SysID),
s.sessionRepo.OrderByDesc("create_at"), // 条件系统ID
) )
if err != nil { if err != nil {
return return
} else if !has { }
if !has {
// 不存在,创建一个 // 不存在,创建一个
session = model.AiSession{ session, err = s.CreateNew(sysConfig.SysID, req)
SysID: sysConfig.SysID,
SessionID: utils.UUID(),
UserID: req.UserId,
UserName: req.UserName,
DingUserId: req.DingUserId,
}
err = s.sessionRepo.Create(&session)
if err != nil { if err != nil {
return return nil, err
} }
chat := entitys.ChatHistory{ chat := entitys.ChatHistory{
SessionID: session.SessionID, SessionID: session.SessionID,
Role: constants.RoleSystem, Role: constants.RoleSystem,
@ -122,6 +112,32 @@ func (s *SessionBiz) SessionInit(ctx context.Context, req *entitys.SessionInitRe
return return
} }
func (s *SessionBiz) GetSysConfig(sysId string) (model.AiSy, error) {
sysConfig, has, err := s.sysRepo.FindOne(s.sysRepo.WithSysId(sysId))
if err != nil {
return sysConfig, err
} else if !has {
err = errorcode.SysNotFound
return sysConfig, err
}
return sysConfig, nil
}
func (s *SessionBiz) CreateNew(sysID int32, req *entitys.SessionInitRequest) (model.AiSession, error) {
session := model.AiSession{
SysID: sysID,
SessionID: utils.UUID(),
UserID: req.UserId,
UserName: req.UserName,
DingUserId: req.DingUserId,
}
err := s.sessionRepo.Create(&session)
if err != nil {
return session, err
}
return session, nil
}
// SessionList 会话列表 // SessionList 会话列表
func (s *SessionBiz) SessionList(ctx context.Context, req *entitys.SessionListRequest) (list []model.AiSession, err error) { func (s *SessionBiz) SessionList(ctx context.Context, req *entitys.SessionListRequest) (list []model.AiSession, err error) {

View File

@ -27,6 +27,11 @@ type Config struct {
LLM LLM `mapstructure:"llm"` LLM LLM `mapstructure:"llm"`
Dingtalk DingtalkConfig `mapstructure:"dingtalk"` Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
Qywx QywxConfig `mapstructure:"qywx"` Qywx QywxConfig `mapstructure:"qywx"`
ZLTX ZLTX `mapstructure:"zltx"`
}
type ZLTX struct {
ReqUrl string `mapstructure:"req_url"`
} }
type SysPrompt struct { type SysPrompt struct {
@ -136,6 +141,7 @@ type LSXDConfig struct {
LoginURL string `mapstructure:"login_url"` LoginURL string `mapstructure:"login_url"`
Phone string `mapstructure:"phone"` Phone string `mapstructure:"phone"`
Password string `mapstructure:"password"` Password string `mapstructure:"password"`
Code string `mapstructure:"code"`
CheckTokenURL string `mapstructure:"check_token_url"` CheckTokenURL string `mapstructure:"check_token_url"`
} }

View File

@ -9,3 +9,16 @@ const (
Enable = 1 Enable = 1
Disable = 2 Disable = 2
) )
// IrregularTaskToolIndexMap 不规则任务工具索引映射
var IrregularTaskToolIndexMap = map[string]string{
"zltxProduct": "product_diagnosis",
"zltxOrderDetail": "order_diagnosis",
"knowledgeBase": "knowledge_qa",
"zltxOrderStatistics": "account_statistics",
"normalChat": "chat",
"zltxOrderAfterSaleSupplier": "after_sale_supplier",
"zltxOrderAfterSaleReseller": "after_sale_reseller",
"zltxOrderAfterSaleResellerBatch": "after_sale_reseller_batch",
"zltxLossRiskSearch": "loss_order_direct",
}

View File

@ -43,19 +43,21 @@ type BaseRepository[P PO] interface {
FindAll(conditions ...CondFunc) ([]P, error) // 查询所有 FindAll(conditions ...CondFunc) ([]P, error) // 查询所有
Paginate(page, pageSize int, conditions ...CondFunc) (*PaginationResult[P], error) // 分页查询 Paginate(page, pageSize int, conditions ...CondFunc) (*PaginationResult[P], error) // 分页查询
FindOne(conditions ...CondFunc) (P, bool, error) // 查询单条记录,若未找到则返回 has=false, err=nil FindOne(conditions ...CondFunc) (P, bool, error) // 查询单条记录,若未找到则返回 has=false, err=nil
Create(m *P) error // 创建 Take(conditions ...CondFunc) (P, bool, error)
BatchCreate(m *[]P) (err error) // 批量创建 Create(m *P) error // 创建
Update(m *P, conditions ...CondFunc) (err error) // 更新 BatchCreate(m *[]P) (err error) // 批量创建
Delete(conditions ...CondFunc) (err error) // 删除 Update(m *P, conditions ...CondFunc) (err error) // 更新
Count(conditions ...CondFunc) (count int64, err error) // 查询条数 Delete(conditions ...CondFunc) (err error) // 删除
PaginateScope(page, pageSize int) CondFunc // 分页 Count(conditions ...CondFunc) (count int64, err error) // 查询条数
OrderByDesc(field string) CondFunc // 倒序排序 PaginateScope(page, pageSize int) CondFunc // 分页
WithId(id interface{}) CondFunc // 查询id OrderByDesc(field string) CondFunc // 倒序排序
WithStatus(status int) CondFunc // 查询status OrderByAsc(field string) CondFunc // 正序排序
GetDb() *gorm.DB // 获取数据库连接 WithId(id interface{}) CondFunc // 查询id
WithLimit(limit int) CondFunc // 限制返回条数 WithStatus(status int) CondFunc // 查询status
In(field string, values interface{}) CondFunc // 查询字段是否在列表中 GetDb() *gorm.DB // 获取数据库连接
Select(fields ...string) CondFunc // 选择字段 WithLimit(limit int) CondFunc // 限制返回条数
In(field string, values interface{}) CondFunc // 查询字段是否在列表中
Select(fields ...string) CondFunc // 选择字段
} }
// PaginationResult 分页查询结果 // PaginationResult 分页查询结果
@ -127,6 +129,26 @@ func (this *BaseModel[P]) FindOne(conditions ...CondFunc) (P, bool, error) {
return result, true, err return result, true, err
} }
func (this *BaseModel[P]) Take(conditions ...CondFunc) (P, bool, error) {
var (
result P
)
err := this.Db.Model(new(P)).
Scopes(conditions...).
Take(&result).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return result, false, nil // 未找到记录
}
if err != nil {
return result, false, fmt.Errorf("查询单条记录失败: %w", err)
}
return result, true, err
}
// 创建 // 创建
func (this *BaseModel[P]) Create(m *P) error { func (this *BaseModel[P]) Create(m *P) error {
err := this.Db.Create(m).Error err := this.Db.Create(m).Error
@ -193,6 +215,13 @@ func (this *BaseModel[P]) OrderByDesc(field string) CondFunc {
} }
} }
// 正序排序条件生成器
func (this *BaseModel[P]) OrderByAsc(field string) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Order(fmt.Sprintf("%s ASC", field))
}
}
// ID查询条件生成器 // ID查询条件生成器
func (this *BaseModel[P]) WithId(id interface{}) CondFunc { func (this *BaseModel[P]) WithId(id interface{}) CondFunc {
return func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB {

View File

@ -17,4 +17,5 @@ var ProviderImpl = wire.NewSet(
NewBotGroupImpl, NewBotGroupImpl,
NewBotGroupConfigImpl, NewBotGroupConfigImpl,
NewBotGroupQywxImpl, NewBotGroupQywxImpl,
NewReportDailyCacheImpl,
) )

View File

@ -0,0 +1,17 @@
package impl
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils"
)
type ReportDailyCacheImpl struct {
dataTemp.DataTemp
}
func NewReportDailyCacheImpl(db *utils.Db) *ReportDailyCacheImpl {
return &ReportDailyCacheImpl{
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiReportDailyCache)),
}
}

View File

@ -0,0 +1,20 @@
// 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
const TableNameAiReportDailyCache = "ai_report_daily_cache"
// AiReportDailyCache mapped from table <ai_report_daily_cache>
type AiReportDailyCache struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
Key string `gorm:"column:key;not null;default:1;comment:索引方式,可以是任意类型" json:"key"` // 索引方式,可以是任意类型
Value string `gorm:"column:value;comment:类型下所需路由以及参数" json:"value"` // 类型下所需路由以及参数
Index string `gorm:"column:index;not null;comment:类型" json:"index"` // 类型
}
// TableName AiReportDailyCache's table name
func (*AiReportDailyCache) TableName() string {
return TableNameAiReportDailyCache
}

View File

@ -33,9 +33,10 @@ type ChatHisQueryResponse struct {
Files string `gorm:"column:files;not null" json:"files"` Files string `gorm:"column:files;not null" json:"files"`
Useful int32 `gorm:"column:useful;not null;comment:0不评价,1有用其他为无用" json:"useful"` // 0不评价,1有用其他为无用 Useful int32 `gorm:"column:useful;not null;comment:0不评价,1有用其他为无用" json:"useful"` // 0不评价,1有用其他为无用
CreateAt string `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` CreateAt string `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
TaskID int32 `gorm:"column:task_id;not null" json:"task_id"` // 任务ID TaskID int32 `gorm:"column:task_id;not null" json:"task_id"` // 任务ID
TaskName string `gorm:"column:task_name;not null" json:"task_name"` // 任务名称 TaskName string `gorm:"column:task_name;not null" json:"task_name"` // 任务名称
Contents []string `gorm:"column:contents" json:"contents"` // 前端回传数据 Contents []string `gorm:"column:contents" json:"contents"` // 前端回传数据
TaskIndex string `gorm:"column:task_index;not null" json:"task_index"` // 任务索引
} }
func (c *ChatHisQueryResponse) FromModel(chat model.AiChatHi, task model.AiTask) { func (c *ChatHisQueryResponse) FromModel(chat model.AiChatHi, task model.AiTask) {
@ -49,6 +50,7 @@ func (c *ChatHisQueryResponse) FromModel(chat model.AiChatHi, task model.AiTask)
c.TaskID = chat.TaskID c.TaskID = chat.TaskID
c.TaskName = task.Name c.TaskName = task.Name
c.Contents = make([]string, 0) c.Contents = make([]string, 0)
c.TaskIndex = c.parseTaskIndex(task.Index)
// 解析Content // 解析Content
if "" != chat.Content { if "" != chat.Content {
@ -60,6 +62,20 @@ func (c *ChatHisQueryResponse) FromModel(chat model.AiChatHi, task model.AiTask)
} }
} }
func (c *ChatHisQueryResponse) parseTaskIndex(taskIndex string) string {
// 反转 IrregularTaskToolIndexMap
reverseMap := make(map[string]string)
for k, v := range constants.IrregularTaskToolIndexMap {
reverseMap[v] = k
}
if _, ok := reverseMap[taskIndex]; ok {
return reverseMap[taskIndex]
}
return taskIndex
}
type UpdateContentRequest struct { type UpdateContentRequest struct {
HisID int64 `json:"his_id" validate:"required"` HisID int64 `json:"his_id" validate:"required"`
Content string `json:"content" validate:"required"` Content string `json:"content" validate:"required"`

View File

@ -95,30 +95,18 @@ func (l *Login) checkTokenValid(ctx context.Context, token string) bool {
// 调用登录接口获取token // 调用登录接口获取token
func (l *Login) login(ctx context.Context) (string, error) { func (l *Login) login(ctx context.Context) (string, error) {
// 1.获取配置
loginURL := l.config.LSXD.LoginURL
phone := l.config.LSXD.Phone
password := l.config.LSXD.Password
// 2.调用登录接口获取token
if loginURL == "" {
return "", errors.New("login url is empty")
}
if phone == "" || password == "" {
return "", errors.New("phone or password is empty")
}
reqBody := map[string]any{ reqBody := map[string]any{
"phone": phone, "phone": l.config.LSXD.Phone,
"password": password, "password": l.config.LSXD.Password,
"code": "456789", "code": l.config.LSXD.Code,
} }
bodyBytes, err := json.Marshal(reqBody) bodyBytes, err := json.Marshal(reqBody)
if err != nil { if err != nil {
return "", err return "", err
} }
status, respBody, err := l.doRequestWithBody(ctx, http.MethodPost, loginURL, "", "application/json", bodyBytes) status, respBody, err := l.doRequestWithBody(ctx, http.MethodPost, l.config.LSXD.LoginURL, "", "application/json", bodyBytes)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -158,7 +146,7 @@ func (l *Login) login(ctx context.Context) (string, error) {
} }
func (l *Login) getCachedToken(ctx context.Context) (string, error) { func (l *Login) getCachedToken(ctx context.Context) (string, error) {
token, err := l.redisCli.Get(ctx, constants.CACHE_KEY_LSXD_TOKEN).Result() token, err := l.redisCli.Get(ctx, l.getCacheKey()).Result()
if err == nil { if err == nil {
return token, nil return token, nil
} }
@ -168,11 +156,16 @@ func (l *Login) getCachedToken(ctx context.Context) (string, error) {
return "", err return "", err
} }
func (l *Login) getCacheKey() string {
return l.config.Redis.Key + constants.CACHE_KEY_LSXD_TOKEN + l.config.LSXD.Phone // 1.获取配置
}
func (l *Login) cacheToken(ctx context.Context, token string) error { func (l *Login) cacheToken(ctx context.Context, token string) error {
if token == "" { if token == "" {
return errors.New("token is empty") return errors.New("token is empty")
} }
return l.redisCli.Set(ctx, constants.CACHE_KEY_LSXD_TOKEN, token, constants.EXPIRE_LSXD_TOKEN).Err() return l.redisCli.Set(ctx, l.getCacheKey(), token, constants.EXPIRE_LSXD_TOKEN).Err()
} }
func (l *Login) doRequest(ctx context.Context, method string, url string, authorization string, body []byte) (int, error) { func (l *Login) doRequest(ctx context.Context, method string, url string, authorization string, body []byte) (int, error) {

View File

@ -17,6 +17,9 @@ func SetTaskRecExt(requireData *entitys.RequireData, rec *entitys.Recognize) {
} }
func GetTaskRecExt(rec *entitys.Recognize) (ext entitys.TaskExt, err error) { func GetTaskRecExt(rec *entitys.Recognize) (ext entitys.TaskExt, err error) {
if rec == nil {
return
}
err = json.Unmarshal(rec.Ext, &ext) err = json.Unmarshal(rec.Ext, &ext)
return ext, err return ext, err
} }

View File

@ -1,6 +1,10 @@
package util package util
import "encoding/json" import (
"encoding/json"
"reflect"
"strings"
)
// StructToMap 将结构体转换为 map[string]any // StructToMap 将结构体转换为 map[string]any
func StructToMap(v any) (map[string]any, error) { func StructToMap(v any) (map[string]any, error) {
@ -12,3 +16,28 @@ func StructToMap(v any) (map[string]any, error) {
err = json.Unmarshal(b, &m) err = json.Unmarshal(b, &m)
return m, err return m, err
} }
func StructToMapWithReflect(obj interface{}) map[string]interface{} {
val := reflect.ValueOf(obj)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil
}
data := make(map[string]interface{})
for i := 0; i < val.NumField(); i++ {
valueField := val.Field(i)
typeField := val.Type().Field(i)
jsonTag := typeField.Tag.Get("json")
if idx := strings.Index(jsonTag, ","); idx != -1 {
jsonTag = jsonTag[:idx]
}
if !typeField.IsExported() {
continue
}
data[jsonTag] = valueField.Interface()
}
return data
}

View File

@ -56,7 +56,7 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi
c.Response().SetBody([]byte("1")) c.Response().SetBody([]byte("1"))
return nil return nil
}) })
r.Post("/session/new", sessionService.NewSession)
r.Post("/session/init", sessionService.SessionInit) // 会话初始化,不存在则创建,存在则返回会话ID和默认条数会话历史 r.Post("/session/init", sessionService.SessionInit) // 会话初始化,不存在则创建,存在则返回会话ID和默认条数会话历史
r.Post("/session/list", sessionService.SessionList) r.Post("/session/list", sessionService.SessionList)

View File

@ -63,7 +63,7 @@ func run() {
botGroupImpl := impl.NewBotGroupImpl(db) botGroupImpl := impl.NewBotGroupImpl(db)
botUserImpl := impl.NewBotUserImpl(db) botUserImpl := impl.NewBotUserImpl(db)
// 初始化Do业务对象 // 初始化Do业务对象
doDo := do.NewDo(sysImpl, taskImpl, chatHisImpl, configConfig) doDo := do.NewDo(sessionImpl, sysImpl, taskImpl, chatHisImpl, configConfig)
// 初始化Ollama客户端 // 初始化Ollama客户端
client, _, _ := utils_ollama.NewClient(configConfig) client, _, _ := utils_ollama.NewClient(configConfig)
// 初始化vLLM客户端 // 初始化vLLM客户端

View File

@ -35,6 +35,23 @@ func (s *SessionService) SessionInit(c *fiber.Ctx) error {
return c.JSON(result) return c.JSON(result)
} }
// NewSession 新会话
func (s *SessionService) NewSession(c *fiber.Ctx) error {
req := &entitys.SessionInitRequest{}
if err := c.BodyParser(req); err != nil {
return err
}
sysConfig, err := s.sessionBiz.GetSysConfig(req.SysId)
if err != nil {
return err
}
session, err := s.sessionBiz.CreateNew(sysConfig.SysID, req)
return c.JSON(session)
}
// SessionList 获取会话列表 // SessionList 获取会话列表
func (s *SessionService) SessionList(c *fiber.Ctx) error { func (s *SessionService) SessionList(c *fiber.Ctx) error {
req := &entitys.SessionListRequest{} req := &entitys.SessionListRequest{}

View File

@ -3,6 +3,8 @@ package bbxt
import ( import (
"ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/l_request" "ai_scheduler/internal/pkg/l_request"
"ai_scheduler/internal/pkg/util"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@ -29,12 +31,23 @@ type StatisOursProductLossSumResponse struct {
} }
const Base = "https://reportapi.1688sup.com/api" const Base = "https://reportapi.1688sup.com/api"
const AuthUrl = "http://test.analysis.com/api"
// StatisOursProductLossSumApi 负利润分析 // StatisOursProductLossSumApi 负利润分析
func StatisOursProductLossSumApi(param *StatisOursProductLossSumReq) (*StatisOursProductLossSumRes, error) { func StatisOursProductLossSumApi(param *StatisOursProductLossSumReq) (*StatisOursProductLossSumRes, error) {
url := "/dataanalytics/statisOursProductLossSum" url := "/dataanalytics/statisOursProductLossSum"
var res StatisOursProductLossSumRes var res StatisOursProductLossSumRes
if err := request(url, param, &res); err != nil { if err := request(url, param, &res, ""); err != nil {
return nil, err
}
return &res, nil
}
// StatisOursProductLossSumApi 负利润分析
func StatisOursProductLossSumApiWithAuth(param *StatisOursProductLossSumReq, token string) (*StatisOursProductLossSumRes, error) {
url := "/dataanalytics/statisOursProductLossSum"
var res StatisOursProductLossSumRes
if err := request(url, param, &res, token); err != nil {
return nil, err return nil, err
} }
return &res, nil return &res, nil
@ -73,7 +86,7 @@ type ProfitRankingSumResponse struct {
func GetProfitRankingSumApi(param *GetProfitRankingSumRequest) (*GetProfitRankingSumResponse, error) { func GetProfitRankingSumApi(param *GetProfitRankingSumRequest) (*GetProfitRankingSumResponse, error) {
url := "/dataanalytics/profitRankingSum" url := "/dataanalytics/profitRankingSum"
var res GetProfitRankingSumResponse var res GetProfitRankingSumResponse
if err := request(url, param, &res); err != nil { if err := request(url, param, &res, ""); err != nil {
return nil, err return nil, err
} }
return &res, nil return &res, nil
@ -106,7 +119,7 @@ type GetStatisOfficialProductSum struct {
func GetStatisOfficialProductSumApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumResponse, error) { func GetStatisOfficialProductSumApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumResponse, error) {
url := "/dataanalytics/statisOfficialProduct" url := "/dataanalytics/statisOfficialProduct"
var res GetStatisOfficialProductSumResponse var res GetStatisOfficialProductSumResponse
if err := request(url, param, &res); err != nil { if err := request(url, param, &res, ""); err != nil {
return nil, err return nil, err
} }
return &res, nil return &res, nil
@ -133,7 +146,7 @@ type GetStatisOfficialProductSumDecline struct {
func GetStatisOfficialProductSumDeclineApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumDeclineResponse, error) { func GetStatisOfficialProductSumDeclineApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumDeclineResponse, error) {
url := "/dataanalytics/statisOfficialProductDecline" url := "/dataanalytics/statisOfficialProductDecline"
var res GetStatisOfficialProductSumDeclineResponse var res GetStatisOfficialProductSumDeclineResponse
if err := request(url, param, &res); err != nil { if err := request(url, param, &res, ""); err != nil {
return nil, err return nil, err
} }
return &res, nil return &res, nil
@ -162,23 +175,119 @@ type StatisFilterOfficialProductResponse struct {
func GetStatisFilterOfficialProductApi(param *GetStatisFilterOfficialProductRequest) (*GetStatisFilterOfficialProductResponse, error) { func GetStatisFilterOfficialProductApi(param *GetStatisFilterOfficialProductRequest) (*GetStatisFilterOfficialProductResponse, error) {
url := "/dataanalytics/statisFilterOfficialProduct" url := "/dataanalytics/statisFilterOfficialProduct"
var res GetStatisFilterOfficialProductResponse var res GetStatisFilterOfficialProductResponse
if err := request(url, param, &res); err != nil { if err := request(url, param, &res, ""); err != nil {
return nil, err return nil, err
} }
return &res, nil return &res, nil
} }
func request(url string, reqData interface{}, resData interface{}) error { //type GetManagerAndDefaultLossReasonRequest struct {
// ResellerId int32 ` json:"reseller_id"`
// GoodsIds int32 ` json:"reseller_id"`
//}
type GetManagerAndDefaultLossReasonRequest struct {
Param map[int32]map[string][]int32 ` json:"param"`
}
type GetManagerAndDefaultLossReasonResponse struct {
Res []*GetManagerAndDefaultLossReasonResponseList `json:"res,omitempty"`
}
type GetManagerAndDefaultLossReasonResponseList struct {
ResellerInfo *GetManagerAndDefaultLossReasonResponse_ResellerInfo `json:"GetManagerAndDefaultLossReasonResponse_ResellerInfo,omitempty"`
ProductList []*GetManagerAndDefaultLossReasonResponse_ProductList `json:"GetManagerAndDefaultLossReasonResponse_ProductList,omitempty"`
}
type GetManagerAndDefaultLossReasonResponse_ResellerInfo struct {
ResellerId int32 `json:"reseller_id,omitempty"`
AfterSaleName string `json:"after_sale_name,omitempty"`
}
type GetManagerAndDefaultLossReasonResponse_ProductList struct {
ProductId int32 `json:"product_id,omitempty"`
LossReason string `json:"loss_reason,omitempty"`
}
// GetStatisFilterOfficialProductApi 官方商品列表
func GetManagerAndDefaultLossReasonApi(param *GetManagerAndDefaultLossReasonRequest, token string, reqUrl string) ([]*GetManagerAndDefaultLossReasonResponseList, error) {
return []*GetManagerAndDefaultLossReasonResponseList{
{
ResellerInfo: &GetManagerAndDefaultLossReasonResponse_ResellerInfo{
ResellerId: 25009,
AfterSaleName: "张三",
},
ProductList: []*GetManagerAndDefaultLossReasonResponse_ProductList{
{
ProductId: 129,
LossReason: "小米钱包h5-QQ音乐绿钻季卡原因",
},
{
ProductId: 2218,
LossReason: "小米钱包h5-百度网盘新vip会员月卡原因",
},
{
ProductId: 3226,
LossReason: "小米钱包h5-腾讯视频月卡-小米钱包2024原因",
},
{
ProductId: 3364,
LossReason: "小米钱包h5-腾讯视频月卡-0元直充原因",
},
},
},
}, nil
reqParam, err := util.StructToMap(param)
if err != nil {
return nil, err
}
req := &l_request.Request{
Url: reqUrl + "/admin/reseller/resellerAuthProduct/getManagerAndDefaultLossReason",
Method: http.MethodPost,
Json: reqParam,
Headers: map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", token),
},
}
res, err := req.Send()
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("request failed, status code: %d,resion: %s", res.StatusCode, res.Reason)
}
var code resCode
if err = json.Unmarshal(res.Content, &code); err != nil {
return nil, fmt.Errorf("返回结构异常:%s", string(res.Content))
}
var resData []*GetManagerAndDefaultLossReasonResponseList
if err = json.Unmarshal(code.Data, &resData); err != nil {
return nil, fmt.Errorf("返回数据异常:%s", string(res.Content))
}
return resData, nil
}
func request(url string, reqData interface{}, resData interface{}, token string) error {
requestSchema := Base
if len(token) > 0 {
requestSchema = AuthUrl
}
reqParam, err := pkg.StructToURLValues(reqData) reqParam, err := pkg.StructToURLValues(reqData)
if err != nil { if err != nil {
return err return err
} }
req := &l_request.Request{ req := &l_request.Request{
Url: FormatPHPURL(Base+url, reqParam), Url: FormatPHPURL(requestSchema+url, reqParam),
Method: http.MethodGet, Method: http.MethodGet,
} }
if len(token) > 0 {
req.Headers = map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", token),
}
}
res, err := req.Send() res, err := req.Send()
if err != nil { if err != nil {
return err return err

View File

@ -3,8 +3,10 @@ package bbxt
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
pkginner "ai_scheduler/internal/pkg" pkginner "ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_oss" "ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/pkg" "ai_scheduler/pkg"
"context"
"fmt" "fmt"
"math/rand" "math/rand"
"slices" "slices"
@ -18,18 +20,17 @@ const (
GreenStyle = "${color: 008000;horizontal:center;vertical:center;borderColor:#000000}" GreenStyle = "${color: 008000;horizontal:center;vertical:center;borderColor:#000000}"
) )
const (
IndexLossSumDetail = "lossSumDetail"
)
type LossSumInitFunc func(ctx context.Context, day time.Time, totalDetail []*ResellerLoss, selfObj *BbxtTools) error
var ( var (
DownWardValue int32 = 1000 DownWardValue int32 = 1000
SumFilter int32 = -150 SumFilter int32 = -150
) )
var resellerBlackList = []string{
"悦跑",
"电商-独立",
"蓝星严选连续包月",
"通钱-2025年12月",
}
var resellerBlackListProduct = []string{ var resellerBlackListProduct = []string{
"悦跑", "悦跑",
"电商-独立", "电商-独立",
@ -43,9 +44,10 @@ type BbxtTools struct {
excelTempDir string excelTempDir string
ossClient *utils_oss.Client ossClient *utils_oss.Client
config *config.Config config *config.Config
login *lsxd.Login
} }
func NewBbxtTools(config *config.Config) (*BbxtTools, error) { func NewBbxtTools(config *config.Config, login *lsxd.Login) (*BbxtTools, error) {
cache, err := pkg.GetCacheDir() cache, err := pkg.GetCacheDir()
if err != nil { if err != nil {
return nil, err return nil, err
@ -59,12 +61,21 @@ func NewBbxtTools(config *config.Config) (*BbxtTools, error) {
cacheDir: cache, cacheDir: cache,
excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir), excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir),
config: config, config: config,
login: login,
}, nil }, nil
} }
func (b *BbxtTools) DailyReport(now time.Time, downWardValue int32, productName []string, sumFilter int32, ossClient *utils_oss.Client) (reports []*ReportRes, err error) { func (b *BbxtTools) DailyReport(
ctx context.Context,
now time.Time,
downWardValue int32,
productName []string,
sumFilter int32,
ossClient *utils_oss.Client,
initFunc LossSumInitFunc,
) (reports []*ReportRes, err error) {
reports = make([]*ReportRes, 0, 4) reports = make([]*ReportRes, 0, 4)
productLossReport, err := b.StatisOursProductLossSum(now) productLossReport, err := b.StatisOursProductLossSum(ctx, now, initFunc)
if err != nil { if err != nil {
return return
} }
@ -94,7 +105,7 @@ func (b *BbxtTools) DailyReport(now time.Time, downWardValue int32, productName
} }
// StatisOursProductLossSum 负利润分析 // StatisOursProductLossSum 负利润分析
func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes, err error) { func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time, initFunc LossSumInitFunc) (report []*ReportRes, err error) {
ct := []string{ ct := []string{
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"), time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
adjustedTime(now), //adjustedTime(time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())), adjustedTime(now), //adjustedTime(time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())),
@ -110,6 +121,7 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
resellerMap = make(map[int32]*ResellerLoss) resellerMap = make(map[int32]*ResellerLoss)
total [][]string total [][]string
gt []*ResellerLoss gt []*ResellerLoss
totalDetail []*ResellerLoss
) )
for _, info := range data.List { for _, info := range data.List {
@ -119,8 +131,8 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
resellerMap[info.ResellerId] = &ResellerLoss{ resellerMap[info.ResellerId] = &ResellerLoss{
ResellerId: info.ResellerId, ResellerId: info.ResellerId,
ResellerName: info.ResellerName, ResellerName: info.ResellerName,
Total: 0, // 初始化为0后续累加 Total: 0, // 初始化为0后续累加
ProductLoss: make(map[int32]ProductLoss), // 初始化map ProductLoss: make(map[int32]*ProductLoss), // 初始化map
} }
} }
@ -133,7 +145,7 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
// 检查产品是否已存在 // 检查产品是否已存在
if _, ok := reseller.ProductLoss[info.OursProductId]; !ok { if _, ok := reseller.ProductLoss[info.OursProductId]; !ok {
// 创建新的产品亏损记录 // 创建新的产品亏损记录
reseller.ProductLoss[info.OursProductId] = ProductLoss{ reseller.ProductLoss[info.OursProductId] = &ProductLoss{
ProductId: info.OursProductId, ProductId: info.OursProductId,
ProductName: info.OursProductName, ProductName: info.OursProductName,
Loss: info.Loss, // 初始化为当前产品的亏损 Loss: info.Loss, // 初始化为当前产品的亏损
@ -155,7 +167,8 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
return resellers[i].Total < resellers[j].Total return resellers[i].Total < resellers[j].Total
}) })
var ( var (
totalSum float64 totalSum float64
totalSum500 float64 totalSum500 float64
) )
// 构建分组 // 构建分组
@ -166,46 +179,116 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
fmt.Sprintf("%.2f", v.Total), fmt.Sprintf("%.2f", v.Total),
}) })
totalSum += v.Total totalSum += v.Total
totalDetail = append(totalDetail, v)
} }
if v.Total <= -500 && !slices.Contains(resellerBlackListProduct, v.ResellerName) { if v.Total <= -500 && !slices.Contains(resellerBlackListProduct, v.ResellerName) {
gt = append(gt, v) gt = append(gt, v)
totalSum500 += v.Total totalSum500 += v.Total
} }
} }
report = make([]*ReportRes, 2) report = make([]*ReportRes, 3)
timeCh := now.Format("1月2日15点") timeCh := now.Format("1月2日15点")
//总量生成excel //总量生成excel
if len(total) > 0 { //if len(total) > 0 {
filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx" // filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total, "") // err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total, "")
report[0] = &ReportRes{ // if err != nil {
ReportName: "负利润分析(合计表)", // return
Title: "截至" + timeCh + "利润累计亏损" + fmt.Sprintf("%.2f", totalSum), // }
Path: filePath, // report[0] = &ReportRes{
Data: total, // ReportName: "分销商负利润统计",
} // Title: "截至" + timeCh + "利润累计亏损" + fmt.Sprintf("%.2f", totalSum),
} // Path: filePath,
// Data: total,
// }
//}
if err != nil { //if len(gt) > 0 {
return // filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
} // title := "截至" + timeCh + "亏损500以上的分销商和产品"
if len(gt) > 0 { // err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt, title)
filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx" // if err != nil {
title := "截至" + timeCh + "亏损500以上的分销商和产品" // return
err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt, title) // }
report[1] = &ReportRes{ // report[1] = &ReportRes{
ReportName: "负利润分析(亏损500以上)", // ReportName: "负利润分析(亏损500以上)",
Title: "截至" + timeCh + "亏损500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500), // Title: "截至" + timeCh + "亏损500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500),
// Path: filePath,
// Data: total,
// }
//}
if len(totalDetail) > 0 {
err = initFunc(ctx, now, totalDetail, b)
if err != nil {
return
}
filePath := b.cacheDir + "/kshj_total_ana" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
title := "截至" + timeCh + "亏损100以上的分销商&产品负利润原因"
err = b.resellerDetailFillExcelAna(b.excelTempDir+"/"+"kshj_total_ana.xlsx", filePath, totalDetail, title)
if err != nil {
return
}
report[2] = &ReportRes{
ReportName: "负利润分析(亏损100以上)",
Title: "截至" + timeCh + "亏损100以上利润原因",
Path: filePath, Path: filePath,
Data: total, Data: total,
} }
} }
if err != nil {
return
}
return report, nil return report, nil
} }
func (b *BbxtTools) GetResellerLossMannagerAndLossReasonFromApi(ctx context.Context, resellerLoss []*ResellerLoss) (map[int32]*ResellerLossSumProductRelation, error) {
var (
resellerMap = make(map[int32]map[string][]int32)
resellerLossMap = make(map[int32]*ResellerLoss, len(resellerLoss))
relationMap = make(map[int32]*ResellerLossSumProductRelation)
)
for _, v := range resellerLoss {
var productSlice = make([]int32, 0, len(v.ProductLoss))
for _, vv := range v.ProductLoss {
productSlice = append(productSlice, vv.ProductId)
}
if _, ex := resellerMap[v.ResellerId]; !ex {
resellerMap[v.ResellerId] = make(map[string][]int32, 1)
}
resellerMap[v.ResellerId]["values"] = productSlice
resellerLossMap[v.ResellerId] = v
relationMap[v.ResellerId] = &ResellerLossSumProductRelation{
ResellerName: v.ResellerName,
Products: make(map[int32]*LossReason, len(v.ProductLoss)),
}
for _, product := range v.ProductLoss {
relationMap[v.ResellerId].Products[product.ProductId] = &LossReason{
ProductName: product.ProductName,
LossReason: "未填写", // 初始化为未填写
}
}
}
res, err := GetManagerAndDefaultLossReasonApi(&GetManagerAndDefaultLossReasonRequest{
Param: resellerMap,
}, b.login.GetToken(ctx), b.config.ZLTX.ReqUrl)
if err != nil {
return nil, err
}
for _, v := range res {
if v == nil {
continue
}
if _, ok := resellerLossMap[v.ResellerInfo.ResellerId]; !ok {
continue
}
relationMap[v.ResellerInfo.ResellerId].AfterSaleName = v.ResellerInfo.AfterSaleName
for _, vv := range v.ProductList {
relationMap[v.ResellerInfo.ResellerId].Products[vv.ProductId].LossReason = vv.LossReason
}
}
return relationMap, nil
}
// GetProfitRankingSum 利润同比分销商排行榜 // GetProfitRankingSum 利润同比分销商排行榜
func (b *BbxtTools) GetProfitRankingSum(now time.Time) (report *ReportRes, err error) { func (b *BbxtTools) GetProfitRankingSum(now time.Time) (report *ReportRes, err error) {

View File

@ -2,10 +2,19 @@ package bbxt
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_oss" "ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/utils"
"context"
"encoding/json"
"strings" "strings"
"testing" "testing"
"time" "time"
"xorm.io/builder"
) )
func Test_StatisOursProductLossSumApiTotal(t *testing.T) { func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
@ -23,29 +32,30 @@ func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
o, err := NewBbxtTools() o, err := NewBbxtTools(nil, nil)
if err != nil { if err != nil {
panic(err) panic(err)
} }
reports, err := o.DailyReport(time.Now(), DownWardValue, []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, SumFilter, ossClient) reports, err := o.DailyReport(context.Background(), time.Now(), DownWardValue, []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, SumFilter, ossClient, GetReportCache)
t.Log(reports, err) t.Log(reports, err)
} }
func Test_StatisOursProductLossSum(t *testing.T) { func Test_StatisOursProductLossSum(t *testing.T) {
o, err := NewBbxtTools() run()
o, err := NewBbxtTools(configConfig, lsxd.NewLogin(configConfig, utils.NewRdb(configConfig)))
if err != nil { if err != nil {
panic(err) panic(err)
} }
report, err := o.StatisOursProductLossSum(time.Now()) report, err := o.StatisOursProductLossSum(context.Background(), time.Now(), GetReportCache)
t.Log(report, err) t.Log(report, err)
} }
func Test_GetProfitRankingSum(t *testing.T) { func Test_GetProfitRankingSum(t *testing.T) {
o, err := NewBbxtTools() o, err := NewBbxtTools(nil, nil)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -56,7 +66,7 @@ func Test_GetProfitRankingSum(t *testing.T) {
} }
func Test_GetStatisOfficialProductSumDecline(t *testing.T) { func Test_GetStatisOfficialProductSumDecline(t *testing.T) {
o, err := NewBbxtTools() o, err := NewBbxtTools(nil, nil)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -69,7 +79,9 @@ func Test_GetStatisOfficialProductSumDecline(t *testing.T) {
} }
func Test_GetStatisOfficialProductSum(t *testing.T) { func Test_GetStatisOfficialProductSum(t *testing.T) {
o, err := NewBbxtTools()
configs := configConfig
o, err := NewBbxtTools(nil, lsxd.NewLogin(configs, utils.NewRdb(configConfig)))
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -79,3 +91,62 @@ func Test_GetStatisOfficialProductSum(t *testing.T) {
t.Log(report, err) t.Log(report, err)
} }
var (
reportDailyCacheImpl *impl.ReportDailyCacheImpl
configConfig *config.Config
)
func run() {
configConfig, _ = config.LoadConfigWithTest()
// 初始化数据库连接
db, _ := utils.NewGormDb(configConfig)
reportDailyCacheImpl = impl.NewReportDailyCacheImpl(db)
}
func GetReportCache(ctx context.Context, day time.Time, totalDetail []*ResellerLoss, bbxtObj *BbxtTools) error {
run()
var ResellerProductRelation map[int32]*ResellerLossSumProductRelation
dayDate := day.Format(time.DateOnly)
cond := builder.NewCond()
cond = cond.And(builder.Eq{"`index`": IndexLossSumDetail})
cond = cond.And(builder.Eq{"`key`": dayDate})
var cache model.AiReportDailyCache
err := reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache)
if err != nil {
return err
}
if cache.Value == "" {
ResellerProductRelation, err = bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail)
if err != nil {
return err
}
cache = model.AiReportDailyCache{
Key: dayDate,
Index: IndexLossSumDetail,
Value: pkg.JsonStringIgonErr(ResellerProductRelation),
}
_, err = reportDailyCacheImpl.Add(&cache)
if err != nil {
return err
}
}
err = json.Unmarshal([]byte(cache.Value), &ResellerProductRelation)
for _, v := range totalDetail {
if _, ex := ResellerProductRelation[v.ResellerId]; !ex {
continue
}
v.Manager = ResellerProductRelation[v.ResellerId].AfterSaleName
for _, vv := range v.ProductLoss {
if _, ex := ResellerProductRelation[v.ResellerId].Products[vv.ProductId]; !ex {
continue
}
vv.LossReason = ResellerProductRelation[v.ResellerId].Products[vv.ProductId].LossReason
}
}
return nil
}

View File

@ -4,13 +4,15 @@ type ResellerLoss struct {
ResellerId int32 ResellerId int32
ResellerName string ResellerName string
Total float64 Total float64
ProductLoss map[int32]ProductLoss ProductLoss map[int32]*ProductLoss
Manager string
} }
type ProductLoss struct { type ProductLoss struct {
ProductId int32 ProductId int32
ProductName string ProductName string
Loss float64 Loss float64
LossReason string
} }
type ReportRes struct { type ReportRes struct {
@ -37,3 +39,14 @@ type ProductSumReseller struct {
HistoryTwoNum int32 //上周成功数量 HistoryTwoNum int32 //上周成功数量
HistoryTwoDiff int32 //同比上周当前增减量 HistoryTwoDiff int32 //同比上周当前增减量
} }
type ResellerLossSumProductRelation struct {
AfterSaleName string `json:"after_sale_name"`
ResellerName string `json:"reseller_name"`
Products map[int32]*LossReason
}
type LossReason struct {
ProductName string
LossReason string
}

View File

@ -242,7 +242,7 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d
for _, reseller := range dataSlice { for _, reseller := range dataSlice {
// 排序 ProductLoss // 排序 ProductLoss
var products []ProductLoss var products []*ProductLoss
for _, p := range reseller.ProductLoss { for _, p := range reseller.ProductLoss {
products = append(products, p) products = append(products, p)
} }
@ -311,4 +311,164 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d
return f.SaveAs(outputPath) return f.SaveAs(outputPath)
} }
// OfficialProductSumDeclineExcel func (b *BbxtTools) resellerDetailFillExcelAna(templatePath, outputPath string, dataSlice []*ResellerLoss, title string) error {
// 1. 读取模板
f, err := excelize.OpenFile(templatePath)
if err != nil {
return err
}
defer f.Close()
sheet := f.GetSheetName(0)
if len(title) > 0 {
// 写入标题
f.SetCellValue(sheet, "A1", title)
}
// ---------------- 样式获取 ----------------
// 模板第2行数据行样式
tplRowData := 3
styleA3, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowData))
if err != nil {
styleA3 = 0
}
// B2和C2通常样式一致这里取B2作为明细列样式
styleB3, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowData))
if err != nil {
styleB3 = 0
}
styleC3, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowData))
if err != nil {
styleC3 = 0
}
styleD3, err := f.GetCellStyle(sheet, fmt.Sprintf("D%d", tplRowData))
if err != nil {
styleC3 = 0
}
styleE3, err := f.GetCellStyle(sheet, fmt.Sprintf("E%d", tplRowData))
if err != nil {
styleC3 = 0
}
rowHeightData, err := f.GetRowHeight(sheet, tplRowData)
if err != nil {
rowHeightData = 20
}
// 模板第4行合计行样式
tplRowTotal := 5
styleTotalA, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowTotal))
if err != nil {
styleTotalA = 0
}
styleTotalB, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowTotal))
if err != nil {
styleTotalB = 0
}
styleTotalC, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowTotal))
if err != nil {
styleTotalC = 0
}
styleTotalD, err := f.GetCellStyle(sheet, fmt.Sprintf("D%d", tplRowTotal))
if err != nil {
styleTotalC = 0
}
styleTotalE, err := f.GetCellStyle(sheet, fmt.Sprintf("E%d", tplRowTotal))
if err != nil {
styleTotalC = 0
}
rowHeightTotal, err := f.GetRowHeight(sheet, tplRowTotal)
if err != nil {
rowHeightTotal = 30
}
// ----------------------------------------
currentRow := 3
totalLoss := 0.0
for _, reseller := range dataSlice {
// 排序 ProductLoss
var products []*ProductLoss
for _, p := range reseller.ProductLoss {
products = append(products, p)
}
sort.Slice(products, func(i, j int) bool {
return products[i].Loss < products[j].Loss
})
startRow := currentRow
// 填充该经销商的所有产品
for _, p := range products {
// 设置行高
f.SetRowHeight(sheet, currentRow, rowHeightData)
// 设置值
f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), reseller.ResellerName)
f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.ProductName)
f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), p.Loss)
f.SetCellValue(sheet, fmt.Sprintf("D%d", currentRow), reseller.Manager)
f.SetCellValue(sheet, fmt.Sprintf("E%d", currentRow), p.LossReason)
// 设置样式
if styleA3 != 0 {
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleA3)
}
if styleB3 != 0 {
f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleB3)
}
if styleC3 != 0 {
f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleC3)
}
if styleD3 != 0 {
f.SetCellStyle(sheet, fmt.Sprintf("D%d", currentRow), fmt.Sprintf("D%d", currentRow), styleD3)
}
if styleE3 != 0 {
f.SetCellStyle(sheet, fmt.Sprintf("E%d", currentRow), fmt.Sprintf("E%d", currentRow), styleE3)
}
totalLoss += p.Loss
currentRow++
}
endRow := currentRow - 1
// 合并单元格 (如果多于1行)
if endRow > startRow {
f.MergeCell(sheet, fmt.Sprintf("A%d", startRow), fmt.Sprintf("A%d", endRow))
f.MergeCell(sheet, fmt.Sprintf("D%d", startRow), fmt.Sprintf("D%d", endRow))
}
}
// ---------------- 填充合计行 ----------------
// 四舍五入保留四位小数
totalLoss, _ = decimal.NewFromFloat(totalLoss).Round(4).Float64()
// 设置行高
f.SetRowHeight(sheet, currentRow, rowHeightTotal)
f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), "合计")
// B列留空C列填充总亏损
f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), totalLoss)
// 设置合计行样式
if styleTotalA != 0 {
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleTotalA)
}
if styleTotalB != 0 {
f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleTotalB)
}
if styleTotalC != 0 {
f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleTotalC)
}
if styleTotalD != 0 {
f.SetCellStyle(sheet, fmt.Sprintf("D%d", currentRow), fmt.Sprintf("D%d", currentRow), styleTotalD)
}
if styleTotalE != 0 {
f.SetCellStyle(sheet, fmt.Sprintf("E%d", currentRow), fmt.Sprintf("E%d", currentRow), styleTotalE)
}
// 取消合并合计行的A、B列
// f.MergeCell(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow))
// 6. 保存
return f.SaveAs(outputPath)
}

View File

@ -4,16 +4,31 @@ import (
config2 "ai_scheduler/internal/config" config2 "ai_scheduler/internal/config"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"context" "context"
"strings"
"testing" "testing"
) )
func Test_task(t *testing.T) { func Test_task(t *testing.T) {
c := NewZltxOrderDetailTool(config2.ToolConfig{}, nil) c := NewZltxOrderDetailTool(config2.ToolConfig{
BaseURL: "https://revcl.1688sup.com/api/admin/direct/ai/%s",
AddURL: "https://revcl.1688sup.com/api/admin/direct/log/%s/%s",
}, nil)
ch := make(chan entitys.Response, 10000)
err := err :=
c.Execute(context.Background(), &entitys.Recognize{ c.Execute(context.Background(), &entitys.Recognize{
Match: &entitys.Match{ Match: &entitys.Match{
Parameters: `{"order_number": 859393216068067329}`, Parameters: `{"order_number": 864086822064234497}`,
}, },
Ext: []byte(`{"auth":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY3OTM0MTg1LCJuYmYiOjE3Njc5MjMzODUsImp0aSI6IjE3OSIsIlBob25lIjoiMTUwMDA0NTAxNTUiLCJVc2VyTmFtZSI6IjE1MDAwNDUwMTU1IiwiUmVhbE5hbWUiOiLliJjlvanlhpsiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9BRE1JTixWQ0xfU1lTVEVNLENSTV9BRE1JTixMU1hERFNfQURNSU4sTFNYRERTX1NZU1RFTSxNQVJLRVRJTkdTQUFTX1NVUEVSQURNSU4sU1RBVElTVElDQUxTWVNURU1fQURNSU4sUEhZU0lDQUxHT09EU19BRE1JTixNQVJLRVRJTkdTWVNURU1fU1VQRVIsUkVQT1JUQ0VOVEVSX0FETUlOLE1PTklUT1JfTUFESU4sWkxUWF9BRE1JTixaTFRYX09QRVJBVEUiLCJEaW5nVXNlcklkIjoiMTcxMTkzNTg3NTAzMjk5MzUifQ.d9z0S1Ia-PFAxhGstT055Amt8PI09bUHxG0_lba4UwvSomiTNCD-5DFdMkbZHwiDTlhVdBjcd1mDYFRRZXWMPoSnanubMBnRnuvTi8csch5nz1L9oWNo-HFyBE3lMw9-UJ5j84gz228_kcBsvRATT1Ixs9bnuaN9CDNz20c524llDt10C3cc8wLGMin4jWEMF4RNrf2oBZOFAahRYSJNeBmutIwRSIP1pMIAaUy_IkMCyOwK8JzgMnHJGLwIH_nxR9XZXlAN0FmrmtWVkRA2YUKvoDX5a5BCYmDVNqUbi_ZNuRPJH87Ia7_-UoyJu8Yq79jX0Qgsm6qJ4rX2nauneg"}`),
UserContent: &entitys.RecognizeUserContent{Text: "订单查询864086822064234497\n"},
}) })
t.Log(err) if err != nil {
t.Log(err)
}
var res strings.Builder
for v := range ch {
res.WriteString(v.Content)
}
t.Log(res)
} }

View File

@ -84,8 +84,8 @@ type ZltxOrderDetailData struct {
// Execute 执行直连天下订单详情查询 // Execute 执行直连天下订单详情查询
func (w *ZltxOrderDetailTool) Execute(ctx context.Context, rec *entitys.Recognize) error { func (w *ZltxOrderDetailTool) Execute(ctx context.Context, rec *entitys.Recognize) error {
var req ZltxOrderDetailRequest var req = &ZltxOrderDetailRequest{}
if err := json.Unmarshal([]byte(rec.Match.Parameters), &req); err != nil { if err := req.UnmarshalJSON([]byte(rec.Match.Parameters)); err != nil {
return fmt.Errorf("invalid zltxOrderDetail request: %w", err) return fmt.Errorf("invalid zltxOrderDetail request: %w", err)
} }
if req.OrderNumber == "" { if req.OrderNumber == "" {
@ -96,6 +96,22 @@ func (w *ZltxOrderDetailTool) Execute(ctx context.Context, rec *entitys.Recogniz
return w.getZltxOrderDetail(rec, req.OrderNumber) return w.getZltxOrderDetail(rec, req.OrderNumber)
} }
func (r *ZltxOrderDetailRequest) UnmarshalJSON(data []byte) error {
var tmp struct {
OrderNumber json.Number `json:"order_number"` // 使用 json.Number 保留原始格式
}
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
// 根据需要转换为 int64 或 string
if num, err := tmp.OrderNumber.Int64(); err == nil {
r.OrderNumber = num
} else {
r.OrderNumber = tmp.OrderNumber.String()
}
return nil
}
// getMockZltxOrderDetail 获取模拟直连天下订单详情数据 // getMockZltxOrderDetail 获取模拟直连天下订单详情数据
func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number interface{}) (err error) { func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number interface{}) (err error) {
log.Infof("订单编号:%v,类型:%v") log.Infof("订单编号:%v,类型:%v")
@ -170,8 +186,13 @@ func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number
err = w.llm.ChatStream(context.TODO(), rec.Ch, []api.Message{ err = w.llm.ChatStream(context.TODO(), rec.Ch, []api.Message{
{ {
Role: "system", Role: "system",
Content: "你是一个订单日志助手。用户可能会提供订单日志,你需要分析订单日志,失败订单->分析失败原因,成功订单->找出整个日志的 Base64 编码的 JSON 数据的内容进行转换并反馈给我", Content: "你是一个订单日志助手。用户可能会提供订单日志,你需要按以下规则处理:\n" +
"1. **先输出结论**:用<conclusion></conclusion>标签包裹关键结论如失败原因或Base64解码内容\n" +
"2. **再输出分析过程**:详细解释如何得出结论;分析过程直接输出分析内容,不需要标明是分析过程\n" +
"3. **订单类型处理**\n" +
" - 失败订单:分析失败原因(如支付超时、库存不足等);\n" +
" - 成功订单提取日志中的Base64编码JSON数据解码后转换为用户可读的格式如表格或JSON。",
}, },
{ {
Role: "assistant", Role: "assistant",

Binary file not shown.