Compare commits

..

2 Commits
v4 ... master

32 changed files with 225 additions and 959 deletions

View File

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

View File

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

View File

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

View File

@ -29,22 +29,21 @@ import (
// AiRouterBiz 智能路由服务
type DingTalkBotBiz struct {
do *do.Do
handle *do.Handle
botConfigImpl *impl.BotConfigImpl
replier *chatbot.ChatbotReplier
log log.Logger
dingTalkUser *dingtalk.User
botGroupImpl *impl.BotGroupImpl
botGroupConfigImpl *impl.BotGroupConfigImpl
botGroupQywxImpl *impl.BotGroupQywxImpl
toolManager *tools.Manager
chatHis *impl.BotChatHisImpl
conf *config.Config
cardSend *dingtalk.SendCardClient
qywxGroupHandle *qywx.Group
groupConfigBiz *GroupConfigBiz
reportDailyCacheImpl *impl.ReportDailyCacheImpl
do *do.Do
handle *do.Handle
botConfigImpl *impl.BotConfigImpl
replier *chatbot.ChatbotReplier
log log.Logger
dingTalkUser *dingtalk.User
botGroupImpl *impl.BotGroupImpl
botGroupConfigImpl *impl.BotGroupConfigImpl
botGroupQywxImpl *impl.BotGroupQywxImpl
toolManager *tools.Manager
chatHis *impl.BotChatHisImpl
conf *config.Config
cardSend *dingtalk.SendCardClient
qywxGroupHandle *qywx.Group
groupConfigBiz *GroupConfigBiz
}
// NewDingTalkBotBiz
@ -55,25 +54,23 @@ func NewDingTalkBotBiz(
botGroupImpl *impl.BotGroupImpl,
dingTalkUser *dingtalk.User,
chatHis *impl.BotChatHisImpl,
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
toolManager *tools.Manager,
conf *config.Config,
cardSend *dingtalk.SendCardClient,
groupConfigBiz *GroupConfigBiz,
) *DingTalkBotBiz {
return &DingTalkBotBiz{
do: do,
handle: handle,
botConfigImpl: botConfigImpl,
replier: chatbot.NewChatbotReplier(),
dingTalkUser: dingTalkUser,
groupConfigBiz: groupConfigBiz,
botGroupImpl: botGroupImpl,
toolManager: toolManager,
chatHis: chatHis,
conf: conf,
cardSend: cardSend,
reportDailyCacheImpl: reportDailyCacheImpl,
do: do,
handle: handle,
botConfigImpl: botConfigImpl,
replier: chatbot.NewChatbotReplier(),
dingTalkUser: dingTalkUser,
groupConfigBiz: groupConfigBiz,
botGroupImpl: botGroupImpl,
toolManager: toolManager,
chatHis: chatHis,
conf: conf,
cardSend: cardSend,
}
}
@ -200,49 +197,6 @@ func (d *DingTalkBotBiz) Macro(ctx context.Context, requireData *entitys.Require
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
}
@ -378,7 +332,9 @@ func (d *DingTalkBotBiz) HandleStreamRes(ctx context.Context, data *chatbot.BotC
}
func (d *DingTalkBotBiz) SendReport(ctx context.Context, groupInfo *model.AiBotGroup, report *bbxt.ReportRes) (err error) {
if report == nil {
return errors.New("report is nil")
}
reportChan := make(chan string, 10)
defer close(reportChan)
reportChan <- report.Title

View File

@ -2,7 +2,6 @@ package do
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
errors "ai_scheduler/internal/data/error"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model"
@ -33,18 +32,16 @@ type Do struct {
}
func NewDo(
sessionImpl *impl.SessionImpl,
sysImpl *impl.SysImpl,
taskImpl *impl.TaskImpl,
hisImpl *impl.ChatHisImpl,
conf *config.Config,
) *Do {
return &Do{
conf: conf,
sessionImpl: sessionImpl,
sysImpl: sysImpl,
hisImpl: hisImpl,
taskImpl: taskImpl,
conf: conf,
sysImpl: sysImpl,
hisImpl: hisImpl,
taskImpl: taskImpl,
}
}
@ -268,27 +265,16 @@ func (d *Do) startMessageHandler(
if len(chat) > 0 {
// 合并所有回答-转json字符串
ans, _ := json.Marshal(chat)
// 通过 chat 获取 task_id
taskId := d.getTaskIdByChat(chat)
AiRes := &model.AiChatHi{
SessionID: requireData.Session,
Ques: requireData.Req.Text,
Ans: string(ans),
Files: requireData.Req.Img,
TaskID: taskId,
TaskID: requireData.Task.TaskID,
}
d.hisImpl.AddWithData(AiRes)
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{
@ -413,28 +399,3 @@ func (d *Do) LoadUserPermission(client *gateway.Client, requireData *entitys.Req
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,15 +9,11 @@ import (
"ai_scheduler/internal/domain/workflow/recharge"
"ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/l_request"
"ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/internal/tools"
"ai_scheduler/internal/tools/bbxt"
"ai_scheduler/utils"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
@ -34,14 +30,12 @@ import (
// AiRouterBiz 智能路由服务
type GroupConfigBiz struct {
botGroupConfigImpl *impl.BotGroupConfigImpl
reportDailyCacheImpl *impl.ReportDailyCacheImpl
ossClient *utils_oss.Client
workflowManager *runtime.Registry
botTools []model.AiBotTool
toolManager *tools.Manager
conf *config.Config
rdb *utils.Rdb
botGroupConfigImpl *impl.BotGroupConfigImpl
ossClient *utils_oss.Client
workflowManager *runtime.Registry
botTools []model.AiBotTool
toolManager *tools.Manager
conf *config.Config
}
// NewDingTalkBotBiz
@ -51,17 +45,13 @@ func NewGroupConfigBiz(
botGroupConfigImpl *impl.BotGroupConfigImpl,
workflowManager *runtime.Registry,
conf *config.Config,
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
rdb *utils.Rdb,
) *GroupConfigBiz {
return &GroupConfigBiz{
botTools: tools.BootTools,
ossClient: ossClient,
botGroupConfigImpl: botGroupConfigImpl,
workflowManager: workflowManager,
conf: conf,
reportDailyCacheImpl: reportDailyCacheImpl,
rdb: rdb,
botTools: tools.BootTools,
ossClient: ossClient,
botGroupConfigImpl: botGroupConfigImpl,
workflowManager: workflowManager,
conf: conf,
}
}
@ -82,12 +72,12 @@ func (g *GroupConfigBiz) GetReportLists(ctx context.Context, groupConfig *model.
product = strings.Split(groupConfig.ProductName, ",")
}
reportList, err := bbxt.NewBbxtTools(g.conf, lsxd.NewLogin(g.conf, g.rdb))
reportList, err := bbxt.NewBbxtTools(g.conf)
if err != nil {
return
}
reports, err = reportList.DailyReport(ctx, time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, g.ossClient, g.GetReportCache)
reports, err = reportList.DailyReport(time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, g.ossClient)
if err != nil {
return
}
@ -155,8 +145,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
}
}
}
rep, err := bbxt.NewBbxtTools(g.conf, lsxd.NewLogin(g.conf, g.rdb))
rep, err := bbxt.NewBbxtTools(g.conf)
uploader := bbxt.NewUploader(g.ossClient, g.conf)
if err != nil {
return err
@ -164,7 +153,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
var reports []*bbxt.ReportRes
switch rec.Match.Index {
case "report_loss_analysis":
repo, _err := rep.StatisOursProductLossSum(ctx, t, g.GetReportCache)
repo, _err := rep.StatisOursProductLossSum(t)
if _err != nil {
return _err
}
@ -185,7 +174,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
reports = append(reports, repo)
case "report_daily":
product := strings.Split(groupConfig.ProductName, ",")
repo, _err := rep.DailyReport(ctx, t, bbxt.DownWardValue, product, bbxt.SumFilter, nil, g.GetReportCache)
repo, _err := rep.DailyReport(t, bbxt.DownWardValue, product, bbxt.SumFilter, nil)
if _err != nil {
return _err
}
@ -415,47 +404,3 @@ func (g *GroupConfigBiz) otherTask(ctx context.Context, rec *entitys.Recognize)
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
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

@ -0,0 +1,12 @@
package biz
import (
"context"
"testing"
)
func Test_report(t *testing.T) {
run()
chatId, err := groupConfigBiz.GetReportLists(context.Background(), nil)
t.Log(chatId, err)
}

View File

@ -10,6 +10,7 @@ import (
"ai_scheduler/internal/pkg/l_request"
"ai_scheduler/internal/tools/bbxt"
"context"
"fmt"
"log"
"strings"
"time"
@ -111,6 +112,10 @@ func (q *QywxAppBiz) SendReport(ctx context.Context, groupInfo *model.AiBotGroup
// SendReportV2 发送到货易通指定的群聊
func (q *QywxAppBiz) SendReportHYT(ctx context.Context, groupInfo *model.AiBotGroupQywx, report *bbxt.ReportRes) (err error) {
if report == nil {
return fmt.Errorf("report is nil")
}
// 文本消息
err = q.sendReportHYT(groupInfo, &bbxt.ReportRes{
Title: report.Title,

View File

@ -2,8 +2,17 @@ package biz
import (
"ai_scheduler/internal/biz/handle/qywx"
"ai_scheduler/internal/biz/tools_regis"
"ai_scheduler/internal/config"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/domain/component"
"ai_scheduler/internal/domain/component/callback"
"ai_scheduler/internal/domain/repo"
"ai_scheduler/internal/domain/workflow"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_ollama"
"ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/utils"
"context"
"testing"
@ -16,8 +25,9 @@ func Test_InitGroup(t *testing.T) {
}
var (
configConfig *config.Config
qywxAppBiz *QywxAppBiz
configConfig *config.Config
qywxAppBiz *QywxAppBiz
groupConfigBiz *GroupConfigBiz
)
func run() {
@ -28,6 +38,21 @@ func run() {
botGroupQywxImpl := impl.NewBotGroupQywxImpl(db)
qywxAuth := qywx.NewAuth(configConfig, rdb)
group := qywx.NewGroup(botGroupQywxImpl, qywxAuth)
sessionImpl := impl.NewSessionImpl(db)
other := qywx.NewOther(qywxAuth)
repos := repo.NewRepos(sessionImpl, configConfig, rdb)
pkgRdb := pkg.NewRdb(configConfig)
redisManager := callback.NewRedisManager(pkgRdb)
login := lsxd.NewLogin(configConfig, rdb)
components := component.NewComponents(redisManager, login)
repos = repo.NewRepos(sessionImpl, configConfig, rdb)
botToolsImpl := impl.NewBotToolsImpl(db)
toolRegis := tools_regis.NewToolsRegis(botToolsImpl)
utils_ossClient, _ := utils_oss.NewClient(configConfig)
client, _, _ := utils_ollama.NewClient(configConfig)
registry := workflow.NewRegistry(configConfig, client, repos, components)
botGroupConfigImpl := impl.NewBotGroupConfigImpl(db)
qywxAppBiz = NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other)
groupConfigBiz = NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, registry, configConfig)
}

View File

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

View File

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

View File

@ -9,16 +9,3 @@ const (
Enable = 1
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,21 +43,19 @@ type BaseRepository[P PO] interface {
FindAll(conditions ...CondFunc) ([]P, error) // 查询所有
Paginate(page, pageSize int, conditions ...CondFunc) (*PaginationResult[P], error) // 分页查询
FindOne(conditions ...CondFunc) (P, bool, error) // 查询单条记录,若未找到则返回 has=false, err=nil
Take(conditions ...CondFunc) (P, bool, error)
Create(m *P) error // 创建
BatchCreate(m *[]P) (err error) // 批量创建
Update(m *P, conditions ...CondFunc) (err error) // 更新
Delete(conditions ...CondFunc) (err error) // 删除
Count(conditions ...CondFunc) (count int64, err error) // 查询条数
PaginateScope(page, pageSize int) CondFunc // 分页
OrderByDesc(field string) CondFunc // 倒序排序
OrderByAsc(field string) CondFunc // 正序排序
WithId(id interface{}) CondFunc // 查询id
WithStatus(status int) CondFunc // 查询status
GetDb() *gorm.DB // 获取数据库连接
WithLimit(limit int) CondFunc // 限制返回条数
In(field string, values interface{}) CondFunc // 查询字段是否在列表中
Select(fields ...string) CondFunc // 选择字段
Create(m *P) error // 创建
BatchCreate(m *[]P) (err error) // 批量创建
Update(m *P, conditions ...CondFunc) (err error) // 更新
Delete(conditions ...CondFunc) (err error) // 删除
Count(conditions ...CondFunc) (count int64, err error) // 查询条数
PaginateScope(page, pageSize int) CondFunc // 分页
OrderByDesc(field string) CondFunc // 倒序排序
WithId(id interface{}) CondFunc // 查询id
WithStatus(status int) CondFunc // 查询status
GetDb() *gorm.DB // 获取数据库连接
WithLimit(limit int) CondFunc // 限制返回条数
In(field string, values interface{}) CondFunc // 查询字段是否在列表中
Select(fields ...string) CondFunc // 选择字段
}
// PaginationResult 分页查询结果
@ -129,26 +127,6 @@ func (this *BaseModel[P]) FindOne(conditions ...CondFunc) (P, bool, error) {
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 {
err := this.Db.Create(m).Error
@ -215,13 +193,6 @@ 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查询条件生成器
func (this *BaseModel[P]) WithId(id interface{}) CondFunc {
return func(db *gorm.DB) *gorm.DB {

View File

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

View File

@ -1,17 +0,0 @@
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

@ -1,20 +0,0 @@
// 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,10 +33,9 @@ type ChatHisQueryResponse struct {
Files string `gorm:"column:files;not null" json:"files"`
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"`
TaskID int32 `gorm:"column:task_id;not null" json:"task_id"` // 任务ID
TaskName string `gorm:"column:task_name;not null" json:"task_name"` // 任务名称
Contents []string `gorm:"column:contents" json:"contents"` // 前端回传数据
TaskIndex string `gorm:"column:task_index;not null" json:"task_index"` // 任务索引
TaskID int32 `gorm:"column:task_id;not null" json:"task_id"` // 任务ID
TaskName string `gorm:"column:task_name;not null" json:"task_name"` // 任务名称
Contents []string `gorm:"column:contents" json:"contents"` // 前端回传数据
}
func (c *ChatHisQueryResponse) FromModel(chat model.AiChatHi, task model.AiTask) {
@ -50,7 +49,6 @@ func (c *ChatHisQueryResponse) FromModel(chat model.AiChatHi, task model.AiTask)
c.TaskID = chat.TaskID
c.TaskName = task.Name
c.Contents = make([]string, 0)
c.TaskIndex = c.parseTaskIndex(task.Index)
// 解析Content
if "" != chat.Content {
@ -62,20 +60,6 @@ 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 {
HisID int64 `json:"his_id" validate:"required"`
Content string `json:"content" validate:"required"`

View File

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

View File

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

View File

@ -1,10 +1,6 @@
package util
import (
"encoding/json"
"reflect"
"strings"
)
import "encoding/json"
// StructToMap 将结构体转换为 map[string]any
func StructToMap(v any) (map[string]any, error) {
@ -16,28 +12,3 @@ func StructToMap(v any) (map[string]any, error) {
err = json.Unmarshal(b, &m)
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

@ -41,13 +41,13 @@ func (c *CronServer) InitJobs(ctx context.Context) {
c.jobs = []*cronJob{
{
Func: c.cronService.CronReportSendDingTalk,
Name: "直连天下报表推送",
Schedule: "0 12,18,23 * * *",
Name: "直连天下报表推送(钉钉)",
Schedule: "20 12,18,23 * * *",
},
{
Func: c.cronService.CronReportSendQywx,
Name: "直连天下报表推送",
Schedule: "0 12,18,23 * * *",
Name: "直连天下报表推送(微信)",
Schedule: "20 12,18,23 * * *",
},
}
}

View File

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

View File

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

View File

@ -35,23 +35,6 @@ func (s *SessionService) SessionInit(c *fiber.Ctx) error {
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 获取会话列表
func (s *SessionService) SessionList(c *fiber.Ctx) error {
req := &entitys.SessionListRequest{}

View File

@ -3,8 +3,6 @@ package bbxt
import (
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/l_request"
"ai_scheduler/internal/pkg/util"
"encoding/json"
"fmt"
"net/http"
@ -31,23 +29,12 @@ type StatisOursProductLossSumResponse struct {
}
const Base = "https://reportapi.1688sup.com/api"
const AuthUrl = "http://test.analysis.com/api"
// StatisOursProductLossSumApi 负利润分析
func StatisOursProductLossSumApi(param *StatisOursProductLossSumReq) (*StatisOursProductLossSumRes, error) {
url := "/dataanalytics/statisOursProductLossSum"
var res StatisOursProductLossSumRes
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 {
if err := request(url, param, &res); err != nil {
return nil, err
}
return &res, nil
@ -86,7 +73,7 @@ type ProfitRankingSumResponse struct {
func GetProfitRankingSumApi(param *GetProfitRankingSumRequest) (*GetProfitRankingSumResponse, error) {
url := "/dataanalytics/profitRankingSum"
var res GetProfitRankingSumResponse
if err := request(url, param, &res, ""); err != nil {
if err := request(url, param, &res); err != nil {
return nil, err
}
return &res, nil
@ -119,7 +106,7 @@ type GetStatisOfficialProductSum struct {
func GetStatisOfficialProductSumApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumResponse, error) {
url := "/dataanalytics/statisOfficialProduct"
var res GetStatisOfficialProductSumResponse
if err := request(url, param, &res, ""); err != nil {
if err := request(url, param, &res); err != nil {
return nil, err
}
return &res, nil
@ -146,7 +133,7 @@ type GetStatisOfficialProductSumDecline struct {
func GetStatisOfficialProductSumDeclineApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumDeclineResponse, error) {
url := "/dataanalytics/statisOfficialProductDecline"
var res GetStatisOfficialProductSumDeclineResponse
if err := request(url, param, &res, ""); err != nil {
if err := request(url, param, &res); err != nil {
return nil, err
}
return &res, nil
@ -175,119 +162,23 @@ type StatisFilterOfficialProductResponse struct {
func GetStatisFilterOfficialProductApi(param *GetStatisFilterOfficialProductRequest) (*GetStatisFilterOfficialProductResponse, error) {
url := "/dataanalytics/statisFilterOfficialProduct"
var res GetStatisFilterOfficialProductResponse
if err := request(url, param, &res, ""); err != nil {
if err := request(url, param, &res); err != nil {
return nil, err
}
return &res, nil
}
//type GetManagerAndDefaultLossReasonRequest struct {
// ResellerId int32 ` json:"reseller_id"`
// GoodsIds int32 ` json:"reseller_id"`
//}
func request(url string, reqData interface{}, resData interface{}) error {
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)
if err != nil {
return err
}
req := &l_request.Request{
Url: FormatPHPURL(requestSchema+url, reqParam),
Url: FormatPHPURL(Base+url, reqParam),
Method: http.MethodGet,
}
if len(token) > 0 {
req.Headers = map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", token),
}
}
res, err := req.Send()
if err != nil {
return err

View File

@ -3,10 +3,8 @@ package bbxt
import (
"ai_scheduler/internal/config"
pkginner "ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/pkg"
"context"
"fmt"
"math/rand"
"slices"
@ -20,17 +18,18 @@ const (
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 (
DownWardValue int32 = 1000
SumFilter int32 = -150
)
var resellerBlackList = []string{
"悦跑",
"电商-独立",
"蓝星严选连续包月",
"通钱-2025年12月",
}
var resellerBlackListProduct = []string{
"悦跑",
"电商-独立",
@ -44,10 +43,9 @@ type BbxtTools struct {
excelTempDir string
ossClient *utils_oss.Client
config *config.Config
login *lsxd.Login
}
func NewBbxtTools(config *config.Config, login *lsxd.Login) (*BbxtTools, error) {
func NewBbxtTools(config *config.Config) (*BbxtTools, error) {
cache, err := pkg.GetCacheDir()
if err != nil {
return nil, err
@ -61,21 +59,12 @@ func NewBbxtTools(config *config.Config, login *lsxd.Login) (*BbxtTools, error)
cacheDir: cache,
excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir),
config: config,
login: login,
}, nil
}
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) {
func (b *BbxtTools) DailyReport(now time.Time, downWardValue int32, productName []string, sumFilter int32, ossClient *utils_oss.Client) (reports []*ReportRes, err error) {
reports = make([]*ReportRes, 0, 4)
productLossReport, err := b.StatisOursProductLossSum(ctx, now, initFunc)
productLossReport, err := b.StatisOursProductLossSum(now)
if err != nil {
return
}
@ -105,7 +94,7 @@ func (b *BbxtTools) DailyReport(
}
// StatisOursProductLossSum 负利润分析
func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time, initFunc LossSumInitFunc) (report []*ReportRes, err error) {
func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes, err error) {
ct := []string{
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())),
@ -121,7 +110,6 @@ func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time,
resellerMap = make(map[int32]*ResellerLoss)
total [][]string
gt []*ResellerLoss
totalDetail []*ResellerLoss
)
for _, info := range data.List {
@ -131,8 +119,8 @@ func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time,
resellerMap[info.ResellerId] = &ResellerLoss{
ResellerId: info.ResellerId,
ResellerName: info.ResellerName,
Total: 0, // 初始化为0后续累加
ProductLoss: make(map[int32]*ProductLoss), // 初始化map
Total: 0, // 初始化为0后续累加
ProductLoss: make(map[int32]ProductLoss), // 初始化map
}
}
@ -145,7 +133,7 @@ func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time,
// 检查产品是否已存在
if _, ok := reseller.ProductLoss[info.OursProductId]; !ok {
// 创建新的产品亏损记录
reseller.ProductLoss[info.OursProductId] = &ProductLoss{
reseller.ProductLoss[info.OursProductId] = ProductLoss{
ProductId: info.OursProductId,
ProductName: info.OursProductName,
Loss: info.Loss, // 初始化为当前产品的亏损
@ -167,8 +155,7 @@ func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time,
return resellers[i].Total < resellers[j].Total
})
var (
totalSum float64
totalSum float64
totalSum500 float64
)
// 构建分组
@ -179,114 +166,44 @@ func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time,
fmt.Sprintf("%.2f", v.Total),
})
totalSum += v.Total
totalDetail = append(totalDetail, v)
}
if v.Total <= -500 && !slices.Contains(resellerBlackListProduct, v.ResellerName) {
gt = append(gt, v)
totalSum500 += v.Total
}
}
report = make([]*ReportRes, 3)
report = make([]*ReportRes, 2)
timeCh := now.Format("1月2日15点")
//总量生成excel
//if len(total) > 0 {
// 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, "")
// if err != nil {
// return
// }
// report[0] = &ReportRes{
// ReportName: "分销商负利润统计",
// Title: "截至" + timeCh + "利润累计亏损" + fmt.Sprintf("%.2f", totalSum),
// Path: filePath,
// Data: total,
// }
//}
//if len(gt) > 0 {
// filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
// title := "截至" + timeCh + "亏损500以上的分销商和产品"
// err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt, title)
// if err != nil {
// return
// }
// report[1] = &ReportRes{
// ReportName: "负利润分析(亏损500以上)",
// 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以上利润原因",
if len(total) > 0 {
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, "")
report[0] = &ReportRes{
ReportName: "负利润分析(合计表)",
Title: "截至" + timeCh + "利润累计亏损" + fmt.Sprintf("%.2f", totalSum),
Path: filePath,
Data: total,
}
}
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
return
}
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
if len(gt) > 0 {
filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
title := "截至" + timeCh + "亏损500以上的分销商和产品"
err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt, title)
report[1] = &ReportRes{
ReportName: "负利润分析(亏损500以上)",
Title: "截至" + timeCh + "亏损500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500),
Path: filePath,
Data: total,
}
}
return relationMap, nil
if err != nil {
return
}
return report, nil
}
// GetProfitRankingSum 利润同比分销商排行榜

View File

@ -2,19 +2,10 @@ package bbxt
import (
"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/utils"
"context"
"encoding/json"
"strings"
"testing"
"time"
"xorm.io/builder"
)
func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
@ -32,30 +23,29 @@ func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
if err != nil {
panic(err)
}
o, err := NewBbxtTools(nil, nil)
o, err := NewBbxtTools(nil)
if err != nil {
panic(err)
}
reports, err := o.DailyReport(context.Background(), time.Now(), DownWardValue, []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, SumFilter, ossClient, GetReportCache)
reports, err := o.DailyReport(time.Now(), DownWardValue, []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, SumFilter, ossClient)
t.Log(reports, err)
}
func Test_StatisOursProductLossSum(t *testing.T) {
run()
o, err := NewBbxtTools(configConfig, lsxd.NewLogin(configConfig, utils.NewRdb(configConfig)))
o, err := NewBbxtTools(nil)
if err != nil {
panic(err)
}
report, err := o.StatisOursProductLossSum(context.Background(), time.Now(), GetReportCache)
report, err := o.StatisOursProductLossSum(time.Now())
t.Log(report, err)
}
func Test_GetProfitRankingSum(t *testing.T) {
o, err := NewBbxtTools(nil, nil)
o, err := NewBbxtTools(nil)
if err != nil {
panic(err)
}
@ -66,22 +56,21 @@ func Test_GetProfitRankingSum(t *testing.T) {
}
func Test_GetStatisOfficialProductSumDecline(t *testing.T) {
o, err := NewBbxtTools(nil, nil)
o, err := NewBbxtTools(nil)
if err != nil {
panic(err)
}
s := "官方--腾讯-周卡,官方--腾讯-月卡,官方--腾讯-季卡,官方--腾讯-年卡,官方--优酷周卡,官方--优酷月卡,官方--优酷季卡,官方--优酷年卡,官方--爱奇艺-周卡,官方--爱奇艺-月卡,官方--爱奇艺-季卡,官方--爱奇艺-年卡,官方--芒果-PC周卡,官方--芒果-PC月卡,官方--芒果-PC季卡,官方--美团外卖红包5元,官方--美团外卖红包10元,官方--QQ音乐-绿钻月卡,官方--饿了么超级会员月卡,官方--网易云黑胶vip月卡,官方--喜马拉雅巅峰会员月卡"
//s := "官方--QQ音乐-绿钻月卡"
report, err := o.GetStatisOfficialProductSumDecline(time.Now(), 1000, strings.Split(s, ","), -150)
now := time.Now()
report, err := o.GetStatisOfficialProductSumDecline(time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, now.Location()), 1000, strings.Split(s, ","), -150)
t.Log(report, err)
}
func Test_GetStatisOfficialProductSum(t *testing.T) {
configs := configConfig
o, err := NewBbxtTools(nil, lsxd.NewLogin(configs, utils.NewRdb(configConfig)))
o, err := NewBbxtTools(nil)
if err != nil {
panic(err)
}
@ -91,62 +80,3 @@ func Test_GetStatisOfficialProductSum(t *testing.T) {
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,15 +4,13 @@ type ResellerLoss struct {
ResellerId int32
ResellerName string
Total float64
ProductLoss map[int32]*ProductLoss
Manager string
ProductLoss map[int32]ProductLoss
}
type ProductLoss struct {
ProductId int32
ProductName string
Loss float64
LossReason string
}
type ReportRes struct {
@ -39,14 +37,3 @@ type ProductSumReseller struct {
HistoryTwoNum 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 {
// 排序 ProductLoss
var products []*ProductLoss
var products []ProductLoss
for _, p := range reseller.ProductLoss {
products = append(products, p)
}
@ -311,164 +311,4 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d
return f.SaveAs(outputPath)
}
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)
}
// OfficialProductSumDeclineExcel

View File

@ -4,31 +4,16 @@ import (
config2 "ai_scheduler/internal/config"
"ai_scheduler/internal/entitys"
"context"
"strings"
"testing"
)
func Test_task(t *testing.T) {
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)
c := NewZltxOrderDetailTool(config2.ToolConfig{}, nil)
err :=
c.Execute(context.Background(), &entitys.Recognize{
Match: &entitys.Match{
Parameters: `{"order_number": 864086822064234497}`,
Parameters: `{"order_number": 859393216068067329}`,
},
Ext: []byte(`{"auth":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY3OTM0MTg1LCJuYmYiOjE3Njc5MjMzODUsImp0aSI6IjE3OSIsIlBob25lIjoiMTUwMDA0NTAxNTUiLCJVc2VyTmFtZSI6IjE1MDAwNDUwMTU1IiwiUmVhbE5hbWUiOiLliJjlvanlhpsiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9BRE1JTixWQ0xfU1lTVEVNLENSTV9BRE1JTixMU1hERFNfQURNSU4sTFNYRERTX1NZU1RFTSxNQVJLRVRJTkdTQUFTX1NVUEVSQURNSU4sU1RBVElTVElDQUxTWVNURU1fQURNSU4sUEhZU0lDQUxHT09EU19BRE1JTixNQVJLRVRJTkdTWVNURU1fU1VQRVIsUkVQT1JUQ0VOVEVSX0FETUlOLE1PTklUT1JfTUFESU4sWkxUWF9BRE1JTixaTFRYX09QRVJBVEUiLCJEaW5nVXNlcklkIjoiMTcxMTkzNTg3NTAzMjk5MzUifQ.d9z0S1Ia-PFAxhGstT055Amt8PI09bUHxG0_lba4UwvSomiTNCD-5DFdMkbZHwiDTlhVdBjcd1mDYFRRZXWMPoSnanubMBnRnuvTi8csch5nz1L9oWNo-HFyBE3lMw9-UJ5j84gz228_kcBsvRATT1Ixs9bnuaN9CDNz20c524llDt10C3cc8wLGMin4jWEMF4RNrf2oBZOFAahRYSJNeBmutIwRSIP1pMIAaUy_IkMCyOwK8JzgMnHJGLwIH_nxR9XZXlAN0FmrmtWVkRA2YUKvoDX5a5BCYmDVNqUbi_ZNuRPJH87Ia7_-UoyJu8Yq79jX0Qgsm6qJ4rX2nauneg"}`),
UserContent: &entitys.RecognizeUserContent{Text: "订单查询864086822064234497\n"},
})
if err != nil {
t.Log(err)
}
var res strings.Builder
for v := range ch {
res.WriteString(v.Content)
}
t.Log(res)
t.Log(err)
}

View File

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

Binary file not shown.