diff --git a/.gitignore b/.gitignore index 7a904e8..c953a6c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ docs cmd/server/wire_gen.go __debug* .bin/ +.idea/ cache/ \ No newline at end of file diff --git a/config/config.yaml b/config/config.yaml index 1396c00..5585caf 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -43,6 +43,12 @@ redis: db: driver: mysql source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +oss: + access_key: "LTAI5tGGZzjf3tvqWk8SQj2G" + secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq" + bucket: "attachment-public" + domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com" + endpoint: "https://oss-cn-hangzhou.aliyuncs.com" tools: zltxOrderDetail: @@ -118,6 +124,13 @@ eino_tools: # 货易通商品品牌查询 hytGoodsBrandSearch: base_url: "https://hyt.86698.cn/admin_upload/api/v1/goods/brand/list" + # == 电商充值系统 == + # 我们的商品统计 + rechargeStatisticsOursProduct: + base_url: "http://admin.lanseds.cn/admin/statistics/oursProduct" + api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY3MTc5ODgzLCJuYmYiOjE3NjcxNjkwODMsImp0aSI6IjEiLCJQaG9uZSI6IjE4MjAwMTYwMTQzIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MiwiR3JvdXBDb2RlcyI6IkxTWEREU19TWVNURU0sU1RBVElTVElDQUxTWVNURU1fQURNSU4sUEhZU0lDQUxHT09EU19BRE1JTiIsIkRpbmdVc2VySWQiOiIifQ.ELNF1Iv6yEwA12nCbXGKwXCw-F5Gq4GI2t2nqo1PlSkFdQ5Oz5s5NwV0RUXA66LxCggI-9IjBtFI1MvBHpvTHq9QRlm-HKzVTMcOBkEtKEfCCI6SPKVTAZyntTJlWPKG3u-CJUotT5YW0j2rU1VcpA7uGEiY7gs5VPUOZ80R1uGJ7HBSqVI2DRqar6STa1xryygdCjK7qamUM2d6aJ6r9VPTBt-JO6dkDdw3KHs3wl-PGM3wcbXHZ2aC18WFd_PxLmtjqErpxTEkdUBCnUHOSKDePG0henDJq71Nh3yRdRmY9VvszHMyIxJA2BVGPIPUT_Y5aewaaEMQVEjhiBnn-Q" + excel2pic: + base_url: "http://192.168.6.109:8010/api/v1/convert" dingtalk: api_key: "dingsbbntrkeiyazcfdg" diff --git a/config/config_env.yaml b/config/config_env.yaml index 21c1faa..4348d2d 100644 --- a/config/config_env.yaml +++ b/config/config_env.yaml @@ -43,6 +43,12 @@ redis: db: driver: mysql source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +oss: + access_key: "LTAI5tGGZzjf3tvqWk8SQj2G" + secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq" + bucket: "attachment-public" + domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com" + endpoint: "https://oss-cn-hangzhou.aliyuncs.com" tools: zltxOrderDetail: @@ -78,6 +84,7 @@ tools: # eino tool 配置 eino_tools: + # == 货易通 hyt == # 货易通商品上传 hytProductUpload: base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/supplier/batch/add/complete" @@ -104,6 +111,23 @@ eino_tools: # 货易通商品品牌查询 hytGoodsBrandSearch: base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/brand/list" + # == 报表分析 data analytics == + # 负利润分析列表 + daOursProductLoss: + base_url: "https://reportapi.1688sup.com/api/dataanalytics/statisOursProductLossSum" + # 利润同比排行榜 + daProfitRanking: + base_url: "https://reportapi.1688sup.com/api/dataanalytics/profitRankingSum" + # 销售同比分析列表 + daOfficialProduct: + base_url: "https://reportapi.1688sup.com/api/dataanalytics/statisOfficialProduct" + # == 电商充值系统 == + # 我们的商品统计 + rechargeStatisticsOursProduct: + base_url: "http://admin.1688sup.cn:8001/admin/statistics/oursProduct" + api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY3MDc3NzcwLCJuYmYiOjE3NjcwNzU5NzAsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.Nuw_aR6iSPmhhh9E5rhyTxHBsgWtaTZvbnc7SFTnUBJXTQvYahnk0LyZaVpsVw6FB3cU0F5xOdX3rmGyWyaiszWO6yi-o1oxGMXwhf39fMiWT2xUI6pAn9Ync8DmZ4tOMCNUTdEk4CaQFzrTwJs0c-VR4yW6LgoPmNPvUVZ-KwmusUpnPz5j9RsJItzIWE3bpGGsfB54e2UERcZdbo9BXxCZIBbpAYKBKdl73KuI8SNaXgKvGTrJ6hEN4ESpnbisJVwT5pp_kuChJlcfjHTHFsEf4RJDjN9gTrtDbBWZyY3OmO2ukqYAM7tZPs6TXJwvQGJQsFRVZUBGxS1nD_6DzQ" + excel2pic: + base_url: "http://192.168.6.109:8010/api/v1/convert" dingtalk: api_key: "dingsbbntrkeiyazcfdg" diff --git a/config/config_test.yaml b/config/config_test.yaml index 7ad9e71..19a7745 100644 --- a/config/config_test.yaml +++ b/config/config_test.yaml @@ -4,7 +4,7 @@ server: host: "0.0.0.0" ollama: - base_url: "http://host.docker.internal:11434" + base_url: "http://127.0.0.1:11434" model: "qwen3-coder:480b-cloud" generate_model: "qwen3-coder:480b-cloud" mapping_model: "deepseek-v3.2:cloud" @@ -43,6 +43,12 @@ redis: db: driver: mysql source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +oss: + access_key: "LTAI5tGGZzjf3tvqWk8SQj2G" + secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq" + bucket: "attachment-public" + domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com" + endpoint: "https://oss-cn-hangzhou.aliyuncs.com" tools: zltxOrderDetail: @@ -118,6 +124,13 @@ eino_tools: # 货易通商品品牌查询 hytGoodsBrandSearch: base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/brand/list" + # == 电商充值系统 == + # 我们的商品统计 + rechargeStatisticsOursProduct: + base_url: "http://admin.lanseds.cn/admin/statistics/oursProduct" + api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY3MTc5ODgzLCJuYmYiOjE3NjcxNjkwODMsImp0aSI6IjEiLCJQaG9uZSI6IjE4MjAwMTYwMTQzIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MiwiR3JvdXBDb2RlcyI6IkxTWEREU19TWVNURU0sU1RBVElTVElDQUxTWVNURU1fQURNSU4sUEhZU0lDQUxHT09EU19BRE1JTiIsIkRpbmdVc2VySWQiOiIifQ.ELNF1Iv6yEwA12nCbXGKwXCw-F5Gq4GI2t2nqo1PlSkFdQ5Oz5s5NwV0RUXA66LxCggI-9IjBtFI1MvBHpvTHq9QRlm-HKzVTMcOBkEtKEfCCI6SPKVTAZyntTJlWPKG3u-CJUotT5YW0j2rU1VcpA7uGEiY7gs5VPUOZ80R1uGJ7HBSqVI2DRqar6STa1xryygdCjK7qamUM2d6aJ6r9VPTBt-JO6dkDdw3KHs3wl-PGM3wcbXHZ2aC18WFd_PxLmtjqErpxTEkdUBCnUHOSKDePG0henDJq71Nh3yRdRmY9VvszHMyIxJA2BVGPIPUT_Y5aewaaEMQVEjhiBnn-Q" + excel2pic: + base_url: "http://192.168.6.109:8010/api/v1/convert" dingtalk: api_key: "dingsbbntrkeiyazcfdg" @@ -188,7 +201,3 @@ llm: temperature: 0.7 max_tokens: 4096 stream: true -#ding_talk_bots: -# public: -# client_id: "dingchg59zwwvmuuvldx", -# client_secret: "ZwetAnRiTQobNFVlNrshRagSMAJIFpBAepWkWI7on7Tt_o617KHtTjBLp8fQfplz", diff --git a/deploy.sh b/deploy.sh index 2e1e972..ba9952a 100644 --- a/deploy.sh +++ b/deploy.sh @@ -36,6 +36,9 @@ docker run -itd \ -e "OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434}" \ -e "MODE=${MODE}" \ -p 8090:8090 \ + -v ./cache:/app/cache \ + -v ./tmpl:/app/tmpl \ + -v ./go.mod:/app/go.mod \ "${CONTAINER_NAME}" ./server --config "./${CONFIG_FILE}" --bot "${BOT}" docker logs -f ${CONTAINER_NAME} \ No newline at end of file diff --git a/go.mod b/go.mod index fa8498d..374a7f3 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/alibabacloud-go/gateway-dingtalk v1.0.2 // indirect github.com/alibabacloud-go/openapi-util v0.1.1 // indirect github.com/alibabacloud-go/tea-xml v1.1.3 // indirect + github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible // indirect github.com/aliyun/credentials-go v1.4.6 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -95,6 +96,7 @@ require ( github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -120,6 +122,7 @@ require ( golang.org/x/net v0.46.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.5.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index b0d2c51..f101537 100644 --- a/go.sum +++ b/go.sum @@ -94,6 +94,8 @@ github.com/alibabacloud-go/tea-utils/v2 v2.0.6 h1:ZkmUlhlQbaDC+Eba/GARMPy6hKdCLi github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0= github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= @@ -392,6 +394,8 @@ github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWR github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -690,6 +694,8 @@ golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index 0cc4090..0475021 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -7,14 +7,19 @@ import ( "ai_scheduler/internal/data/constants" "ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/model" + "ai_scheduler/internal/domain/workflow/recharge" + "ai_scheduler/internal/domain/workflow/runtime" "ai_scheduler/internal/entitys" "ai_scheduler/internal/pkg/l_request" + "ai_scheduler/internal/pkg/utils_oss" "ai_scheduler/internal/tools" + "ai_scheduler/internal/tools/bbxt" "ai_scheduler/tmpl/dataTemp" "io" "net/http" "strconv" "time" + "unicode" "ai_scheduler/internal/config" "context" @@ -32,18 +37,20 @@ import ( // AiRouterBiz 智能路由服务 type DingTalkBotBiz struct { - do *do.Do - handle *do.Handle - botConfigImpl *impl.BotConfigImpl - replier *chatbot.ChatbotReplier - log log.Logger - dingTalkUser *dingtalk.User - botTools []model.AiBotTool - botGroupImpl *impl.BotGroupImpl - toolManager *tools.Manager - chatHis *impl.BotChatHisImpl - conf *config.Config - cardSend *dingtalk.SendCardClient + do *do.Do + handle *do.Handle + botConfigImpl *impl.BotConfigImpl + replier *chatbot.ChatbotReplier + log log.Logger + dingTalkUser *dingtalk.User + botTools []model.AiBotTool + botGroupImpl *impl.BotGroupImpl + toolManager *tools.Manager + chatHis *impl.BotChatHisImpl + conf *config.Config + cardSend *dingtalk.SendCardClient + ossClient *utils_oss.Client + workflowManager *runtime.Registry } // NewDingTalkBotBiz @@ -58,19 +65,23 @@ func NewDingTalkBotBiz( toolManager *tools.Manager, conf *config.Config, cardSend *dingtalk.SendCardClient, + ossClient *utils_oss.Client, + workflowManager *runtime.Registry, ) *DingTalkBotBiz { return &DingTalkBotBiz{ - do: do, - handle: handle, - botConfigImpl: botConfigImpl, - replier: chatbot.NewChatbotReplier(), - dingTalkUser: dingTalkUser, - botTools: tools.BootTools, - botGroupImpl: botGroupImpl, - toolManager: toolManager, - chatHis: chatHis, - conf: conf, - cardSend: cardSend, + do: do, + handle: handle, + botConfigImpl: botConfigImpl, + replier: chatbot.NewChatbotReplier(), + dingTalkUser: dingTalkUser, + botTools: tools.BootTools, + botGroupImpl: botGroupImpl, + toolManager: toolManager, + chatHis: chatHis, + conf: conf, + cardSend: cardSend, + ossClient: ossClient, + workflowManager: workflowManager, } } @@ -137,10 +148,14 @@ func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *enti func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { group, err := d.initGroup(ctx, requireData.Req.ConversationId, requireData.Req.ConversationTitle, requireData.Req.RobotCode) - + //宏 + err, isFinal := d.Macro(ctx, requireData, group) if err != nil { return } + if isFinal { + return + } requireData.ID = group.GroupID groupTools, err := d.getGroupTools(ctx, group) if err != nil { @@ -151,7 +166,60 @@ func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entit return } - return d.handleMatch(ctx, rec) + return d.handleMatch(ctx, rec, group) +} + +func (d *DingTalkBotBiz) Macro(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, group *model.AiBotGroup) (err error, isFinish bool) { + content := processString(requireData.Req.Text.Content) + + if strings.Contains(content, "[利润同比报表]商品修改:") { + // 提取冒号后的内容 + if parts := strings.SplitN(content, ":", 2); len(parts) == 2 { + itemInfo := strings.TrimSpace(parts[1]) + log.Infof("商品修改信息: %s", itemInfo) + group.ProductName = itemInfo + cond := builder.NewCond() + cond = cond.And(builder.Eq{"group_id": group.GroupID}) + err = d.botGroupImpl.UpdateByCond(&cond, group) + if err != nil { + entitys.ResText(requireData.Ch, "", fmt.Sprintf("修改失败:%v", err)) + } + entitys.ResText(requireData.Ch, "", "修改成功") + isFinish = true + return + } + } + + if strings.Contains(content, "[利润同比报表]商品列表") { + // 提取冒号后的内容 + if len(group.ProductName) == 0 { + entitys.ResText(requireData.Ch, "", "暂未设置") + } else { + entitys.ResText(requireData.Ch, "", group.ProductName) + isFinish = true + } + return + } + + return +} + +func processString(s string) string { + // 1. 替换中文逗号为英文逗号 + s = strings.ReplaceAll(s, ",", ",") + + // 2. 过滤控制字符(如 \n, \t, \r 等) + var result []rune + for _, char := range s { + // 判断是否是控制字符(ASCII < 32 或 = 127) + if !unicode.IsControl(char) { + // 如果需要完全移除 \n 和 \t,可以改成: + // if !unicode.IsControl(char) + result = append(result, char) + } + } + + return string(result) } func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string, robotCode string) (group *model.AiBotGroup, err error) { @@ -287,7 +355,7 @@ func (d *DingTalkBotBiz) getUserContent(msgType string, msgContent interface{}) return } -func (d *DingTalkBotBiz) handleMatch(ctx context.Context, rec *entitys.Recognize) (err error) { +func (d *DingTalkBotBiz) handleMatch(ctx context.Context, rec *entitys.Recognize, group *model.AiBotGroup) (err error) { if !rec.Match.IsMatch { if len(rec.Match.Chat) != 0 { @@ -312,6 +380,8 @@ func (d *DingTalkBotBiz) handleMatch(ctx context.Context, rec *entitys.Recognize switch constants.TaskType(pointTask.Type) { case constants.TaskTypeFunc: return d.handleTask(ctx, rec, pointTask) + case constants.TaskTypeReport: + return d.handleReport(ctx, rec, pointTask, group) case constants.TaskTypeCozeWorkflow: return d.handleCozeWorkflow(ctx, rec, pointTask) default: @@ -420,6 +490,75 @@ func handleCozeWorkflowEvents(ctx context.Context, resp coze.Stream[coze.Workflo fmt.Printf("done, log:%s\n", resp.Response().LogID()) } +func (d *DingTalkBotBiz) handleReport(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool, group *model.AiBotGroup) error { + var configData entitys.ConfigDataReport + err := json.Unmarshal([]byte(rec.Match.Parameters), &configData) + if err != nil { + return err + } + t, err := time.Parse(time.DateTime, configData.Time) + if err != nil { + log.Infof("时间识别失败:%s", configData.Time) + entitys.ResText(rec.Ch, "", "时间识别失败了!可以给我一份比较具体的时间吗,例如“2025-12-31 12:00,抱歉抱歉😀") + return nil + } + rep, err := bbxt.NewBbxtTools() + uploader := bbxt.NewUploader(d.ossClient) + if err != nil { + return err + } + var reports []*bbxt.ReportRes + switch rec.Match.Index { + case "report_loss_analysis": + repo, _err := rep.StatisOursProductLossSum(t) + if _err != nil { + return _err + } + reports = append(reports, repo...) + case "report_sales_analysis": + + repo, _err := rep.GetProfitRankingSum(t) + if _err != nil { + return _err + } + reports = append(reports, repo) + case "report_ranking_of_distributors": + product := strings.Split(group.ProductName, ",") + repo, _err := rep.GetStatisOfficialProductSum(t, product) + if _err != nil { + return _err + } + reports = append(reports, repo) + case "report_daily": + product := strings.Split(group.ProductName, ",") + repo, _err := rep.DailyReport(t, product, nil) + if _err != nil { + return _err + } + reports = append(reports, repo...) + default: + return fmt.Errorf("未找到的报表:%s", rec.Match.Index) + } + + for _, report := range reports { + err = uploader.Run(report) + if err != nil { + log.Error(err) + continue + } + + entitys.ResText(rec.Ch, "", fmt.Sprintf("%s![图片](%s)", report.Title, report.Url)) + //rec.Ch <- report.Title + //reportChan <- fmt.Sprintf("![图片](%s)", report.Url) + //err = d.SendReport(ctx, group, report) + //if err != nil { + // log.Error(err) + // continue + //} + } + return nil +} + func (d *DingTalkBotBiz) handleTask(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool) (err error) { var configData entitys.ConfigDataTool err = json.Unmarshal([]byte(task.Config), &configData) @@ -473,12 +612,83 @@ func (d *DingTalkBotBiz) HandleStreamRes(ctx context.Context, data *chatbot.BotC return } -func (d *DingTalkBotBiz) GetReportLists(ctx context.Context) (contentChan chan string, err error) { - contentChan = make(chan string, 10) - defer close(contentChan) - contentChan <- "截止今日23点利润亏损合计:127917.0866元,亏损500元以上的分销商和产品金额如下图:" - contentChan <- "![图片](https://lsxdmgoss.oss-cn-chengdu.aliyuncs.com/MarketingSaaS/image/V2/other/shanghu.png)" +func (d *DingTalkBotBiz) GetReportLists(ctx context.Context, group *model.AiBotGroup) (reports []*bbxt.ReportRes, err error) { + reportList, err := bbxt.NewBbxtTools() + if err != nil { + return + } + var product []string + if group.ProductName != "" { + product = strings.Split(group.ProductName, ",") + } + //[]string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"} + reports, err = reportList.DailyReport(time.Now(), product, d.ossClient) + if err != nil { + return + } + + // 追加电商充值系统统计 - 返回统一使用 []*bbxt.ReportRes + rechargeReports, err := d.rechargeDailyReport(ctx, time.Now(), product, d.ossClient) + if err != nil || len(rechargeReports) == 0 { + return + } + + reports = append(reports, rechargeReports...) + + return +} + +// rechargeDailyReport 获取电商充值系统统计报告 +func (d *DingTalkBotBiz) rechargeDailyReport(ctx context.Context, now time.Time, productNames []string, ossClient *utils_oss.Client) (reports []*bbxt.ReportRes, err error) { + defer func() { + if err := recover(); err != nil { + log.Error(err) + } + }() + + workflowId := recharge.WorkflowIDStatisticsOursProduct + args := &runtime.WorkflowArgs{ + Args: map[string]any{ + "product_names": productNames, + "now": now, + }, + } + res, err := d.workflowManager.Invoke(ctx, workflowId, args) + if err != nil { + return + } + + log.Infof("imgUrl: %s", res["url"].(string)) + + reports = []*bbxt.ReportRes{ + { + ReportName: "我们的商品统计(电商充值系统)", + Title: fmt.Sprintf("%s 电商充值系统我们的商品统计", now.Format("2006-01-02")), + Path: res["path"].(string), + Url: res["url"].(string), + Data: res["data"].([][]string), + Desc: res["desc"].(string), + }, + } + + return +} + +func (d *DingTalkBotBiz) SendReport(ctx context.Context, groupInfo *model.AiBotGroup, report *bbxt.ReportRes) (err error) { + + reportChan := make(chan string, 10) + defer close(reportChan) + reportChan <- report.Title + reportChan <- fmt.Sprintf("![图片](%s)", report.Url) + err = d.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{ + RobotCode: groupInfo.RobotCode, + ConversationType: constants.ConversationTypeGroup, + ConversationId: groupInfo.ConversationID, + Text: chatbot.BotCallbackDataTextModel{ + Content: report.ReportName, + }, + }, reportChan) return } @@ -555,7 +765,7 @@ func (d *DingTalkBotBiz) SaveHis(ctx context.Context, requireData *entitys.Requi } func (d *DingTalkBotBiz) defaultPrompt() string { - + now := time.Now().Format(time.DateTime) return `[system] 你是一个智能路由系统,核心职责是 **精准解析用户意图并路由至对应任务模块**。请严格遵循以下规则: [rule] 1. **返回格式**: @@ -578,5 +788,6 @@ func (d *DingTalkBotBiz) defaultPrompt() string { 4. 格式强制要求: -所有字段值必须是**字符串**(包括 confidence)。 --parameters 必须是 **转义后的 JSON 字符串**(如 "{\"product_name\": \"京东月卡\"}")。` +-parameters 必须是 **转义后的 JSON 字符串**(如 "{\"product_name\": \"京东月卡\"}")。 +当前时间:` + now + `,所有的时间识别精确到秒` } diff --git a/internal/biz/do/handle.go b/internal/biz/do/handle.go index a1a9dcf..afc4b2b 100644 --- a/internal/biz/do/handle.go +++ b/internal/biz/do/handle.go @@ -396,7 +396,7 @@ func (r *Handle) handleEinoWorkflow(ctx context.Context, rec *entitys.Recognize, // 工作流内部输出 workflowId := task.Index - _, err = r.workflowManager.Invoke(ctx, workflowId, rec) + _, err = r.workflowManager.Invoke(ctx, workflowId, &runtime.WorkflowArgs{Recognize: rec}) if err != nil { return err } diff --git a/internal/config/config.go b/internal/config/config.go index 35f2115..132fb23 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,8 +3,9 @@ package config import ( "ai_scheduler/pkg" "fmt" - "github.com/spf13/viper" "time" + + "github.com/spf13/viper" ) // Config 应用配置 @@ -19,11 +20,11 @@ type Config struct { Logging LoggingConfig `mapstructure:"logging"` Redis Redis `mapstructure:"redis"` DB DB `mapstructure:"db"` + Oss Oss `mapstructure:"oss"` DefaultPrompt SysPrompt `mapstructure:"default_prompt"` PermissionConfig PermissionConfig `mapstructure:"permissionConfig"` LLM LLM `mapstructure:"llm"` - // DingTalkBots map[string]*DingTalkBot `mapstructure:"ding_talk_bots"` - Dingtalk DingtalkConfig `mapstructure:"dingtalk"` + Dingtalk DingtalkConfig `mapstructure:"dingtalk"` } type SysPrompt struct { @@ -136,6 +137,15 @@ type DB struct { IsDebug bool `mapstructure:"isDebug"` } +// Oss 阿里云OSS配置 +type Oss struct { + AccessKey string `mapstructure:"access_key"` + SecretKey string `mapstructure:"secret_key"` + Bucket string `mapstructure:"bucket"` + Domain string `mapstructure:"domain"` + Endpoint string `mapstructure:"endpoint"` +} + // ToolsConfig 工具配置 type ToolsConfig struct { Weather ToolConfig `mapstructure:"weather"` @@ -188,6 +198,18 @@ type EinoToolsConfig struct { HytGoodsCategorySearch ToolConfig `mapstructure:"hytGoodsCategorySearch"` // 货易通商品品牌查询 HytGoodsBrandSearch ToolConfig `mapstructure:"hytGoodsBrandSearch"` + // 负利润分析列表、 详情 + DaOursProductLoss ToolConfig `mapstructure:"daOursProductLoss"` + // 利润同比排行榜 + DaProfitRanking ToolConfig `mapstructure:"daProfitRanking"` + // 销售同比分析列表 + DaOfficialProduct ToolConfig `mapstructure:"daOfficialProduct"` + // 销售同比下滑详情 + DaOfficialProductDecline ToolConfig `mapstructure:"daOfficialProductDecline"` + // 我们的商品统计 + RechargeStatisticsOursProduct ToolConfig `mapstructure:"rechargeStatisticsOursProduct"` + // Excel 转图片 + Excel2Pic ToolConfig `mapstructure:"excel2Pic"` } // LoggingConfig 日志配置 diff --git a/internal/data/constants/const.go b/internal/data/constants/const.go index b3c6ef0..f5ac22a 100644 --- a/internal/data/constants/const.go +++ b/internal/data/constants/const.go @@ -17,6 +17,7 @@ const ( TaskTypeBot TaskType = 4 TaskTypeEinoWorkflow TaskType = 5 // eino 工作流 TaskTypeCozeWorkflow TaskType = 6 // coze 工作流 + TaskTypeReport TaskType = 7 //报表 ) type UseFul int32 diff --git a/internal/data/model/ai_bot_group.gen.go b/internal/data/model/ai_bot_group.gen.go index 80c50d1..3dec7b5 100644 --- a/internal/data/model/ai_bot_group.gen.go +++ b/internal/data/model/ai_bot_group.gen.go @@ -12,14 +12,15 @@ const TableNameAiBotGroup = "ai_bot_group" // AiBotGroup mapped from table type AiBotGroup struct { - GroupID int32 `gorm:"column:group_id;primaryKey;autoIncrement:true" json:"group_id"` - ConversationID string `gorm:"column:conversation_id;not null;comment:会话ID" json:"conversation_id"` // 会话ID - RobotCode string `gorm:"column:robot_code;not null;comment:绑定机器人code" json:"robot_code"` // 绑定机器人code - Title string `gorm:"column:title;not null;comment:群名称" json:"title"` // 群名称 - ToolList string `gorm:"column:tool_list;not null;comment:开通工具列表" json:"tool_list"` // 开通工具列表 - Status int32 `gorm:"column:status;not null;default:1" json:"status"` - DeleteAt *time.Time `gorm:"column:delete_at" json:"delete_at"` - CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` + GroupID int32 `gorm:"column:group_id;primaryKey;autoIncrement:true" json:"group_id"` + ConversationID string `gorm:"column:conversation_id;not null;comment:会话ID" json:"conversation_id"` // 会话ID + RobotCode string `gorm:"column:robot_code;not null;comment:绑定机器人code" json:"robot_code"` // 绑定机器人code + Title string `gorm:"column:title;not null;comment:群名称" json:"title"` // 群名称 + ToolList string `gorm:"column:tool_list;not null;comment:开通工具列表" json:"tool_list"` // 开通工具列表 + ProductName string `gorm:"column:product_name;not null;comment:针对报表商品筛选快速实现,后期优化" json:"product_name"` // 针对报表商品筛选快速实现,后期优化 + Status int32 `gorm:"column:status;not null;default:1" json:"status"` + DeleteAt time.Time `gorm:"column:delete_at" json:"delete_at"` + CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` } // TableName AiBotGroup's table name diff --git a/internal/data/model/ai_bot_tools.gen.go b/internal/data/model/ai_bot_tools.gen.go index f57889b..0ea3302 100644 --- a/internal/data/model/ai_bot_tools.gen.go +++ b/internal/data/model/ai_bot_tools.gen.go @@ -13,16 +13,16 @@ const TableNameAiBotTool = "ai_bot_tools" // AiBotTool mapped from table type AiBotTool struct { ToolID int32 `gorm:"column:tool_id;primaryKey;autoIncrement:true" json:"tool_id"` - PermissionType int32 `gorm:"column:permission_type;not null;comment:类型,1为公共工具,不需要进行权限管理,反之则为2" json:"permission_type"` // 类型,1为公共工具,不需要进行权限管理,反之则为2 - Config string `gorm:"column:config;not null;comment:类型下所需路由以及参数" json:"config"` // 类型下所需路由以及参数 + PermissionType int32 `gorm:"column:permission_type;not null;default:1;comment:类型,1为公共工具,不需要进行权限管理,反之则为2" json:"permission_type"` // 类型,1为公共工具,不需要进行权限管理,反之则为2 + Config string `gorm:"column:config;comment:类型下所需路由以及参数" json:"config"` // 类型下所需路由以及参数 Type int32 `gorm:"column:type;not null;default:3" json:"type"` - Name string `gorm:"column:name;not null;default:1;comment:工具名称" json:"name"` // 工具名称 + Name string `gorm:"column:name;not null;comment:工具名称" json:"name"` // 工具名称 Index string `gorm:"column:index;not null;comment:索引" json:"index"` // 索引 Desc string `gorm:"column:desc;not null;comment:工具描述" json:"desc"` // 工具描述 TempPrompt string `gorm:"column:temp_prompt;not null;comment:提示词模板" json:"temp_prompt"` // 提示词模板 CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP" json:"updated_at"` - Status int32 `gorm:"column:status;not null" json:"status"` + Status int32 `gorm:"column:status;not null;default:1" json:"status"` DeleteAt time.Time `gorm:"column:delete_at" json:"delete_at"` } diff --git a/internal/domain/repo/repos.go b/internal/domain/repo/repos.go index 40ba3de..0350d95 100644 --- a/internal/domain/repo/repos.go +++ b/internal/domain/repo/repos.go @@ -1,17 +1,21 @@ package repo import ( + "ai_scheduler/internal/config" "ai_scheduler/internal/data/impl" - "ai_scheduler/utils" + "ai_scheduler/internal/pkg/utils_oss" ) // Repos 聚合所有 Repository type Repos struct { - Session SessionRepo + Session SessionRepo + OssClient *utils_oss.Client } -func NewRepos(sessionImpl *impl.SessionImpl, rdb *utils.Rdb) *Repos { +func NewRepos(sessionImpl *impl.SessionImpl, cfg *config.Config) *Repos { + ossClient, _ := utils_oss.NewClient(cfg) return &Repos{ - Session: NewSessionAdapter(sessionImpl), + Session: NewSessionAdapter(sessionImpl), + OssClient: ossClient, } } diff --git a/internal/domain/tools/common/excel_generator/client.go b/internal/domain/tools/common/excel_generator/client.go new file mode 100644 index 0000000..f2cd74f --- /dev/null +++ b/internal/domain/tools/common/excel_generator/client.go @@ -0,0 +1,77 @@ +package excel_generator + +import ( + "fmt" + + "github.com/go-kratos/kratos/v2/log" + "github.com/xuri/excelize/v2" +) + +// Client Excel 生成器 +type Client struct{} + +func New() *Client { + return &Client{} +} + +// Call 根据模板和数据生成 Excel 字节流 +func (g *Client) Call(req *ExcelGeneratorRequest) ([]byte, error) { + if req.StartRow <= 0 { + req.StartRow = 2 + } + if req.StyleRow <= 0 { + req.StyleRow = 2 + } + + f, err := excelize.OpenFile(req.TemplatePath) + if err != nil { + return nil, err + } + defer f.Close() + + sheet := f.GetSheetName(0) + + // 若提供标题,替换第一行表格标题 + if len(req.Title) > 0 { + f.SetCellValue(sheet, "A1", req.Title) + } + + // 获取样式和行高 + styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", req.StyleRow)) + if err != nil { + log.Errorf("获取样式失败: %v", err) + styleID = 0 + } + rowHeight, err := f.GetRowHeight(sheet, req.StyleRow) + if err != nil { + log.Errorf("获取行高失败: %v", err) + rowHeight = 31 // 默认高度 + } + + row := req.StartRow + for i, item := range req.ExcelData { + currentRow := row + i + + // 设置行高 + f.SetRowHeight(sheet, currentRow, rowHeight) + + // 填充数据 + for col, value := range item { + cell := fmt.Sprintf("%c%d", 'A'+col, currentRow) + f.SetCellValue(sheet, cell, value) + } + + // 设置样式 + if styleID != 0 { + endCol := 'A' + len(item) - 1 + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("%c%d", endCol, currentRow), styleID) + } + } + + buf, err := f.WriteToBuffer() + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/internal/domain/tools/common/excel_generator/type.go b/internal/domain/tools/common/excel_generator/type.go new file mode 100644 index 0000000..3a1b7b4 --- /dev/null +++ b/internal/domain/tools/common/excel_generator/type.go @@ -0,0 +1,9 @@ +package excel_generator + +type ExcelGeneratorRequest struct { + TemplatePath string // 模板文件路径 + ExcelData [][]string // 二维字符串数组 + StartRow int // 数据填充起始行 (默认 2) + StyleRow int // 样式参考行 (默认 2) + Title string // 表格标题(仅替换) +} diff --git a/internal/domain/tools/common/image_converter/client.go b/internal/domain/tools/common/image_converter/client.go new file mode 100644 index 0000000..2de57d2 --- /dev/null +++ b/internal/domain/tools/common/image_converter/client.go @@ -0,0 +1,66 @@ +package image_converter + +import ( + "ai_scheduler/internal/config" + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" +) + +// Client 图片转换器 +type Client struct { + cfg config.ToolConfig +} + +func New(cfg config.ToolConfig) *Client { + return &Client{ + cfg: cfg, + } +} + +// Call 将 Excel 文件转换为图片 +func (c *Client) Call(filename string, fileBytes []byte, scale int) ([]byte, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("file", filename) + if err != nil { + return nil, err + } + if _, err = io.Copy(part, bytes.NewReader(fileBytes)); err != nil { + return nil, err + } + + // 添加 scale 参数 + if scale <= 0 { + scale = 2 + } + if err = writer.WriteField("scale", fmt.Sprintf("%d", scale)); err != nil { + return nil, err + } + + if err = writer.Close(); err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.cfg.BaseURL, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("excel2pic service returned status: %s", resp.Status) + } + + return io.ReadAll(resp.Body) +} diff --git a/internal/domain/tools/data_analytics/official_product/client.go b/internal/domain/tools/data_analytics/official_product/client.go new file mode 100644 index 0000000..bffb6be --- /dev/null +++ b/internal/domain/tools/data_analytics/official_product/client.go @@ -0,0 +1,70 @@ +package official_product + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/l_request" + "context" + "encoding/json" + "fmt" + "strings" +) + +type Client struct { + cfg config.ToolConfig +} + +func New(cfg config.ToolConfig) *Client { + return &Client{ + cfg: cfg, + } +} + +// Call 调用销售同比分析接口 +func (c *Client) Call(ctx context.Context, req OfficialProductRequest) (*OfficialProductData, error) { + // 构建 URL 参数 + var queryParams []string + + if req.Page > 0 { + queryParams = append(queryParams, fmt.Sprintf("page=%d", req.Page)) + } + if req.Limit > 0 { + queryParams = append(queryParams, fmt.Sprintf("limit=%d", req.Limit)) + } + + for _, pid := range req.OfficialProductIds { + queryParams = append(queryParams, fmt.Sprintf("official_product_id[]=%s", pid)) + } + + for _, t := range req.Ct { + queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+"))) + } + + queryString := strings.Join(queryParams, "&") + fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString) + + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey), + } + + reqObj := l_request.Request{ + Method: "GET", + Url: fullURL, + Headers: headers, + } + + res, err := reqObj.Send() + if err != nil { + return nil, fmt.Errorf("请求失败,err: %v", err) + } + + var resData OfficialProductResponse + if err := json.Unmarshal([]byte(res.Text), &resData); err != nil { + return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text) + } + + if resData.Code != 200 { + return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg) + } + + return &resData.Data, nil +} diff --git a/internal/domain/tools/data_analytics/official_product/types.go b/internal/domain/tools/data_analytics/official_product/types.go new file mode 100644 index 0000000..a0890b5 --- /dev/null +++ b/internal/domain/tools/data_analytics/official_product/types.go @@ -0,0 +1,31 @@ +package official_product + +// OfficialProductRequest 销售同比分析请求参数 +type OfficialProductRequest struct { + Page int `json:"page"` // 页码 + Limit int `json:"limit"` // 每页条数 + OfficialProductIds []string `json:"official_product_ids"` // 官方产品ID列表 + Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间] +} + +// OfficialProductResponse 销售同比分析响应结构 +type OfficialProductResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data OfficialProductData `json:"data"` +} + +type OfficialProductData struct { + OfficialProductSum []OfficialProductItem `json:"officialProductSum"` + DataCount int `json:"dataCount"` +} + +type OfficialProductItem struct { + OfficialProductId int `json:"officialProductId"` + OfficialProductName string `json:"officialProductName"` + CurrentNum int `json:"currentNum"` + HistoryOneNum int `json:"historyOneNum"` + HistoryTwoNum int `json:"historyTwoNum"` + HistoryOneDiff int `json:"historyOneDiff"` + HistoryTwoDiff int `json:"historyTwoDiff"` +} diff --git a/internal/domain/tools/data_analytics/official_product_decline/client.go b/internal/domain/tools/data_analytics/official_product_decline/client.go new file mode 100644 index 0000000..9ce9499 --- /dev/null +++ b/internal/domain/tools/data_analytics/official_product_decline/client.go @@ -0,0 +1,79 @@ +package official_product_decline + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/l_request" + "context" + "encoding/json" + "fmt" + "strings" +) + +type Client struct { + cfg config.ToolConfig +} + +func New(cfg config.ToolConfig) *Client { + return &Client{ + cfg: cfg, + } +} + +// Call 调用销售同比下滑详情接口 +func (c *Client) Call(ctx context.Context, req OfficialProductDeclineRequest) (*OfficialProductDeclineData, error) { + // 构建 URL 参数 + var queryParams []string + + if req.Page > 0 { + queryParams = append(queryParams, fmt.Sprintf("page=%d", req.Page)) + } + if req.Limit > 0 { + queryParams = append(queryParams, fmt.Sprintf("limit=%d", req.Limit)) + } + if req.DownwardValue > 0 { + queryParams = append(queryParams, fmt.Sprintf("downwardValue=%d", req.DownwardValue)) + } + // showTime 可能是 0,所以这里不做 > 0 判断,如果业务默认是 0 可以忽略,或者根据实际需求 + // 假设始终传递该参数如果已设置 + queryParams = append(queryParams, fmt.Sprintf("showTime=%d", req.ShowTime)) + + for _, pid := range req.OfficialProductIds { + queryParams = append(queryParams, fmt.Sprintf("official_product_id[]=%s", pid)) + } + + for _, t := range req.Ct { + queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+"))) + } + + queryString := strings.Join(queryParams, "&") + fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString) + + headers := map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36", + "Accept": "application/json, text/plain, */*", + "Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey), + "Accept-Language": "zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7", + } + + reqObj := l_request.Request{ + Method: "GET", + Url: fullURL, + Headers: headers, + } + + res, err := reqObj.Send() + if err != nil { + return nil, fmt.Errorf("请求失败,err: %v", err) + } + + var resData OfficialProductDeclineResponse + if err := json.Unmarshal([]byte(res.Text), &resData); err != nil { + return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text) + } + + if resData.Code != 200 { + return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg) + } + + return &resData.Data, nil +} diff --git a/internal/domain/tools/data_analytics/official_product_decline/types.go b/internal/domain/tools/data_analytics/official_product_decline/types.go new file mode 100644 index 0000000..3fa598d --- /dev/null +++ b/internal/domain/tools/data_analytics/official_product_decline/types.go @@ -0,0 +1,35 @@ +package official_product_decline + +// OfficialProductDeclineRequest 销售同比下滑详情请求参数 +type OfficialProductDeclineRequest struct { + Page int `json:"page"` // 页码 + Limit int `json:"limit"` // 每页条数 + Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间] + OfficialProductIds []string `json:"official_product_ids"` // 官方产品ID列表 + DownwardValue int `json:"downward_value"` // 下滑值 + ShowTime int `json:"show_time"` // 是否显示时间 (0:不显示, 1:显示) +} + +// OfficialProductDeclineResponse 销售同比下滑详情响应结构 +type OfficialProductDeclineResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data OfficialProductDeclineData `json:"data"` +} + +type OfficialProductDeclineData struct { + OfficialProductSumDecline []OfficialProductDeclineItem `json:"officialProductSumDecline"` + DataCount int `json:"dataCount"` +} + +type OfficialProductDeclineItem struct { + ResellerId int `json:"resellerId"` + OfficialProductId int `json:"officialProductId"` + OfficialProductName string `json:"officialProductName"` + ResellerName string `json:"resellerName"` + CurrentNum int `json:"currentNum"` + HistoryOneNum int `json:"historyOneNum"` + HistoryTwoNum int `json:"historyTwoNum"` + HistoryOneDiff int `json:"historyOneDiff"` + HistoryTwoDiff int `json:"historyTwoDiff"` +} diff --git a/internal/domain/tools/data_analytics/ours_product_loss/client.go b/internal/domain/tools/data_analytics/ours_product_loss/client.go new file mode 100644 index 0000000..9e145b3 --- /dev/null +++ b/internal/domain/tools/data_analytics/ours_product_loss/client.go @@ -0,0 +1,78 @@ +package ours_product_loss + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/l_request" + "context" + "encoding/json" + "fmt" + "strings" +) + +type Client struct { + cfg config.ToolConfig +} + +func New(cfg config.ToolConfig) *Client { + return &Client{ + cfg: cfg, + } +} + +// Call 调用负利润分析接口 +// 支持列表查询和详情查询 +// 列表查询:提供 page, limit, ct[] +// 详情查询:提供 ct[], resellerId +func (c *Client) Call(ctx context.Context, req OursProductLossRequest) (*OursProductLossData, error) { + // 处理数组参数 ct[] + // util.StructToMap 通常不支持数组到 url query array 的转换,这里手动处理查询字符串 + // 或者如果 l_request 支持 map 中的 slice 自动转换最好,假设不支持需手动拼接 + + // 构建 URL 参数 + var queryParams []string + + if req.Page > 0 { + queryParams = append(queryParams, fmt.Sprintf("page=%d", req.Page)) + } + if req.Limit > 0 { + queryParams = append(queryParams, fmt.Sprintf("limit=%d", req.Limit)) + } + if req.ResellerId != "" { + queryParams = append(queryParams, fmt.Sprintf("resellerId=%s", req.ResellerId)) + } + + for _, t := range req.Ct { + // URL 编码处理,这里简单处理,实际应使用 url.QueryEscape + // 假设输入已经是合法的格式 + queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+"))) + } + + queryString := strings.Join(queryParams, "&") + fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString) + + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey), + } + + reqObj := l_request.Request{ + Method: "GET", + Url: fullURL, + Headers: headers, + } + + res, err := reqObj.Send() + if err != nil { + return nil, fmt.Errorf("请求失败,err: %v", err) + } + + var resData OursProductLossResponse + if err := json.Unmarshal([]byte(res.Text), &resData); err != nil { + return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text) + } + + if resData.Code != 200 { + return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg) + } + + return &resData.Data, nil +} diff --git a/internal/domain/tools/data_analytics/ours_product_loss/types.go b/internal/domain/tools/data_analytics/ours_product_loss/types.go new file mode 100644 index 0000000..ceb006a --- /dev/null +++ b/internal/domain/tools/data_analytics/ours_product_loss/types.go @@ -0,0 +1,29 @@ +package ours_product_loss + +// OursProductLossRequest 负利润分析请求参数 +type OursProductLossRequest struct { + Page int `json:"page"` // 页码 + Limit int `json:"limit"` // 每页条数 + Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间] + ResellerId string `json:"reseller_id"` // 经销商ID (详情查询时使用) +} + +// OursProductLossResponse 负利润分析响应结构 +type OursProductLossResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data OursProductLossData `json:"data"` +} + +type OursProductLossData struct { + List []OursProductLossItem `json:"list"` + DataCount int `json:"dataCount"` +} + +type OursProductLossItem struct { + OursProductId int `json:"oursProductId"` + OursProductName string `json:"oursProductName"` + ResellerName string `json:"resellerName"` + ResellerId int `json:"resellerId"` + Loss float64 `json:"loss"` +} diff --git a/internal/domain/tools/data_analytics/profit_ranking/client.go b/internal/domain/tools/data_analytics/profit_ranking/client.go new file mode 100644 index 0000000..251c59e --- /dev/null +++ b/internal/domain/tools/data_analytics/profit_ranking/client.go @@ -0,0 +1,63 @@ +package profit_ranking + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/l_request" + "context" + "encoding/json" + "fmt" + "strings" +) + +type Client struct { + cfg config.ToolConfig +} + +func New(cfg config.ToolConfig) *Client { + return &Client{ + cfg: cfg, + } +} + +// Call 调用利润同比排行榜接口 +func (c *Client) Call(ctx context.Context, req ProfitRankingRequest) (*ProfitRankingData, error) { + // 构建 URL 参数 + var queryParams []string + + for _, t := range req.Ct { + queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+"))) + } + + for _, rid := range req.ResellerIds { + queryParams = append(queryParams, fmt.Sprintf("resellerIds[]=%s", rid)) + } + + queryString := strings.Join(queryParams, "&") + fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString) + + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey), + } + + reqObj := l_request.Request{ + Method: "GET", + Url: fullURL, + Headers: headers, + } + + res, err := reqObj.Send() + if err != nil { + return nil, fmt.Errorf("请求失败,err: %v", err) + } + + var resData ProfitRankingResponse + if err := json.Unmarshal([]byte(res.Text), &resData); err != nil { + return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text) + } + + if resData.Code != 200 { + return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg) + } + + return &resData.Data, nil +} diff --git a/internal/domain/tools/data_analytics/profit_ranking/client_test.go b/internal/domain/tools/data_analytics/profit_ranking/client_test.go new file mode 100644 index 0000000..ad5bc29 --- /dev/null +++ b/internal/domain/tools/data_analytics/profit_ranking/client_test.go @@ -0,0 +1,27 @@ +package profit_ranking + +import ( + "ai_scheduler/internal/config" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClient_Call(t *testing.T) { + cfg := config.ToolConfig{ + BaseURL: "http://test.analysis.com/api/dataanalytics/profitRankingSum", + APIKey: "test_jwt_token", + } + + client := New(cfg) + assert.NotNil(t, client) + + req := ProfitRankingRequest{ + Ct: []string{"2025-01-01 00:00:00", "2025-01-01 23:59:59"}, + ResellerIds: []string{"1001", "1002"}, + } + + t.Logf("Testing Call with req: %+v", req) + // _, err := client.Call(context.Background(), req) + // assert.Error(t, err) +} diff --git a/internal/domain/tools/data_analytics/profit_ranking/types.go b/internal/domain/tools/data_analytics/profit_ranking/types.go new file mode 100644 index 0000000..d0d8552 --- /dev/null +++ b/internal/domain/tools/data_analytics/profit_ranking/types.go @@ -0,0 +1,29 @@ +package profit_ranking + +// ProfitRankingRequest 利润同比排行请求参数 +type ProfitRankingRequest struct { + Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间] + ResellerIds []string `json:"reseller_ids"` // 经销商ID列表 +} + +// ProfitRankingResponse 利润同比排行响应结构 +type ProfitRankingResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data ProfitRankingData `json:"data"` +} + +type ProfitRankingData struct { + List []ProfitRankingItem `json:"list"` + DataCount int `json:"dataCount"` +} + +type ProfitRankingItem struct { + ResellerId string `json:"resellerId"` + ResellerName string `json:"resellerName"` + CurrentProfit float64 `json:"currentProfit"` + HistoryOneProfit float64 `json:"historyOneProfit"` + HistoryTwoProfit float64 `json:"historyTwoProfit"` + HistoryOneDiff float64 `json:"historyOneDiff"` + HistoryTwoDiff float64 `json:"historyTwoDiff"` +} diff --git a/internal/domain/tools/recharge/statistics_ours_product/client.go b/internal/domain/tools/recharge/statistics_ours_product/client.go new file mode 100644 index 0000000..a9706b9 --- /dev/null +++ b/internal/domain/tools/recharge/statistics_ours_product/client.go @@ -0,0 +1,73 @@ +package statistics_ours_product + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/l_request" + "context" + "encoding/json" + "fmt" + "strings" + "time" +) + +type Client struct { + cfg config.ToolConfig +} + +func New(cfg config.ToolConfig) *Client { + return &Client{ + cfg: cfg, + } +} + +// Call 调用我们的商品统计接口 +func (c *Client) Call(ctx context.Context, req StatisticsOursProductRequest) ([]StatisticsOursProductItem, error) { + // 构建 URL 参数 + var queryParams []string + + if req.Page > 0 { + queryParams = append(queryParams, fmt.Sprintf("page=%d", req.Page)) + } + if req.Limit > 0 { + queryParams = append(queryParams, fmt.Sprintf("limit=%d", req.Limit)) + } + if req.OursProductId != "" { + queryParams = append(queryParams, fmt.Sprintf("ours_product_id=%s", req.OursProductId)) + } + + for _, s := range req.Serial { + queryParams = append(queryParams, fmt.Sprintf("serial[]=%s", s)) + } + + // 添加 timestamp + queryParams = append(queryParams, fmt.Sprintf("timestamp=%d", time.Now().UnixMilli())) + + queryString := strings.Join(queryParams, "&") + fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString) + + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey), + } + + reqObj := l_request.Request{ + Method: "GET", + Url: fullURL, + Headers: headers, + } + + res, err := reqObj.Send() + if err != nil { + return nil, fmt.Errorf("请求失败,err: %v", err) + } + + var resData StatisticsOursProductResponse + if err := json.Unmarshal([]byte(res.Text), &resData); err != nil { + return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text) + } + + if resData.Code != 200 { + return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg) + } + + return resData.Data, nil +} diff --git a/internal/domain/tools/recharge/statistics_ours_product/client_test.go b/internal/domain/tools/recharge/statistics_ours_product/client_test.go new file mode 100644 index 0000000..4789fba --- /dev/null +++ b/internal/domain/tools/recharge/statistics_ours_product/client_test.go @@ -0,0 +1,33 @@ +package statistics_ours_product + +import ( + "ai_scheduler/internal/config" + "context" + "testing" +) + +func TestClient_Call(t *testing.T) { + cfg := config.ToolConfig{ + BaseURL: "http://admin.1688sup.cn:8001/admin/statistics/oursProduct", + APIKey: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY3MDc3NzcwLCJuYmYiOjE3NjcwNzU5NzAsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.Nuw_aR6iSPmhhh9E5rhyTxHBsgWtaTZvbnc7SFTnUBJXTQvYahnk0LyZaVpsVw6FB3cU0F5xOdX3rmGyWyaiszWO6yi-o1oxGMXwhf39fMiWT2xUI6pAn9Ync8DmZ4tOMCNUTdEk4CaQFzrTwJs0c-VR4yW6LgoPmNPvUVZ-KwmusUpnPz5j9RsJItzIWE3bpGGsfB54e2UERcZdbo9BXxCZIBbpAYKBKdl73KuI8SNaXgKvGTrJ6hEN4ESpnbisJVwT5pp_kuChJlcfjHTHFsEf4RJDjN9gTrtDbBWZyY3OmO2ukqYAM7tZPs6TXJwvQGJQsFRVZUBGxS1nD_6DzQ", + } + + client := New(cfg) + + req := StatisticsOursProductRequest{ + Page: 1, + Limit: 100, + Serial: []string{"2025122300", "2025123123"}, + } + + t.Logf("Testing Call with req: %+v", req) + + // 由于没有真实的后端环境和 Token,这里注释掉实际调用 + // 在开发环境中,你可以取消注释并填入有效的 Token 进行测试 + res, err := client.Call(context.Background(), req) + if err != nil { + t.Logf("Call failed: %v", err) + } else { + t.Logf("Call success, resp: %+v", res) + } +} diff --git a/internal/domain/tools/recharge/statistics_ours_product/types.go b/internal/domain/tools/recharge/statistics_ours_product/types.go new file mode 100644 index 0000000..3d8e86d --- /dev/null +++ b/internal/domain/tools/recharge/statistics_ours_product/types.go @@ -0,0 +1,30 @@ +package statistics_ours_product + +// StatisticsOursProductRequest 我们的商品统计请求参数 +type StatisticsOursProductRequest struct { + Page int `json:"page"` // 页码 + Limit int `json:"limit"` // 每页条数 + Serial []string `json:"serial"` // 流水号范围 (通常是日期格式,如 YYYYMMDDHH) + OursProductId string `json:"ours_product_id"` // 我们的商品ID (可选) +} + +// StatisticsOursProductResponse 我们的商品统计响应结构 +// 注意:接口直接返回数组,而不是包含在 data 字段中的对象 +type StatisticsOursProductResponse struct { + Code int `json:"code"` + Msg string `json:"error"` // 接口返回字段名为 error + Data []StatisticsOursProductItem `json:"data"` // data 是一个数组 +} + +type StatisticsOursProductItem struct { + OursProductId int `json:"ours_product_id"` + ResellerId int `json:"reseller_id"` + TotalPrice any `json:"total_price"` + Count any `json:"count"` + SuccessCount any `json:"success_count"` + SuccessPrice any `json:"success_price"` + FailCount any `json:"fail_count"` + FailPrice any `json:"fail_price"` + Profit any `json:"profit"` + OursProductName string `json:"ours_product_name"` +} diff --git a/internal/domain/tools/registry.go b/internal/domain/tools/registry.go index ad9439d..f6b2193 100644 --- a/internal/domain/tools/registry.go +++ b/internal/domain/tools/registry.go @@ -2,6 +2,8 @@ package tools import ( "ai_scheduler/internal/config" + "ai_scheduler/internal/domain/tools/common/excel_generator" + "ai_scheduler/internal/domain/tools/common/image_converter" "ai_scheduler/internal/domain/tools/hyt/goods_add" "ai_scheduler/internal/domain/tools/hyt/goods_brand_search" "ai_scheduler/internal/domain/tools/hyt/goods_category_add" @@ -10,13 +12,21 @@ import ( "ai_scheduler/internal/domain/tools/hyt/product_upload" "ai_scheduler/internal/domain/tools/hyt/supplier_search" "ai_scheduler/internal/domain/tools/hyt/warehouse_search" + "ai_scheduler/internal/domain/tools/recharge/statistics_ours_product" ) type Manager struct { - Hyt *HytTools + Hyt *HytTools + Recharge *RechargeTools + Common *CommonTools // Zltx *ZltxTools } +type CommonTools struct { + ExcelGenerator *excel_generator.Client + ImageConverter *image_converter.Client +} + type HytTools struct { ProductUpload *product_upload.Client SupplierSearch *supplier_search.Client @@ -28,6 +38,10 @@ type HytTools struct { GoodsBrandSearch *goods_brand_search.Client } +type RechargeTools struct { + StatisticsOursProduct *statistics_ours_product.Client +} + func NewManager(cfg *config.Config) *Manager { return &Manager{ Hyt: &HytTools{ @@ -40,5 +54,12 @@ func NewManager(cfg *config.Config) *Manager { GoodsCategorySearch: goods_category_search.New(cfg.EinoTools.HytGoodsCategorySearch), GoodsBrandSearch: goods_brand_search.New(cfg.EinoTools.HytGoodsBrandSearch), }, + Recharge: &RechargeTools{ + StatisticsOursProduct: statistics_ours_product.New(cfg.EinoTools.RechargeStatisticsOursProduct), + }, + Common: &CommonTools{ + ExcelGenerator: excel_generator.New(), + ImageConverter: image_converter.New(cfg.EinoTools.Excel2Pic), + }, } } diff --git a/internal/domain/workflow/hyt/goods_add.go b/internal/domain/workflow/hyt/goods_add.go index 91db063..c41b66b 100644 --- a/internal/domain/workflow/hyt/goods_add.go +++ b/internal/domain/workflow/hyt/goods_add.go @@ -8,7 +8,6 @@ import ( "ai_scheduler/internal/domain/tools/hyt/goods_category_add" "ai_scheduler/internal/domain/tools/hyt/goods_media_add" "ai_scheduler/internal/domain/workflow/runtime" - "ai_scheduler/internal/entitys" "context" "encoding/json" "errors" @@ -42,7 +41,7 @@ type GoodsAddWorkflowInput struct { func (o *goodsAdd) ID() string { return WorkflowIDGoodsAdd } -func (o *goodsAdd) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) { +func (o *goodsAdd) Invoke(ctx context.Context, rec *runtime.WorkflowArgs) (map[string]any, error) { // 构建工作流 runnable, err := o.buildWorkflow(ctx) if err != nil { diff --git a/internal/domain/workflow/hyt/product_upload.go b/internal/domain/workflow/hyt/product_upload.go index 35114ed..b2af877 100644 --- a/internal/domain/workflow/hyt/product_upload.go +++ b/internal/domain/workflow/hyt/product_upload.go @@ -6,7 +6,6 @@ import ( toolManager "ai_scheduler/internal/domain/tools" toolPu "ai_scheduler/internal/domain/tools/hyt/product_upload" "ai_scheduler/internal/domain/workflow/runtime" - "ai_scheduler/internal/entitys" "context" "encoding/json" "errors" @@ -39,7 +38,7 @@ type ProductUploadWorkflowInput struct { func (o *productUpload) ID() string { return WorkflowIDProductUpload } -func (o *productUpload) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) { +func (o *productUpload) Invoke(ctx context.Context, rec *runtime.WorkflowArgs) (map[string]any, error) { // 构建工作流 runnable, err := o.buildWorkflow(ctx) if err != nil { diff --git a/internal/domain/workflow/recharge/statistics_ours_product.go b/internal/domain/workflow/recharge/statistics_ours_product.go new file mode 100644 index 0000000..8ca32c2 --- /dev/null +++ b/internal/domain/workflow/recharge/statistics_ours_product.go @@ -0,0 +1,206 @@ +package recharge + +import ( + "ai_scheduler/internal/config" + errorcode "ai_scheduler/internal/data/error" + toolManager "ai_scheduler/internal/domain/tools" + "ai_scheduler/internal/domain/tools/common/excel_generator" + "ai_scheduler/internal/domain/tools/recharge/statistics_ours_product" + "ai_scheduler/internal/domain/workflow/runtime" + "ai_scheduler/internal/pkg/utils_oss" + "context" + "errors" + "fmt" + "math/rand" + "path/filepath" + "time" + + "github.com/cloudwego/eino/compose" + "github.com/go-kratos/kratos/v2/log" +) + +const WorkflowIDStatisticsOursProduct = "recharge.statisticsOursProduct" + +func init() { + runtime.Register(WorkflowIDStatisticsOursProduct, func(d *runtime.Deps) (runtime.Workflow, error) { + return &statisticsOursProduct{cfg: d.Conf, toolManager: d.ToolManager, ossClient: d.Repos.OssClient}, nil + }) +} + +type statisticsOursProduct struct { + cfg *config.Config + toolManager *toolManager.Manager + ossClient *utils_oss.Client +} + +type StatisticsOursProductWorkflowInput struct { + Time time.Time `json:"time"` +} + +type StatisticsOursProductWorkflowOutput struct { + Path string `json:"path"` + Url string `json:"url"` + Data [][]string `json:"data"` + Desc string `json:"desc"` +} + +func (w *statisticsOursProduct) ID() string { return WorkflowIDStatisticsOursProduct } + +func (w *statisticsOursProduct) Invoke(ctx context.Context, args *runtime.WorkflowArgs) (map[string]any, error) { + // 构建工作流 + runnable, err := w.buildWorkflow(ctx) + if err != nil { + return nil, err + } + + // 获取参数时间 + input := &StatisticsOursProductWorkflowInput{ + Time: args.Args["now"].(time.Time), + } + + // 工作流过程调用 + output, err := runnable.Invoke(ctx, input) + if err != nil { + fmt.Println("Invoke err:", err) + errStr := err.Error() + if u := errors.Unwrap(err); u != nil { + errStr = u.Error() + } + return nil, errorcode.WorkflowErr(errStr) + } + + return output, nil +} + +type StatisticsOursProductContext struct { + Time time.Time + StartTime string + EndTime string + Title string + ProductData []statistics_ours_product.StatisticsOursProductItem + ImgUrl string + ExcelData [][]string +} + +func (w *statisticsOursProduct) buildWorkflow(ctx context.Context) (compose.Runnable[*StatisticsOursProductWorkflowInput, map[string]any], error) { + c := compose.NewChain[*StatisticsOursProductWorkflowInput, map[string]any]() + + // 1. 参数整理 + c.AppendLambda(compose.InvokableLambda(w.formatContext)) + + // 2. 调用工具统计我们的商品 + c.AppendLambda(compose.InvokableLambda(w.callStatisticsTool)) + + // 3. 生成 Excel 并转图片上传 + c.AppendLambda(compose.InvokableLambda(w.generateExcelAndUpload)) + + // 4. 转map输出 + c.AppendLambda(compose.InvokableLambda(w.convertToMap)) + + return c.Compile(ctx) +} + +// formatContext 整理上下文参数 +func (w *statisticsOursProduct) formatContext(ctx context.Context, input *StatisticsOursProductWorkflowInput) (*StatisticsOursProductContext, error) { + startTime := input.Time.Format("2006010200") + endTime := input.Time.Format("2006010215") + endTimeStr := input.Time.Format("1.2号15点") + + return &StatisticsOursProductContext{ + Time: time.Now(), + StartTime: startTime, + EndTime: endTime, + Title: fmt.Sprintf("截止 %s 我们的商品统计", endTimeStr), + }, nil +} + +func (w *statisticsOursProduct) callStatisticsTool(ctx context.Context, state *StatisticsOursProductContext) (*StatisticsOursProductContext, error) { + req := statistics_ours_product.StatisticsOursProductRequest{ + Page: 1, + Limit: 100, // 仅取前100条 + Serial: []string{state.StartTime, state.EndTime}, + } + + dataList, err := w.toolManager.Recharge.StatisticsOursProduct.Call(ctx, req) + if err != nil { + log.Errorf("调用统计我们的商品工具失败: %v", err) + return nil, fmt.Errorf("获取统计我们的商品数据失败") + } + if len(dataList) == 0 { + return nil, fmt.Errorf("我们的商品数据为空") + } + + state.ProductData = dataList + + return state, nil +} + +func (w *statisticsOursProduct) generateExcelAndUpload(ctx context.Context, state *StatisticsOursProductContext) (*StatisticsOursProductContext, error) { + // 1. 获取模板路径 + cwd, _ := filepath.Abs(".") + templatePath := filepath.Join(cwd, "tmpl", "excel_temp", "recharge_statistics_ours_product.xlsx") + fileName := fmt.Sprintf("statistics_ours_product_%d%d", time.Now().Unix(), rand.Intn(1000)) + + // 2. 转换数据为 [][]string + excelData := w.convertDataToExcelFormat(state.ProductData) + + // 3. 生成 Excel + req := &excel_generator.ExcelGeneratorRequest{ + TemplatePath: templatePath, + ExcelData: excelData, + StartRow: 4, + StyleRow: 3, + Title: state.Title, + } + excelBytes, err := w.toolManager.Common.ExcelGenerator.Call(req) + if err != nil { + return nil, fmt.Errorf("生成 Excel 失败: %v", err) + } + + // 4. Excel 转图片 + picBytes, err := w.toolManager.Common.ImageConverter.Call(fileName+".xlsx", excelBytes, 2) + if err != nil { + return nil, fmt.Errorf("Excel 转图片失败: %v", err) + } + + // 5. 上传 OSS + url, err := w.ossClient.UploadBytes(fileName+".png", picBytes) + if err != nil { + return nil, fmt.Errorf("上传 OSS 失败: %v", err) + } + + state.ImgUrl = url + state.ExcelData = excelData + + return state, nil +} + +// convertDataToExcelFormat 将业务数据转换为 Excel 生成器需要的二维字符串数组 +func (w *statisticsOursProduct) convertDataToExcelFormat(data []statistics_ours_product.StatisticsOursProductItem) [][]string { + var result [][]string + for _, item := range data { + row := []string{ + item.OursProductName, + // fmt.Sprintf("%d", item.OursProductId), + fmt.Sprintf("%v", item.Count), + // item.TotalPrice, + // item.SuccessCount, + fmt.Sprintf("%v", item.SuccessPrice), + // item.FailCount, + // item.FailPrice + fmt.Sprintf("%v", item.Profit), + } + + result = append(result, row) + } + return result +} + +func (w *statisticsOursProduct) convertToMap(ctx context.Context, state *StatisticsOursProductContext) (map[string]any, error) { + return map[string]any{ + "path": "", + "url": state.ImgUrl, + "data": state.ExcelData, + "desc": state.Title, + }, nil +} diff --git a/internal/domain/workflow/runtime/registry.go b/internal/domain/workflow/runtime/registry.go index f804e1d..1d391f2 100644 --- a/internal/domain/workflow/runtime/registry.go +++ b/internal/domain/workflow/runtime/registry.go @@ -15,7 +15,7 @@ import ( type Workflow interface { ID() string // Schema() map[string]any - Invoke(ctx context.Context, requireData *entitys.Recognize) (map[string]any, error) + Invoke(ctx context.Context, requireData *WorkflowArgs) (map[string]any, error) } type Deps struct { @@ -28,6 +28,11 @@ type Deps struct { type Factory func(deps *Deps) (Workflow, error) +type WorkflowArgs struct { + *entitys.Recognize + Args map[string]any +} + var ( regMu sync.RWMutex factories = map[string]Factory{} @@ -69,7 +74,7 @@ func Default() *Registry { return r } -func (r *Registry) Invoke(ctx context.Context, id string, rec *entitys.Recognize) (map[string]any, error) { +func (r *Registry) Invoke(ctx context.Context, id string, rec *WorkflowArgs) (map[string]any, error) { regMu.RLock() f, ok := factories[id] regMu.RUnlock() diff --git a/internal/domain/workflow/zltx/bug_optimization_submit.bak.go b/internal/domain/workflow/zltx/bug_optimization_submit.bak.go index 6ed6bb4..0ab94ee 100644 --- a/internal/domain/workflow/zltx/bug_optimization_submit.bak.go +++ b/internal/domain/workflow/zltx/bug_optimization_submit.bak.go @@ -42,7 +42,7 @@ func (w *bugOptimizationSubmitBak) ID() string { type BugOptimizationSubmitBakInput struct { Ch chan entitys.Response - RequireData *entitys.Recognize + RequireData *runtime.WorkflowArgs } type BugOptimizationSubmitBakOutput struct { @@ -54,7 +54,7 @@ type contextWithTaskBak struct { TaskID string } -func (w *bugOptimizationSubmitBak) Invoke(ctx context.Context, recognize *entitys.Recognize) (map[string]any, error) { +func (w *bugOptimizationSubmitBak) Invoke(ctx context.Context, recognize *runtime.WorkflowArgs) (map[string]any, error) { chain, err := w.buildWorkflow(ctx) if err != nil { return nil, err diff --git a/internal/domain/workflow/zltx/bug_optimization_submit.go b/internal/domain/workflow/zltx/bug_optimization_submit.go index 30ad0bc..ccf6812 100644 --- a/internal/domain/workflow/zltx/bug_optimization_submit.go +++ b/internal/domain/workflow/zltx/bug_optimization_submit.go @@ -42,7 +42,7 @@ func (w *bugOptimizationSubmit) ID() string { type BugOptimizationSubmitInput struct { Ch chan entitys.Response - RequireData *entitys.Recognize + RequireData *runtime.WorkflowArgs } type BugOptimizationSubmitOutput struct { @@ -54,7 +54,7 @@ type contextWithTask struct { TaskID string } -func (w *bugOptimizationSubmit) Invoke(ctx context.Context, recognize *entitys.Recognize) (map[string]any, error) { +func (w *bugOptimizationSubmit) Invoke(ctx context.Context, recognize *runtime.WorkflowArgs) (map[string]any, error) { chain, err := w.buildWorkflow(ctx) if err != nil { return nil, err diff --git a/internal/domain/workflow/zltx/order_after_reseller_batch.go b/internal/domain/workflow/zltx/order_after_reseller_batch.go index eee022a..620d636 100644 --- a/internal/domain/workflow/zltx/order_after_reseller_batch.go +++ b/internal/domain/workflow/zltx/order_after_reseller_batch.go @@ -78,7 +78,7 @@ type OrderAfterSaleResellerBatchData struct { func (o *orderAfterSaleResellerBatch) ID() string { return "zltx.orderAfterSaleResellerBatch" } // Invoke 调用原有编排工作流并规范化输出 -func (o *orderAfterSaleResellerBatch) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) { +func (o *orderAfterSaleResellerBatch) Invoke(ctx context.Context, rec *runtime.WorkflowArgs) (map[string]any, error) { // 构建工作流 chain, err := o.buildWorkflow(ctx) if err != nil { diff --git a/internal/entitys/types.go b/internal/entitys/types.go index 601f50e..815eeba 100644 --- a/internal/entitys/types.go +++ b/internal/entitys/types.go @@ -92,6 +92,10 @@ type ConfigDataTool struct { Tool string `json:"tool"` } +type ConfigDataReport struct { + Time string `json:"time"` +} + // Message 消息 type Message struct { Role string `json:"role"` diff --git a/internal/pkg/func.go b/internal/pkg/func.go index 4d9232b..648996c 100644 --- a/internal/pkg/func.go +++ b/internal/pkg/func.go @@ -10,8 +10,6 @@ import ( "strconv" "strings" "time" - - jsoniter "github.com/json-iterator/go" ) func JsonStringIgonErr(data interface{}) string { @@ -170,131 +168,257 @@ func SafeReplace(template string, replaceTag string, replacements ...string) (st return template, nil } -func StructToMapUsingJsoniter(obj interface{}) (map[string]string, error) { - var json = jsoniter.ConfigCompatibleWithStandardLibrary - - // 转换为JSON - jsonBytes, err := json.Marshal(obj) - if err != nil { - return nil, err - } - - // 解析为map[string]interface{} - var tempMap map[string]interface{} - err = json.Unmarshal(jsonBytes, &tempMap) - if err != nil { - return nil, err - } - - // 转换为map[string]string - result := make(map[string]string) - for k, v := range tempMap { - result[k] = fmt.Sprintf("%v", v) - } - - return result, nil +// 配置选项 +type URLValuesOptions struct { + ArrayFormat string // 数组格式:"brackets" -> name[], "indices" -> name[0], "repeat" -> name=value1&name=value2 + TimeFormat string // 时间格式 } -// 通用结构体转 Query 参数 -func StructToQuery(obj interface{}) (url.Values, error) { - values := url.Values{} - v := reflect.ValueOf(obj) - t := reflect.TypeOf(obj) +var defaultOptions = URLValuesOptions{ + ArrayFormat: "brackets", // 默认使用括号格式 + TimeFormat: time.DateTime, +} - // 如果是指针,获取指向的值 +// StructToURLValues 将结构体转换为 url.Values +func StructToURLValues(input interface{}, options ...URLValuesOptions) (url.Values, error) { + opts := defaultOptions + if len(options) > 0 { + opts = options[0] + } + + values := url.Values{} + + if input == nil { + return values, nil + } + + v := reflect.ValueOf(input) + t := reflect.TypeOf(input) + + // 如果是指针,获取其指向的值 if v.Kind() == reflect.Ptr { + if v.IsNil() { + return values, nil + } v = v.Elem() t = t.Elem() } - // 确保是结构体 + // 确保是结构体类型 if v.Kind() != reflect.Struct { - return values, fmt.Errorf("expected struct, got %v", v.Kind()) + return nil, fmt.Errorf("input must be a struct or pointer to struct") } + // 遍历结构体字段 for i := 0; i < v.NumField(); i++ { - field := v.Field(i) - fieldType := t.Field(i) + field := t.Field(i) + fieldValue := v.Field(i) - // 跳过零值字段(omitempty) - tag := fieldType.Tag.Get("json") - if strings.Contains(tag, "omitempty") && field.IsZero() { + // 跳过非导出字段 + if !field.IsExported() { continue } - // 获取字段名 - fieldName := getFieldName(fieldType) + // 解析 JSON 标签(也可以支持 form 标签) + tag := field.Tag.Get("json") + fieldName, omitempty := parseJSONTag(tag) + if fieldName == "-" { + continue // 忽略该字段 + } if fieldName == "" { + fieldName = field.Name + } + + // 处理指针类型 + if fieldValue.Kind() == reflect.Ptr { + if fieldValue.IsNil() { + if omitempty { + continue + } + // 可以为 nil 指针添加空值 + values.Set(fieldName, "") + continue + } + fieldValue = fieldValue.Elem() + } + + // 处理切片/数组 + if fieldValue.Kind() == reflect.Slice || fieldValue.Kind() == reflect.Array { + if fieldValue.Len() == 0 && omitempty { + continue + } + + // 将切片转换为 URL 参数 + err := addSliceToValues(values, fieldName, fieldValue, opts) + if err != nil { + return nil, err + } continue } - // 处理不同类型的字段 - addFieldToValues(values, fieldName, field) + // 检查是否需要忽略空值 + if omitempty && isEmptyValue(fieldValue) { + continue + } + + // 转换单个值 + str, err := valueToString(fieldValue, opts) + if err != nil { + return nil, err + } + values.Set(fieldName, str) } return values, nil } -func getFieldName(field reflect.StructField) string { - tag := field.Tag.Get("json") - if tag != "" { - parts := strings.Split(tag, ",") - if parts[0] != "-" && parts[0] != "" { - return parts[0] - } - if parts[0] == "-" { - return "" // 跳过该字段 - } - } - return field.Name -} - -func addFieldToValues(values url.Values, name string, field reflect.Value) { - if !field.IsValid() || field.IsZero() { - return +// 解析 JSON 标签 +func parseJSONTag(tag string) (fieldName string, omitempty bool) { + if tag == "" { + return "", false } - switch field.Kind() { - case reflect.String: - values.Add(name, field.String()) + parts := strings.Split(tag, ",") + fieldName = parts[0] - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - values.Add(name, strconv.FormatInt(field.Int(), 10)) - - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - values.Add(name, strconv.FormatUint(field.Uint(), 10)) - - case reflect.Float32, reflect.Float64: - values.Add(name, strconv.FormatFloat(field.Float(), 'f', -1, 64)) - - case reflect.Bool: - values.Add(name, strconv.FormatBool(field.Bool())) - - case reflect.Slice: - // 处理切片,特别是 []string - if field.Type().Elem().Kind() == reflect.String { - for i := 0; i < field.Len(); i++ { - item := field.Index(i).String() - // 特殊处理 ct 字段 - if name == "ct" { - formatted := strings.Replace(item, " ", "+", 1) - if i == 1 && field.Len() >= 2 { - formatted = formatted + ".999" - } - values.Add("ct[]", formatted) - } else { - values.Add(fmt.Sprintf("%s[]", name), item) - } + if len(parts) > 1 { + for _, part := range parts[1:] { + if part == "omitempty" { + omitempty = true } } + } - case reflect.Struct: - // 处理 time.Time - if t, ok := field.Interface().(time.Time); ok { - values.Add(name, t.Format("2006-01-02+15:04:05")) + return fieldName, omitempty +} + +// 添加切片到 values +func addSliceToValues(values url.Values, fieldName string, slice reflect.Value, opts URLValuesOptions) error { + length := slice.Len() + if length == 0 { + return nil + } + + switch opts.ArrayFormat { + case "brackets": + // 格式:field[]=value1&field[]=value2 + for i := 0; i < length; i++ { + item := slice.Index(i) + str, err := valueToString(item, opts) + if err != nil { + return err + } + values.Add(fieldName, str) + } + + case "indices": + // 格式:field[0]=value1&field[1]=value2 + for i := 0; i < length; i++ { + item := slice.Index(i) + str, err := valueToString(item, opts) + if err != nil { + return err + } + values.Set(fmt.Sprintf("%s[%d]", fieldName, i), str) + } + + case "repeat": + // 格式:field=value1&field=value2 + for i := 0; i < length; i++ { + item := slice.Index(i) + str, err := valueToString(item, opts) + if err != nil { + return err + } + values.Add(fieldName, str) } default: - values.Add(name, fmt.Sprintf("%v", field.Interface())) + // 默认使用 brackets 格式 + for i := 0; i < length; i++ { + item := slice.Index(i) + str, err := valueToString(item, opts) + if err != nil { + return err + } + values.Add(fieldName+"[]", str) + } + } + + return nil +} + +// 将值转换为字符串 +func valueToString(v reflect.Value, opts URLValuesOptions) (string, error) { + if !v.IsValid() { + return "", nil + } + + // 处理不同类型 + switch v.Kind() { + case reflect.String: + return v.String(), nil + + case reflect.Bool: + return strconv.FormatBool(v.Bool()), nil + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(v.Int(), 10), nil + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.FormatUint(v.Uint(), 10), nil + + case reflect.Float32, reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + + case reflect.Struct: + // 特殊处理 time.Time + if t, ok := v.Interface().(time.Time); ok { + return t.Format(opts.TimeFormat), nil + } + // 其他结构体递归处理 + // 这里可以扩展为递归处理嵌套结构体 + + default: + // 默认使用 fmt 的字符串表示 + return fmt.Sprintf("%v", v.Interface()), nil + } + + return fmt.Sprintf("%v", v.Interface()), nil +} + +// 检查值是否为空 +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.String: + return v.String() == "" + case reflect.Bool: + return false + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Slice, reflect.Array, reflect.Map: + return v.Len() == 0 + case reflect.Ptr, reflect.Interface: + return v.IsNil() + case reflect.Struct: + if t, ok := v.Interface().(time.Time); ok { + return t.IsZero() + } + return false + default: + return false } } + +// 方便函数:直接生成查询字符串 +func StructToQueryString(input interface{}, options ...URLValuesOptions) (string, error) { + values, err := StructToURLValues(input, options...) + if err != nil { + return "", err + } + return values.Encode(), nil +} diff --git a/internal/pkg/provider_set.go b/internal/pkg/provider_set.go index 603dedc..dcb5d8f 100644 --- a/internal/pkg/provider_set.go +++ b/internal/pkg/provider_set.go @@ -4,6 +4,7 @@ import ( "ai_scheduler/internal/pkg/dingtalk" "ai_scheduler/internal/pkg/utils_langchain" "ai_scheduler/internal/pkg/utils_ollama" + "ai_scheduler/internal/pkg/utils_oss" "ai_scheduler/internal/pkg/utils_vllm" "github.com/google/wire" @@ -19,4 +20,6 @@ var ProviderSetClient = wire.NewSet( dingtalk.NewOldClient, dingtalk.NewContactClient, dingtalk.NewNotableClient, + + utils_oss.NewClient, ) diff --git a/internal/pkg/utils_oss/client.go b/internal/pkg/utils_oss/client.go new file mode 100644 index 0000000..6db558e --- /dev/null +++ b/internal/pkg/utils_oss/client.go @@ -0,0 +1,57 @@ +package utils_oss + +import ( + "ai_scheduler/internal/config" + "bytes" + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" + "github.com/go-kratos/kratos/v2/log" +) + +type Client struct { + config config.Oss + client *oss.Client + bucket *oss.Bucket +} + +// NewClient 初始化 OSS 客户端 +func NewClient(cfg *config.Config) (*Client, error) { + client, err := oss.New(cfg.Oss.Endpoint, cfg.Oss.AccessKey, cfg.Oss.SecretKey) + if err != nil { + return nil, fmt.Errorf("oss new client failed: %v", err) + } + + bucket, err := client.Bucket(cfg.Oss.Bucket) + if err != nil { + return nil, fmt.Errorf("oss get bucket failed: %v", err) + } + + return &Client{ + config: cfg.Oss, + client: client, + bucket: bucket, + }, nil +} + +// UploadBytes 上传字节数组到 OSS +// objectKey: OSS 中的文件路径,例如 "ai_scheduler/test.png" +// fileBytes: 文件内容 +// 返回: 文件的访问 URL +func (c *Client) UploadBytes(objectKey string, fileBytes []byte) (string, error) { + err := c.bucket.PutObject(objectKey, bytes.NewReader(fileBytes)) + if err != nil { + log.Errorf("oss PutObject failed: %v", err) + return "", err + } + + // 构造返回 URL + var url string + if c.config.Domain != "" { + url = fmt.Sprintf("%s/%s", c.config.Domain, objectKey) + } else { + // 这里简单处理协议头 + url = fmt.Sprintf("https://%s.%s/%s", c.config.Bucket, c.config.Endpoint, objectKey) + } + return url, nil +} diff --git a/internal/services/capability.go b/internal/services/capability.go index 759433c..d2c1e9b 100644 --- a/internal/services/capability.go +++ b/internal/services/capability.go @@ -198,7 +198,7 @@ func (s *CapabilityService) ProductIngestConfirm(c *fiber.Ctx) error { // 调用eino工作流,实现商品上传到目标系统 rec := &entitys.Recognize{UserContent: &entitys.RecognizeUserContent{Text: req.Confirmed}} - res, err := s.workflowManager.Invoke(ctx, workflowId, rec) + res, err := s.workflowManager.Invoke(ctx, workflowId, &runtime.WorkflowArgs{Recognize: rec}) if err != nil { return err } diff --git a/internal/services/cron.go b/internal/services/cron.go index b51d0ab..1a1252e 100644 --- a/internal/services/cron.go +++ b/internal/services/cron.go @@ -3,10 +3,9 @@ package services import ( "ai_scheduler/internal/biz" "ai_scheduler/internal/config" - "ai_scheduler/internal/data/constants" "context" - "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" + "github.com/gofiber/fiber/v2/log" ) type CronService struct { @@ -22,7 +21,9 @@ func NewCronService(config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz) * } func (d *CronService) CronReportSend(ctx context.Context) error { - reportChan, err := d.dingTalkBotBiz.GetReportLists(ctx) + + groupId := 28 + groupInfo, err := d.dingTalkBotBiz.GetGroupInfo(ctx, groupId) if err != nil { return err } @@ -31,13 +32,14 @@ func (d *CronService) CronReportSend(ctx context.Context) error { if err != nil { return err } - err = d.dingTalkBotBiz.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{ - RobotCode: groupInfo.RobotCode, - ConversationType: constants.ConversationTypeGroup, - ConversationId: groupInfo.ConversationID, - Text: chatbot.BotCallbackDataTextModel{ - Content: "报表", - }, - }, reportChan) + //contentChan <- "截止今日23点利润亏损合计:127917.0866元,亏损500元以上的分销商和产品金额如下图:" + //contentChan <- "![图片](https://lsxdmgoss.oss-cn-chengdu.aliyuncs.com/MarketingSaaS/image/V2/other/shanghu.png)" + for _, report := range reports { + err = d.dingTalkBotBiz.SendReport(ctx, &groupInfo, report) + if err != nil { + log.Error(err) + continue + } + } return nil } diff --git a/internal/services/dtalk_bot.go b/internal/services/dtalk_bot.go index 4635b63..b71e40b 100644 --- a/internal/services/dtalk_bot.go +++ b/internal/services/dtalk_bot.go @@ -3,7 +3,6 @@ package services import ( "ai_scheduler/internal/biz" "ai_scheduler/internal/config" - "ai_scheduler/internal/data/constants" "ai_scheduler/internal/entitys" "context" "log" @@ -136,24 +135,3 @@ func (d *DingBotService) runBackgroundTasks(ctx context.Context, data *chatbot.B return nil } - -func (d *DingBotService) CronReportSend(ctx context.Context) error { - reportChan, err := d.dingTalkBotBiz.GetReportLists(ctx) - if err != nil { - return err - } - groupId := 23 - groupInfo, err := d.dingTalkBotBiz.GetGroupInfo(ctx, groupId) - if err != nil { - return err - } - err = d.dingTalkBotBiz.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{ - RobotCode: groupInfo.RobotCode, - ConversationType: constants.ConversationTypeGroup, - ConversationId: groupInfo.ConversationID, - Text: chatbot.BotCallbackDataTextModel{ - Content: "报表", - }, - }, reportChan) - return nil -} diff --git a/internal/services/dtalk_bot_test.go b/internal/services/dtalk_bot_test.go index 2b8223f..bd0ceef 100644 --- a/internal/services/dtalk_bot_test.go +++ b/internal/services/dtalk_bot_test.go @@ -12,9 +12,11 @@ import ( "ai_scheduler/internal/domain/component/callback" "ai_scheduler/internal/domain/repo" "ai_scheduler/internal/domain/workflow" + "ai_scheduler/internal/domain/workflow/runtime" "ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg/dingtalk" "ai_scheduler/internal/pkg/utils_ollama" + "ai_scheduler/internal/pkg/utils_oss" "ai_scheduler/internal/pkg/utils_vllm" "ai_scheduler/internal/tools" @@ -27,7 +29,7 @@ import ( func Test_Report(t *testing.T) { run() - a := dingBotService.CronReportSend(context.Background()) + a := cronService.CronReportSend(context.Background()) t.Log(a) } @@ -35,6 +37,7 @@ var ( configConfig *config.Config err error dingBotService *DingBotService + cronService *CronService ) // run 函数是程序的入口函数,负责初始化和配置各个组件 @@ -60,7 +63,7 @@ func run() { // 初始化Redis数据库连接 rdb := utils.NewRdb(configConfig) // 初始化仓库层 - repos := repo.NewRepos(sessionImpl, rdb) + repos := repo.NewRepos(sessionImpl, configConfig) // 初始化包级别的Redis连接 pkgRdb := pkg.NewRdb(configConfig) @@ -99,7 +102,11 @@ func run() { // 初始化处理器 handle := do.NewHandle(ollamaService, manager, configConfig, sessionImpl, registry, oldClient, contactClient, notableClient) // 初始化钉钉机器人业务逻辑 - dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, toolRegis, botChatHisImpl, manager, configConfig, sendCardClient) + utils_ossClient, _ := utils_oss.NewClient(configConfig) + // 初始化工作流管理器 + workflowManager := runtime.NewRegistry() + dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, toolRegis, botChatHisImpl, manager, configConfig, sendCardClient, utils_ossClient, workflowManager) // 初始化钉钉机器人服务 + cronService = NewCronService(configConfig, dingTalkBotBiz) dingBotService = NewDingBotService(configConfig, dingTalkBotBiz) } diff --git a/internal/tools/bbxt/api.go b/internal/tools/bbxt/api.go index 64632cb..8b6c823 100644 --- a/internal/tools/bbxt/api.go +++ b/internal/tools/bbxt/api.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/url" + "strings" ) @@ -40,10 +41,10 @@ func StatisOursProductLossSumApi(param *StatisOursProductLossSumReq) (*StatisOur } type GetProfitRankingSumRequest struct { - Ct []string `protobuf:"bytes,1,rep,name=ct,proto3" json:"ct,omitempty"` - Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` - Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` - ResellerIds []int32 `protobuf:"varint,5,rep,packed,name=reseller_ids,json=resellerIds,proto3" json:"reseller_ids,omitempty"` + Ct []string `json:"ct,omitempty"` + Page int32 `json:"page,omitempty"` + Limit int32 `json:"limit,omitempty"` + ResellerIds []int32 `json:"reseller_ids,omitempty"` } type GetProfitRankingSumResponse struct { @@ -53,19 +54,19 @@ type GetProfitRankingSumResponse struct { type ProfitRankingSumResponse struct { // 分销商ID - ResellerId string `protobuf:"bytes,1,opt,name=reseller_id,json=resellerId,proto3" json:"reseller_id,omitempty"` + ResellerId string `protobuf:"bytes,1,opt,name=reseller_id,json=resellerId,proto3" json:"ResellerId,omitempty"` // 分销商名称 - ResellerName string `protobuf:"bytes,2,opt,name=reseller_name,json=resellerName,proto3" json:"reseller_name,omitempty"` + ResellerName string `protobuf:"bytes,2,opt,name=reseller_name,json=resellerName,proto3" json:"ResellerName,omitempty"` // 当前利润 - CurrentProfit float64 `protobuf:"fixed64,3,opt,name=current_profit,json=currentProfit,proto3" json:"current_profit,omitempty"` + CurrentProfit float64 `protobuf:"fixed64,3,opt,name=current_profit,json=currentProfit,proto3" json:"CurrentProfit,omitempty"` // 昨日同比利润 - HistoryOneProfit float64 `protobuf:"fixed64,4,opt,name=history_one_profit,json=historyOneProfit,proto3" json:"history_one_profit,omitempty"` + HistoryOneProfit float64 `protobuf:"fixed64,4,opt,name=history_one_profit,json=historyOneProfit,proto3" json:"HistoryOneProfit,omitempty"` // 上周同比利润 - HistoryTwoProfit float64 `protobuf:"fixed64,5,opt,name=history_two_profit,json=historyTwoProfit,proto3" json:"history_two_profit,omitempty"` + HistoryTwoProfit float64 `protobuf:"fixed64,5,opt,name=history_two_profit,json=historyTwoProfit,proto3" json:"HistoryTwoProfit,omitempty"` // 昨日同比利润差值 - HistoryOneDiff float64 `protobuf:"fixed64,6,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"history_one_diff,omitempty"` + HistoryOneDiff float64 `protobuf:"fixed64,6,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"HistoryOneDiff,omitempty"` // 上周同比利润差值 - HistoryTwoDiff float64 `protobuf:"fixed64,7,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"history_two_diff,omitempty"` + HistoryTwoDiff float64 `protobuf:"fixed64,7,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"HistoryTwoDiff,omitempty"` } // GetProfitRankingSumApi 利润同比分销商排行榜 @@ -87,18 +88,18 @@ type GetStatisOfficialProductSumRequest struct { } type GetStatisOfficialProductSumResponse struct { - OfficialProductSum []*GetStatisOfficialProductSum `protobuf:"bytes,1,rep,name=official_product_sum,json=officialProductSum,proto3" json:"official_product_sum,omitempty"` - DataCount int32 `protobuf:"varint,2,opt,name=data_count,json=dataCount,proto3" json:"data_count,omitempty"` + OfficialProductSum []*GetStatisOfficialProductSum `protobuf:"bytes,1,rep,name=OfficialProductSum,json=officialProductSum,proto3" json:"officialProductSum,omitempty"` + DataCount int32 `protobuf:"varint,2,opt,name=DataCount,json=dataCount,proto3" json:"dataCount,omitempty"` } type GetStatisOfficialProductSum struct { - OfficialProductId int32 `protobuf:"varint,1,opt,name=official_product_id,json=officialProductId,proto3" json:"official_product_id,omitempty"` - OfficialProductName string `protobuf:"bytes,2,opt,name=official_product_name,json=officialProductName,proto3" json:"official_product_name,omitempty"` - CurrentNum int32 `protobuf:"varint,3,opt,name=current_num,json=currentNum,proto3" json:"current_num,omitempty"` - HistoryOneNum int32 `protobuf:"varint,4,opt,name=history_one_num,json=historyOneNum,proto3" json:"history_one_num,omitempty"` - HistoryTwoNum int32 `protobuf:"varint,5,opt,name=history_two_num,json=historyTwoNum,proto3" json:"history_two_num,omitempty"` - HistoryOneDiff int32 `protobuf:"varint,6,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"history_one_diff,omitempty"` - HistoryTwoDiff int32 `protobuf:"varint,7,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"history_two_diff,omitempty"` + OfficialProductId int32 `protobuf:"varint,1,opt,name=official_product_id,json=officialProductId,proto3" json:"officialProductId,omitempty"` + OfficialProductName string `protobuf:"bytes,2,opt,name=official_product_name,json=officialProductName,proto3" json:"officialProductName,omitempty"` + CurrentNum int32 `protobuf:"varint,3,opt,name=current_num,json=currentNum,proto3" json:"currentNum,omitempty"` + HistoryOneNum int32 `protobuf:"varint,4,opt,name=history_one_num,json=historyOneNum,proto3" json:"historyOneNum,omitempty"` + HistoryTwoNum int32 `protobuf:"varint,5,opt,name=history_two_num,json=historyTwoNum,proto3" json:"historyTwoNum,omitempty"` + HistoryOneDiff int32 `protobuf:"varint,6,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"historyOneDiff,omitempty"` + HistoryTwoDiff int32 `protobuf:"varint,7,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"historyTwoDiff,omitempty"` } // GetStatisOfficialProductSumApi 销量同比分析 @@ -144,18 +145,44 @@ type resCode struct { Error string `json:"error"` } +type GetStatisFilterOfficialProductRequest struct { + OfficialProductId int32 `protobuf:"varint,1,opt,name=official_product_id,json=officialProductId,proto3" json:"official_product_id,omitempty"` +} + +type GetStatisFilterOfficialProductResponse struct { + List []*StatisFilterOfficialProductResponse `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` +} + +type StatisFilterOfficialProductResponse struct { + OfficialProductId int32 `protobuf:"varint,1,opt,name=official_product_id,json=officialProductId,proto3" json:"OfficialProductId,omitempty"` + OfficialProductName string `protobuf:"bytes,2,opt,name=official_product_name,json=officialProductName,proto3" json:"OfficialProductName,omitempty"` +} + +// GetStatisFilterOfficialProductApi 官方商品列表 +func GetStatisFilterOfficialProductApi(param *GetStatisFilterOfficialProductRequest) (*GetStatisFilterOfficialProductResponse, error) { + url := "/dataanalytics/statisFilterOfficialProduct" + var res GetStatisFilterOfficialProductResponse + if err := request(url, param, &res); err != nil { + return nil, err + } + return &res, nil +} + func request(url string, reqData interface{}, resData interface{}) error { - reqParam, err := pkg.StructToQuery(reqData) + reqParam, err := pkg.StructToURLValues(reqData) if err != nil { return err } req := &l_request.Request{ - Url: Base + url + "?" + customEncode(reqParam), + Url: FormatPHPURL(Base+url, reqParam), Method: http.MethodGet, } res, err := req.Send() + if err != nil { + return err + } if res.StatusCode != http.StatusOK { return fmt.Errorf("request failed, status code: %d,resion: %s", res.StatusCode, res.Reason) } @@ -172,13 +199,50 @@ func request(url string, reqData interface{}, resData interface{}) error { return nil } -func customEncode(params url.Values) string { - encoded := params.Encode() +// FormatPHPURL 将 url.Values 格式化为 PHP 风格的 URL +// 输入:基础URL和url.Values参数 +// 输出:PHP风格的URL字符串 +func FormatPHPURL(baseURL string, values url.Values) string { + if values == nil || len(values) == 0 { + return baseURL + } - // 解码我们想要保留的字符 - encoded = strings.ReplaceAll(encoded, "%5B", "[") // 恢复 [ - encoded = strings.ReplaceAll(encoded, "%5D", "]") // 恢复 ] - encoded = strings.ReplaceAll(encoded, "%2B", "+") // 恢复 + + var queryParts []string - return encoded + // 遍历所有参数 + for key, paramValues := range values { + // 检查这个key是否有多个值(数组参数) + if len(paramValues) > 1 { + // 多值参数,使用PHP数组格式:key[]=value + for _, value := range paramValues { + if value != "" { + // 编码值 + encodedValue := url.QueryEscape(value) + // 使用PHP数组格式 + queryParts = append(queryParts, fmt.Sprintf("%s[]=%s", key, encodedValue)) + } + } + } else if len(paramValues) == 1 && paramValues[0] != "" { + // 单值参数 + encodedValue := url.QueryEscape(paramValues[0]) + queryParts = append(queryParts, fmt.Sprintf("%s=%s", key, encodedValue)) + } + } + + if len(queryParts) == 0 { + return baseURL + } + + // 构建查询字符串 + query := strings.Join(queryParts, "&") + + // 转换为PHP风格:解码中括号和冒号 + query = strings.ReplaceAll(query, "%5B", "[") + query = strings.ReplaceAll(query, "%5D", "]") + query = strings.ReplaceAll(query, "%3A", ":") + + // 注意:保留空格为+号(这是PHP的常见格式) + // query = strings.ReplaceAll(query, "+", "%20") // 如果需要将+转为%20,可以取消注释 + + return baseURL + "?" + query } diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index 7675d64..9bc400e 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -1,17 +1,33 @@ package bbxt import ( + "ai_scheduler/internal/pkg/utils_oss" "ai_scheduler/pkg" "fmt" - "reflect" - "time" + "math/rand" + "slices" - "github.com/xuri/excelize/v2" + "sort" + + "time" ) +const ( + RedStyle = "${color: FF0000;horizontal:center;vertical:center;borderColor:#000000}" + GreenStyle = "${color: 00B050;horizontal:center;vertical:center;borderColor:#000000}" +) + +var resellerBlackList = []string{ + "悦跑", + "电商-独立", + "蓝星严选连续包月", + "通钱-2025年12月", +} + type BbxtTools struct { cacheDir string excelTempDir string + ossClient *utils_oss.Client } func NewBbxtTools() (*BbxtTools, error) { @@ -30,20 +46,40 @@ func NewBbxtTools() (*BbxtTools, error) { }, nil } -func (b *BbxtTools) DailyReport(today time.Time) (err error) { - - err = b.StatisOursProductLossSumTotal([]string{ - time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, today.Location()).Format("2006-01-02 15:04:05"), - time.Date(today.Year(), today.Month(), today.Day(), 23, 59, 59, 0, today.Location()).Format("2006-01-02 15:04:05"), - }) +func (b *BbxtTools) DailyReport(now time.Time, productName []string, ossClient *utils_oss.Client) (reports []*ReportRes, err error) { + reports = make([]*ReportRes, 0, 4) + productLossReport, err := b.StatisOursProductLossSum(now) if err != nil { return } + profitRankingSum, err := b.GetProfitRankingSum(now) + if err != nil { + return + } + statisOfficialProductSum, err := b.GetStatisOfficialProductSum(now, productName) + if err != nil { + return + } + reports = append(reports, productLossReport...) + reports = append(reports, statisOfficialProductSum, profitRankingSum) + + if ossClient != nil { + uploader := NewUploader(ossClient) + for _, report := range reports { + _ = uploader.Run(report) + } + } + return } -// OursProductLossSum 负利润分析 -func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { +// StatisOursProductLossSum 负利润分析 +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())), + } + data, err := StatisOursProductLossSumApi(&StatisOursProductLossSumReq{ Ct: ct, }) @@ -89,80 +125,213 @@ func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { reseller.ProductLoss[info.OursProductId] = productLoss } } + + // 按经销商总亏损排序 + resellers := make([]*ResellerLoss, 0, len(resellerMap)) for _, v := range resellerMap { + resellers = append(resellers, v) + } + sort.Slice(resellers, func(i, j int) bool { + return resellers[i].Total < resellers[j].Total + }) + var ( + totalSum float64 + totalSum500 float64 + ) + // 构建分组 + for _, v := range resellers { if v.Total <= -100 { total = append(total, []string{ fmt.Sprintf("%s", v.ResellerName), fmt.Sprintf("%.2f", v.Total), }) } - if v.Total <= -500 { + if v.Total <= -500 && !slices.Contains(resellerBlackList, v.ResellerName) { gt = append(gt, v) + totalSum500 += v.Total } + totalSum += v.Total } + report = make([]*ReportRes, 2) + timeCh := now.Format("1月2日15点") //总量生成excel if len(total) > 0 { - filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" - err = b.SimpleFillExcel(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total) + 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, + } } - - if len(gt) > 0 { - filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" - err = b.SimpleFillExcel(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, total) - } - return err -} - -// 最简单的通用函数 -func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice interface{}) error { - // 1. 打开模板 - f, err := excelize.OpenFile(templatePath) if err != nil { - return err + return } - defer f.Close() - - sheet := f.GetSheetName(0) - - // 2. 反射获取切片数据 - v := reflect.ValueOf(dataSlice) - if v.Kind() != reflect.Slice { - return fmt.Errorf("dataSlice must be a slice") - } - - // 3. 从第2行开始填充 - row := 2 - for i := 0; i < v.Len(); i++ { - item := v.Index(i).Interface() - currentRow := row + i - - // 4. 将item转换为一行数据 - var rowData []interface{} - - // 如果是切片 - if reflect.TypeOf(item).Kind() == reflect.Slice { - itemV := reflect.ValueOf(item) - for j := 0; j < itemV.Len(); j++ { - rowData = append(rowData, itemV.Index(j).Interface()) - } - } else if reflect.TypeOf(item).Kind() == reflect.Struct { - itemV := reflect.ValueOf(item) - for j := 0; j < itemV.NumField(); j++ { - if itemV.Field(j).CanInterface() { - rowData = append(rowData, itemV.Field(j).Interface()) - } - } - } else { - rowData = []interface{}{item} - } - - // 5. 填充到Excel - for col, value := range rowData { - cell := fmt.Sprintf("%c%d", 'A'+col, currentRow) - f.SetCellValue(sheet, cell, value) + if len(gt) > 0 { + filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx" + err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) + report[1] = &ReportRes{ + ReportName: "负利润分析(亏损500以上)", + Title: "截至今日" + timeCh + "亏顺500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500), + Path: filePath, + Data: total, } } - - // 6. 保存 - return f.SaveAs(outputPath) + if err != nil { + return + } + return report, nil +} + +// GetProfitRankingSum 利润同比分销商排行榜 +func (b *BbxtTools) GetProfitRankingSum(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), + } + + data, err := GetProfitRankingSumApi(&GetProfitRankingSumRequest{ + Ct: ct, + }) + timeCh := now.Format("1月2日15点") + title := "截至" + timeCh + "利润同比分销商排行榜" + if err != nil { + return + } + + //排序 + sort.Slice(data.List, func(i, j int) bool { + return data.List[i].HistoryOneDiff > data.List[j].HistoryOneDiff + }) + //取前20和后20 + var ( + total [][]string + top = data.List[:20] + bottom = data.List[len(data.List)-20:] + ) + //合并前20和后20 + top = append(top, bottom...) + + // 构建分组 + for _, v := range top { + var diff string + if v.HistoryOneDiff > 0 { + diff = fmt.Sprintf("%s↑%.4f", RedStyle, v.HistoryOneDiff) + } else { + diff = fmt.Sprintf("%s↓%.4f", GreenStyle, v.HistoryOneDiff) + } + total = append(total, []string{ + fmt.Sprintf("%s", v.ResellerName), + fmt.Sprintf("%.4f", v.CurrentProfit), + fmt.Sprintf("%.4f", v.HistoryOneProfit), + diff, + }) + } + //总量生成excel + if len(total) == 0 { + return + } + filePath := b.cacheDir + "/lrtb_rank" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" + err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"lrtb_rank.xlsx", filePath, total, title) + return &ReportRes{ + ReportName: "利润同比分销商排行榜", + Title: title, + Path: filePath, + Data: total, + }, err +} + +// GetStatisOfficialProductSum 利润同比分销商排行榜 +func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []string) (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), + } + var ids []int32 + if len(productName) > 0 { + ids, err = b.getProductIdFromProductName(productName) + if err != nil { + return + } + } + reqParam := &GetStatisOfficialProductSumRequest{ + Ct: ct, + } + if len(ids) > 0 { + reqParam.OfficialProductId = ids + } + data, err := GetStatisOfficialProductSumApi(reqParam) + if err != nil { + return + } + var total [][]string + for _, v := range data.OfficialProductSum { + var ( + yeterDatyDiff string + lastWeekDiff string + ) + if v.HistoryOneDiff > 0 { + yeterDatyDiff = fmt.Sprintf("%s↑%d", RedStyle, v.HistoryOneDiff) + } else { + yeterDatyDiff = fmt.Sprintf("%s↓%d", GreenStyle, v.HistoryOneDiff) + } + if v.HistoryTwoDiff > 0 { + lastWeekDiff = fmt.Sprintf("%s↑%d", RedStyle, v.HistoryTwoDiff) + } else { + lastWeekDiff = fmt.Sprintf("%s↓%d", GreenStyle, v.HistoryTwoDiff) + } + total = append(total, []string{ + fmt.Sprintf("%s", v.OfficialProductName), + fmt.Sprintf("%d", v.CurrentNum), + fmt.Sprintf("%d", v.HistoryOneNum), + yeterDatyDiff, + fmt.Sprintf("%d", v.HistoryTwoNum), + lastWeekDiff, + }) + } + + timeCh := now.Format("1月2日15点") + title := "截至" + timeCh + "销售同比分析" + //总量生成excel + if len(total) == 0 { + return + } + filePath := b.cacheDir + "/xstb_ana" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" + err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"xstb_ana.xlsx", filePath, total, title) + return &ReportRes{ + ReportName: "利润同比分销商排行榜", + Title: title, + Path: filePath, + Data: total, + }, err +} + +func (b *BbxtTools) getProductIdFromProductName(productNames []string) ([]int32, error) { + data, err := GetStatisFilterOfficialProductApi(&GetStatisFilterOfficialProductRequest{}) + if err != nil { + return nil, err + } + var product2IdMap = make(map[string]int32) + for _, v := range data.List { + product2IdMap[v.OfficialProductName] = v.OfficialProductId + } + var ids []int32 + for _, v := range productNames { + if id, ok := product2IdMap[v]; ok { + ids = append(ids, id) + } + } + return ids, nil +} + +func adjustedTime(t time.Time) string { + adjusted := time.Date( + t.Year(), t.Month(), t.Day(), + t.Hour(), t.Minute(), 59, 999_000_000, + t.Location(), + ) + return adjusted.Format("2006-01-02 15:04:05.999") } diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go index 2770488..4a5f700 100644 --- a/internal/tools/bbxt/bbxt_test.go +++ b/internal/tools/bbxt/bbxt_test.go @@ -1,21 +1,66 @@ package bbxt import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/utils_oss" "testing" "time" ) func Test_StatisOursProductLossSumApiTotal(t *testing.T) { + var config = &config.Config{ + Oss: config.Oss{ + AccessKey: "LTAI5tGGZzjf3tvqWk8SQj2G", + SecretKey: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq", + Bucket: "attachment-public", + Domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com", + Endpoint: "https://oss-cn-hangzhou.aliyuncs.com", + }, + } + + ossClient, err := utils_oss.NewClient(config) + if err != nil { + panic(err) + } o, err := NewBbxtTools() if err != nil { panic(err) } - today := time.Now() - err = o.StatisOursProductLossSumTotal([]string{ - time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, today.Location()).Format("2006-01-02 15:04:05"), - time.Date(today.Year(), today.Month(), today.Day(), 23, 59, 59, 0, today.Location()).Format("2006-01-02 15:04:05"), - }) + reports, err := o.DailyReport(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, ossClient) - t.Log(err) + t.Log(reports, err) + +} + +func Test_StatisOursProductLossSum(t *testing.T) { + o, err := NewBbxtTools() + if err != nil { + panic(err) + } + report, err := o.StatisOursProductLossSum(time.Now()) + + t.Log(report, err) + +} + +func Test_GetProfitRankingSum(t *testing.T) { + o, err := NewBbxtTools() + if err != nil { + panic(err) + } + report, err := o.GetProfitRankingSum(time.Now()) + + t.Log(report, err) + +} + +func Test_GetStatisOfficialProductSum(t *testing.T) { + o, err := NewBbxtTools() + if err != nil { + panic(err) + } + report, err := o.GetStatisOfficialProductSum(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}) + + t.Log(report, err) } diff --git a/internal/tools/bbxt/entitys.go b/internal/tools/bbxt/entitys.go index a3dc743..d886dd6 100644 --- a/internal/tools/bbxt/entitys.go +++ b/internal/tools/bbxt/entitys.go @@ -12,3 +12,12 @@ type ProductLoss struct { ProductName string Loss float64 } + +type ReportRes struct { + ReportName string + Title string + Path string + Url string + Data [][]string + Desc string +} diff --git a/internal/tools/bbxt/excel.go b/internal/tools/bbxt/excel.go new file mode 100644 index 0000000..b6b1b6c --- /dev/null +++ b/internal/tools/bbxt/excel.go @@ -0,0 +1,307 @@ +package bbxt + +import ( + "fmt" + "reflect" + "regexp" + "sort" + "strings" + + "github.com/go-kratos/kratos/v2/log" + "github.com/shopspring/decimal" + "github.com/xuri/excelize/v2" +) + +func (b *BbxtTools) SimpleFillExcelWithTitle(templatePath, outputPath string, dataSlice interface{}, title string) error { + // 打开模板 + + f, err := excelize.OpenFile(templatePath) + if err != nil { + return err + } + defer f.Close() + + sheet := f.GetSheetName(0) + startLen := 2 + if len(title) > 0 { + // 写入标题 + f.SetCellValue(sheet, "A1", title) + startLen = 3 + } + // 获取模板样式 + templateRow := startLen + styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", templateRow)) + if err != nil { + log.Errorf("获取模板样式失败: %v", err) + styleID = 0 + } + + // 获取模板行高 + rowHeight, err := f.GetRowHeight(sheet, templateRow) + if err != nil { + log.Errorf("获取模板行高失败: %v", err) + rowHeight = 31 // 默认高度 + } + + // 反射获取切片数据 + v := reflect.ValueOf(dataSlice) + if v.Kind() != reflect.Slice { + return fmt.Errorf("dataSlice must be a slice") + } + + if v.Len() == 0 { + return nil + } + + // 从第三行开始填充数据(第二行留空或作为标题行) + startRow := startLen + pattern := `\$\{(.*?)\}` + re := regexp.MustCompile(pattern) + for i := 0; i < v.Len(); i++ { + currentRow := startRow + i + + // 获取当前行数据 + item := v.Index(i) + + // 处理不同类型的切片 + var rowData []interface{} + + if item.Kind() == reflect.Slice || item.Kind() == reflect.Array { + // 处理 []string 或 [][]string 中的一行 + for j := 0; j < item.Len(); j++ { + if item.Index(j).CanInterface() { + rowData = append(rowData, item.Index(j).Interface()) + } + } + } else if item.Kind() == reflect.Interface { + // 处理 interface{} 类型 + if actualValue, ok := item.Interface().([]string); ok { + for _, val := range actualValue { + rowData = append(rowData, val) + } + } else { + rowData = []interface{}{item.Interface()} + } + } else { + rowData = []interface{}{item.Interface()} + } + + // 4.1 设置行高 + f.SetRowHeight(sheet, currentRow, rowHeight) + + // 应用模板样式到整行(根据实际列数) + if styleID != 0 && len(rowData) > 0 { + startCol := "A" + endCol := fmt.Sprintf("%c", 'A'+len(rowData)-1) + endCell := fmt.Sprintf("%s%d", endCol, currentRow) + + f.SetCellStyle(sheet, fmt.Sprintf("%s%d", startCol, currentRow), + endCell, styleID) + } + // 填充数据到Excel + for col, value := range rowData { + cell := fmt.Sprintf("%c%d", 'A'+col, currentRow) + + switch value.(type) { + case string: + var style = value.(string) + if re.MatchString(style) { + matches := re.FindStringSubmatch(style) + styleMap := make(map[string]string) + //matches = strings.Replace(matches, "$", "", 1) + if len(matches) != 2 { + continue + } + for _, kv := range strings.Split(matches[1], ";") { + kvParts := strings.Split(kv, ":") + if len(kvParts) == 2 { + styleMap[strings.TrimSpace(kvParts[0])] = strings.TrimSpace(kvParts[1]) + } + } + fontStyleID, _err := SetStyle(styleMap, f) + if _err == nil { + f.SetCellStyle(sheet, cell, cell, fontStyleID) + } + + value = re.ReplaceAllString(style, "") + + } + f.SetCellValue(sheet, cell, value) + default: + + } + } + + } + // 保存 + return f.SaveAs(outputPath) +} + +func SetStyle(styleMap map[string]string, f *excelize.File) (int, error) { + + var style = &excelize.Style{} + // 设置字体颜色 + if colorHex, exists := styleMap["color"]; exists { + style.Font = &excelize.Font{ + Color: colorHex, + } + } + // 设置水平对齐 + if horizontal, exists := styleMap["horizontal"]; exists { + if style.Alignment == nil { + style.Alignment = &excelize.Alignment{} + } + style.Alignment.Horizontal = horizontal + } + // 设置垂直对齐 + if vertical, exists := styleMap["vertical"]; exists { + if style.Alignment == nil { + style.Alignment = &excelize.Alignment{} + } + style.Alignment.Vertical = vertical + } + + // 设置边框(新增) + if borderColor, exists := styleMap["borderColor"]; exists { + style.Border = []excelize.Border{ + {Type: "left", Color: borderColor, Style: 1}, // 左边框 + {Type: "right", Color: borderColor, Style: 1}, // 右边框 + {Type: "top", Color: borderColor, Style: 1}, // 上边框 + {Type: "bottom", Color: borderColor, Style: 1}, // 下边框 + } + } + return f.NewStyle(style) +} + +// 分销商负利润详情填充excel-V2 +// 1.使用模板文件作为输出文件,从第二行开始填充 +// 2.整体为3列:1.分销商名称(以ResellerName为分组,分销商名称列使用的样式为) 2.商品名称(p.ProductName) 3.亏损金额(p.Loss) +// 3.分销商名称列使用的样式为 A2;商品名称、亏损金额使用的样式为 B2、C2;样式包括宽高、背景、颜色等 +// 4.以ResellerName分组,合并单元格 +// 5.在文件末尾使用“合计”,合计行样式为模板第四行 +// 6.保存为新文件 +func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, dataSlice []*ResellerLoss) error { + // 1. 读取模板 + f, err := excelize.OpenFile(templatePath) + if err != nil { + return err + } + defer f.Close() + + sheet := f.GetSheetName(0) + + // ---------------- 样式获取 ---------------- + // 模板第2行:数据行样式 + tplRowData := 2 + styleA2, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowData)) + if err != nil { + styleA2 = 0 + } + // B2和C2通常样式一致,这里取B2作为明细列样式 + styleB2, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowData)) + if err != nil { + styleB2 = 0 + } + styleC2, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowData)) + if err != nil { + styleC2 = 0 + } + + rowHeightData, err := f.GetRowHeight(sheet, tplRowData) + if err != nil { + rowHeightData = 20 + } + + // 模板第4行:合计行样式 + tplRowTotal := 4 + 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 + } + rowHeightTotal, err := f.GetRowHeight(sheet, tplRowTotal) + if err != nil { + rowHeightTotal = 30 + } + // ---------------------------------------- + + currentRow := 2 + 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) + + // 设置样式 + if styleA2 != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleA2) + } + if styleB2 != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleB2) + } + if styleC2 != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleC2) + } + + totalLoss += p.Loss + currentRow++ + } + + endRow := currentRow - 1 + // 合并单元格 (如果多于1行) + if endRow > startRow { + f.MergeCell(sheet, fmt.Sprintf("A%d", startRow), fmt.Sprintf("A%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) + } + // 取消合并合计行的A、B列 + // f.MergeCell(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow)) + + // 6. 保存 + return f.SaveAs(outputPath) +} diff --git a/internal/tools/bbxt/upload.go b/internal/tools/bbxt/upload.go new file mode 100644 index 0000000..90277e3 --- /dev/null +++ b/internal/tools/bbxt/upload.go @@ -0,0 +1,172 @@ +package bbxt + +import ( + "ai_scheduler/internal/pkg/utils_oss" + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/gofiber/fiber/v2/log" + "github.com/xuri/excelize/v2" +) + +type Uploader struct { + ossClient *utils_oss.Client +} + +const RequestUrl = "http://192.168.6.109:8010/api/v1/convert" + +func NewUploader(oss *utils_oss.Client) *Uploader { + return &Uploader{ + ossClient: oss, + } +} + +func (u *Uploader) Run(report *ReportRes) (err error) { + if len(report.Path) == 0 { + return + } + f, err := excelize.OpenFile(report.Path) + if err != nil { + return err + } + defer f.Close() + + excelBytes, err := f.WriteToBuffer() + if err != nil { + return fmt.Errorf("write to bytes failed: %v", err) + } + + picBytes, err := u.excel2picPy(report.Path, excelBytes.Bytes(), 2) + if err != nil { + return fmt.Errorf("excel2picPy failed: %v", err) + } + // b.savePic("temp.png", picBytes) // 本地生成图片,仅测试 + // outputPath 提取文件名(不包含扩展名) + filename := filepath.Base(report.Path) + filename = strings.TrimSuffix(filename, filepath.Ext(filename)) + report.Url = u.uploadToOSS(filename, picBytes) + log.Infof("imgUrl: %s", report.Url) + + return +} + +// excel2picPy 将excel转换为图片python +// python 接口如下: +// curl --location --request POST 'http://192.168.6.109:8010/api/v1/convert' \ +// --header 'Content-Type: multipart/form-data; boundary=--------------------------952147881043913664015069' \ +// --form 'file=@"C:\\Users\\Administrator\\Downloads\\销售同比分析2025-12-29 0-12点.xlsx"' \ +// --form 'sheet_name="销售同比分析"' +func (u *Uploader) excel2picPy(templatePath string, excelBytes []byte, scale int) ([]byte, error) { + // 1. 获取 Sheet Name + // 尝试从 excelBytes 解析,如果失败则使用默认值 "Sheet1" + sheetName := "Sheet1" + f, err := excelize.OpenReader(bytes.NewReader(excelBytes)) + if err == nil { + sheetName = f.GetSheetName(0) + if sheetName == "" { + sheetName = "Sheet1" + } + f.Close() + } + + // 2. 构造 Multipart 请求 + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // 添加文件字段 + // 使用 templatePath 的文件名作为上传文件名,如果没有则用 default.xlsx + filename := "default.xlsx" + if templatePath != "" { + filename = filepath.Base(templatePath) + } + + part, err := writer.CreateFormFile("file", filename) + if err != nil { + return nil, fmt.Errorf("create form file failed: %v", err) + } + if _, err = part.Write(excelBytes); err != nil { + return nil, fmt.Errorf("write file part failed: %v", err) + } + + // 添加 sheet_name 字段 + if err = writer.WriteField("sheet_name", sheetName); err != nil { + return nil, fmt.Errorf("write field sheet_name failed: %v", err) + } + + // 添加 scale 字段 + if scale <= 0 { + scale = 2 + } + if err = writer.WriteField("scale", fmt.Sprintf("%d", scale)); err != nil { + return nil, fmt.Errorf("write field scale failed: %v", err) + } + + if err = writer.Close(); err != nil { + return nil, fmt.Errorf("close writer failed: %v", err) + } + + // 3. 发送 HTTP POST 请求 + + req, err := http.NewRequest("POST", RequestUrl, body) + if err != nil { + return nil, fmt.Errorf("create request failed: %v", err) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("send request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + respBody, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("api request failed with status: %d, body: %s", resp.StatusCode, string(respBody)) + } + + // 4. 读取响应 Body (图片内容) + picBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read response body failed: %v", err) + } + + return picBytes, nil +} + +// savePic 保存图片到本地 +func (u *Uploader) savePic(outputPath string, picBytes []byte) error { + dir := filepath.Dir(outputPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("create directory failed: %v", err) + } + return os.WriteFile(outputPath, picBytes, 0644) +} + +// uploadToOSS 上传至 oss 返回图片url +func (u *Uploader) uploadToOSS(fileName string, fileBytes []byte) string { + objectKey := fmt.Sprintf("ai-scheduler/data-analytics/images/%s.png", fileName) + url, err := u.ossClient.UploadBytes(objectKey, fileBytes) + if err != nil { + log.Errorf("oss upload failed: %v", err) + return "" + } + return url +} + +//// uploadToOSS 上传至 oss 返回图片url +//func (r *ReportRes) To(fileName string, fileBytes []byte) string { +// objectKey := fmt.Sprintf("ai-scheduler/data-analytics/images/%s.png", fileName) +// url, err := u.ossClient.UploadBytes(objectKey, fileBytes) +// if err != nil { +// log.Errorf("oss upload failed: %v", err) +// return "" +// } +// return url +//} diff --git a/internal/tools/zltx/zltx_statistics.go b/internal/tools/zltx/zltx_statistics.go index 5d71a9b..29e143a 100644 --- a/internal/tools/zltx/zltx_statistics.go +++ b/internal/tools/zltx/zltx_statistics.go @@ -45,7 +45,7 @@ func (z ZltxOrderStatisticsTool) Definition() entitys.ToolDefinition { } type ZltxOrderStatisticsRequest struct { - Number string `json:"number"` + Number interface{} `json:"number"` } func (z ZltxOrderStatisticsTool) Execute(ctx context.Context, rec *entitys.Recognize) error { @@ -53,7 +53,7 @@ func (z ZltxOrderStatisticsTool) Execute(ctx context.Context, rec *entitys.Recog if err := json.Unmarshal([]byte(rec.Match.Parameters), &req); err != nil { return err } - if req.Number == "" { + if req.Number == nil { return fmt.Errorf("number is required") } return z.getZltxOrderStatistics(req.Number, rec) @@ -76,14 +76,13 @@ type ZltxOrderStatisticsData struct { Total int `json:"total"` } -func (z ZltxOrderStatisticsTool) getZltxOrderStatistics(number string, rec *entitys.Recognize) error { +func (z ZltxOrderStatisticsTool) getZltxOrderStatistics(number interface{}, rec *entitys.Recognize) error { ext, err := rec_extra.GetTaskRecExt(rec) if err != nil { return err } //查询订单详情 - - url := fmt.Sprintf("%s%s", z.config.BaseURL, number) + url := fmt.Sprintf("%s%s", z.config.BaseURL, fmt.Sprintf("%v", number)) req := l_request.Request{ Url: url, Headers: map[string]string{ diff --git a/pkg/func.go b/pkg/func.go index 006f16f..9b4f89b 100644 --- a/pkg/func.go +++ b/pkg/func.go @@ -63,3 +63,11 @@ func GetTmplDir() (string, error) { } return path, nil } + +func ReverseSliceNew[T any](s []T) []T { + result := make([]T, len(s)) + for i := 0; i < len(s); i++ { + result[i] = s[len(s)-1-i] + } + return result +} diff --git a/tmpl/dataTemp/queryTempl.go b/tmpl/dataTemp/queryTempl.go index 7185bc2..2761318 100644 --- a/tmpl/dataTemp/queryTempl.go +++ b/tmpl/dataTemp/queryTempl.go @@ -189,3 +189,9 @@ func (k DataTemp) UpdateByCond(cond *builder.Cond, data interface{}) (err error) err = model.Where(query).Updates(data).Error return } + +func (k DataTemp) UpdateById(id int32, data interface{}) (err error) { + err = k.Db.Model(k.Model).Where("id = ?", id).Updates(data).Error + + return +} diff --git a/tmpl/excel_temp/kshj_gt.xlsx b/tmpl/excel_temp/kshj_gt.xlsx new file mode 100755 index 0000000..5aeeab5 Binary files /dev/null and b/tmpl/excel_temp/kshj_gt.xlsx differ diff --git a/tmpl/excel_temp/kshj_total.xlsx b/tmpl/excel_temp/kshj_total.xlsx old mode 100644 new mode 100755 index 2035ef9..9074fe0 Binary files a/tmpl/excel_temp/kshj_total.xlsx and b/tmpl/excel_temp/kshj_total.xlsx differ diff --git a/tmpl/excel_temp/lrtb_rank.xlsx b/tmpl/excel_temp/lrtb_rank.xlsx new file mode 100644 index 0000000..d3ed484 Binary files /dev/null and b/tmpl/excel_temp/lrtb_rank.xlsx differ diff --git a/tmpl/excel_temp/recharge_statistics_ours_product.xlsx b/tmpl/excel_temp/recharge_statistics_ours_product.xlsx new file mode 100755 index 0000000..8294b55 Binary files /dev/null and b/tmpl/excel_temp/recharge_statistics_ours_product.xlsx differ diff --git a/tmpl/excel_temp/xstb_ana.xlsx b/tmpl/excel_temp/xstb_ana.xlsx new file mode 100644 index 0000000..54fb056 Binary files /dev/null and b/tmpl/excel_temp/xstb_ana.xlsx differ