Merge remote-tracking branch 'origin/master'
# Conflicts: # internal/services/cron.go
This commit is contained in:
commit
84dae06187
|
|
@ -5,4 +5,5 @@ docs
|
|||
cmd/server/wire_gen.go
|
||||
__debug*
|
||||
.bin/
|
||||
.idea/
|
||||
cache/
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
3
go.mod
3
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
|
||||
|
|
|
|||
6
go.sum
6
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=
|
||||
|
|
|
|||
|
|
@ -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", report.Title, report.Url))
|
||||
//rec.Ch <- report.Title
|
||||
//reportChan <- fmt.Sprintf("", 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 <- ""
|
||||
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("", 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 + `,所有的时间识别精确到秒`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 日志配置
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const (
|
|||
TaskTypeBot TaskType = 4
|
||||
TaskTypeEinoWorkflow TaskType = 5 // eino 工作流
|
||||
TaskTypeCozeWorkflow TaskType = 6 // coze 工作流
|
||||
TaskTypeReport TaskType = 7 //报表
|
||||
)
|
||||
|
||||
type UseFul int32
|
||||
|
|
|
|||
|
|
@ -12,14 +12,15 @@ const TableNameAiBotGroup = "ai_bot_group"
|
|||
|
||||
// AiBotGroup mapped from table <ai_bot_group>
|
||||
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
|
||||
|
|
|
|||
|
|
@ -13,16 +13,16 @@ const TableNameAiBotTool = "ai_bot_tools"
|
|||
// AiBotTool mapped from table <ai_bot_tools>
|
||||
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"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package excel_generator
|
||||
|
||||
type ExcelGeneratorRequest struct {
|
||||
TemplatePath string // 模板文件路径
|
||||
ExcelData [][]string // 二维字符串数组
|
||||
StartRow int // 数据填充起始行 (默认 2)
|
||||
StyleRow int // 样式参考行 (默认 2)
|
||||
Title string // 表格标题(仅替换)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <- ""
|
||||
for _, report := range reports {
|
||||
err = d.dingTalkBotBiz.SendReport(ctx, &groupInfo, report)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
//}
|
||||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue