From 0f0da660bad89a7985d98c25513e3900b0e49500 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Mon, 29 Dec 2025 14:59:34 +0800 Subject: [PATCH 01/27] =?UTF-8?q?chore=EF=BC=9A=20=E6=9A=82=E5=AD=98?= =?UTF-8?q?=E4=B8=80=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config_env.yaml | 14 ++++ internal/config/config.go | 8 ++ .../data_analytics/official_product/client.go | 70 ++++++++++++++++ .../data_analytics/official_product/types.go | 31 ++++++++ .../official_product_decline/client.go | 79 +++++++++++++++++++ .../official_product_decline/types.go | 35 ++++++++ .../ours_product_loss/client.go | 78 ++++++++++++++++++ .../data_analytics/ours_product_loss/types.go | 29 +++++++ .../data_analytics/profit_ranking/client.go | 63 +++++++++++++++ .../profit_ranking/client_test.go | 27 +++++++ .../data_analytics/profit_ranking/types.go | 29 +++++++ 11 files changed, 463 insertions(+) create mode 100644 internal/domain/tools/data_analytics/official_product/client.go create mode 100644 internal/domain/tools/data_analytics/official_product/types.go create mode 100644 internal/domain/tools/data_analytics/official_product_decline/client.go create mode 100644 internal/domain/tools/data_analytics/official_product_decline/types.go create mode 100644 internal/domain/tools/data_analytics/ours_product_loss/client.go create mode 100644 internal/domain/tools/data_analytics/ours_product_loss/types.go create mode 100644 internal/domain/tools/data_analytics/profit_ranking/client.go create mode 100644 internal/domain/tools/data_analytics/profit_ranking/client_test.go create mode 100644 internal/domain/tools/data_analytics/profit_ranking/types.go diff --git a/config/config_env.yaml b/config/config_env.yaml index 21c1faa..9757b80 100644 --- a/config/config_env.yaml +++ b/config/config_env.yaml @@ -78,6 +78,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 +105,19 @@ eino_tools: # 货易通商品品牌查询 hytGoodsBrandSearch: base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/brand/list" + # == 报表分析 data analytics == + # 负利润分析列表、 负利润分析详情 + daOursProductLoss: + base_url: "http://test.analysis.com/api/dataanalytics/statisOursProductLossSum" + api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY2OTc4MTQwLCJuYmYiOjE3NjY5NzYzNDAsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.nyQw7_JZd7IfnIkPlfySGJMkFAHy_RtIL6gRqOYdku3ikp_w3x9bt7rrSBDQMi4Z8jC4jL_qxwhJ1D1DV4uLZ_cZBmuuG-gymIDN1TSJKa8hpu4E8L3tPdbT1H6hehXvg10-_ugKAYNBgnvFUbCRnpMl-sNuqCLWZKa_v63L1v3lGZOQiPPyzEGmIulNwqSF8rlzZggJneVTWEQac2BNK4181mpbz0S_m84xuFFO3Qwen2sEntBaPKeKbJ9BAfdSAQPcIlnG9FHF3mttZfxQQF5nbxU7CAGYsu_2kjpcmP-u5x3tT3gCWsF1t84lSZ8mMxIax7rh0-Y55abRoBnrwQ" + # 利润同比排行榜 + daProfitRanking: + base_url: "http://test.analysis.com/api/dataanalytics/profitRankingSum" + api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY2OTc4MTQwLCJuYmYiOjE3NjY5NzYzNDAsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.nyQw7_JZd7IfnIkPlfySGJMkFAHy_RtIL6gRqOYdku3ikp_w3x9bt7rrSBDQMi4Z8jC4jL_qxwhJ1D1DV4uLZ_cZBmuuG-gymIDN1TSJKa8hpu4E8L3tPdbT1H6hehXvg10-_ugKAYNBgnvFUbCRnpMl-sNuqCLWZKa_v63L1v3lGZOQiPPyzEGmIulNwqSF8rlzZggJneVTWEQac2BNK4181mpbz0S_m84xuFFO3Qwen2sEntBaPKeKbJ9BAfdSAQPcIlnG9FHF3mttZfxQQF5nbxU7CAGYsu_2kjpcmP-u5x3tT3gCWsF1t84lSZ8mMxIax7rh0-Y55abRoBnrwQ" + # 销售同比分析列表 + daOfficialProduct: + base_url: "http://test.analysis.com/api/dataanalytics/statisOfficialProduct" + api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY2OTc4MTQwLCJuYmYiOjE3NjY5NzYzNDAsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.nyQw7_JZd7IfnIkPlfySGJMkFAHy_RtIL6gRqOYdku3ikp_w3x9bt7rrSBDQMi4Z8jC4jL_qxwhJ1D1DV4uLZ_cZBmuuG-gymIDN1TSJKa8hpu4E8L3tPdbT1H6hehXvg10-_ugKAYNBgnvFUbCRnpMl-sNuqCLWZKa_v63L1v3lGZOQiPPyzEGmIulNwqSF8rlzZggJneVTWEQac2BNK4181mpbz0S_m84xuFFO3Qwen2sEntBaPKeKbJ9BAfdSAQPcIlnG9FHF3mttZfxQQF5nbxU7CAGYsu_2kjpcmP-u5x3tT3gCWsF1t84lSZ8mMxIax7rh0-Y55abRoBnrwQ" dingtalk: api_key: "dingsbbntrkeiyazcfdg" diff --git a/internal/config/config.go b/internal/config/config.go index 441d09d..713663f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -188,6 +188,14 @@ 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"` } // LoggingConfig 日志配置 diff --git a/internal/domain/tools/data_analytics/official_product/client.go b/internal/domain/tools/data_analytics/official_product/client.go new file mode 100644 index 0000000..bffb6be --- /dev/null +++ b/internal/domain/tools/data_analytics/official_product/client.go @@ -0,0 +1,70 @@ +package official_product + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/l_request" + "context" + "encoding/json" + "fmt" + "strings" +) + +type Client struct { + cfg config.ToolConfig +} + +func New(cfg config.ToolConfig) *Client { + return &Client{ + cfg: cfg, + } +} + +// Call 调用销售同比分析接口 +func (c *Client) Call(ctx context.Context, req OfficialProductRequest) (*OfficialProductData, error) { + // 构建 URL 参数 + var queryParams []string + + if req.Page > 0 { + queryParams = append(queryParams, fmt.Sprintf("page=%d", req.Page)) + } + if req.Limit > 0 { + queryParams = append(queryParams, fmt.Sprintf("limit=%d", req.Limit)) + } + + for _, pid := range req.OfficialProductIds { + queryParams = append(queryParams, fmt.Sprintf("official_product_id[]=%s", pid)) + } + + for _, t := range req.Ct { + queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+"))) + } + + queryString := strings.Join(queryParams, "&") + fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString) + + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey), + } + + reqObj := l_request.Request{ + Method: "GET", + Url: fullURL, + Headers: headers, + } + + res, err := reqObj.Send() + if err != nil { + return nil, fmt.Errorf("请求失败,err: %v", err) + } + + var resData OfficialProductResponse + if err := json.Unmarshal([]byte(res.Text), &resData); err != nil { + return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text) + } + + if resData.Code != 200 { + return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg) + } + + return &resData.Data, nil +} diff --git a/internal/domain/tools/data_analytics/official_product/types.go b/internal/domain/tools/data_analytics/official_product/types.go new file mode 100644 index 0000000..a0890b5 --- /dev/null +++ b/internal/domain/tools/data_analytics/official_product/types.go @@ -0,0 +1,31 @@ +package official_product + +// OfficialProductRequest 销售同比分析请求参数 +type OfficialProductRequest struct { + Page int `json:"page"` // 页码 + Limit int `json:"limit"` // 每页条数 + OfficialProductIds []string `json:"official_product_ids"` // 官方产品ID列表 + Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间] +} + +// OfficialProductResponse 销售同比分析响应结构 +type OfficialProductResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data OfficialProductData `json:"data"` +} + +type OfficialProductData struct { + OfficialProductSum []OfficialProductItem `json:"officialProductSum"` + DataCount int `json:"dataCount"` +} + +type OfficialProductItem struct { + OfficialProductId int `json:"officialProductId"` + OfficialProductName string `json:"officialProductName"` + CurrentNum int `json:"currentNum"` + HistoryOneNum int `json:"historyOneNum"` + HistoryTwoNum int `json:"historyTwoNum"` + HistoryOneDiff int `json:"historyOneDiff"` + HistoryTwoDiff int `json:"historyTwoDiff"` +} diff --git a/internal/domain/tools/data_analytics/official_product_decline/client.go b/internal/domain/tools/data_analytics/official_product_decline/client.go new file mode 100644 index 0000000..9ce9499 --- /dev/null +++ b/internal/domain/tools/data_analytics/official_product_decline/client.go @@ -0,0 +1,79 @@ +package official_product_decline + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/l_request" + "context" + "encoding/json" + "fmt" + "strings" +) + +type Client struct { + cfg config.ToolConfig +} + +func New(cfg config.ToolConfig) *Client { + return &Client{ + cfg: cfg, + } +} + +// Call 调用销售同比下滑详情接口 +func (c *Client) Call(ctx context.Context, req OfficialProductDeclineRequest) (*OfficialProductDeclineData, error) { + // 构建 URL 参数 + var queryParams []string + + if req.Page > 0 { + queryParams = append(queryParams, fmt.Sprintf("page=%d", req.Page)) + } + if req.Limit > 0 { + queryParams = append(queryParams, fmt.Sprintf("limit=%d", req.Limit)) + } + if req.DownwardValue > 0 { + queryParams = append(queryParams, fmt.Sprintf("downwardValue=%d", req.DownwardValue)) + } + // showTime 可能是 0,所以这里不做 > 0 判断,如果业务默认是 0 可以忽略,或者根据实际需求 + // 假设始终传递该参数如果已设置 + queryParams = append(queryParams, fmt.Sprintf("showTime=%d", req.ShowTime)) + + for _, pid := range req.OfficialProductIds { + queryParams = append(queryParams, fmt.Sprintf("official_product_id[]=%s", pid)) + } + + for _, t := range req.Ct { + queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+"))) + } + + queryString := strings.Join(queryParams, "&") + fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString) + + headers := map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36", + "Accept": "application/json, text/plain, */*", + "Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey), + "Accept-Language": "zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7", + } + + reqObj := l_request.Request{ + Method: "GET", + Url: fullURL, + Headers: headers, + } + + res, err := reqObj.Send() + if err != nil { + return nil, fmt.Errorf("请求失败,err: %v", err) + } + + var resData OfficialProductDeclineResponse + if err := json.Unmarshal([]byte(res.Text), &resData); err != nil { + return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text) + } + + if resData.Code != 200 { + return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg) + } + + return &resData.Data, nil +} diff --git a/internal/domain/tools/data_analytics/official_product_decline/types.go b/internal/domain/tools/data_analytics/official_product_decline/types.go new file mode 100644 index 0000000..3fa598d --- /dev/null +++ b/internal/domain/tools/data_analytics/official_product_decline/types.go @@ -0,0 +1,35 @@ +package official_product_decline + +// OfficialProductDeclineRequest 销售同比下滑详情请求参数 +type OfficialProductDeclineRequest struct { + Page int `json:"page"` // 页码 + Limit int `json:"limit"` // 每页条数 + Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间] + OfficialProductIds []string `json:"official_product_ids"` // 官方产品ID列表 + DownwardValue int `json:"downward_value"` // 下滑值 + ShowTime int `json:"show_time"` // 是否显示时间 (0:不显示, 1:显示) +} + +// OfficialProductDeclineResponse 销售同比下滑详情响应结构 +type OfficialProductDeclineResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data OfficialProductDeclineData `json:"data"` +} + +type OfficialProductDeclineData struct { + OfficialProductSumDecline []OfficialProductDeclineItem `json:"officialProductSumDecline"` + DataCount int `json:"dataCount"` +} + +type OfficialProductDeclineItem struct { + ResellerId int `json:"resellerId"` + OfficialProductId int `json:"officialProductId"` + OfficialProductName string `json:"officialProductName"` + ResellerName string `json:"resellerName"` + CurrentNum int `json:"currentNum"` + HistoryOneNum int `json:"historyOneNum"` + HistoryTwoNum int `json:"historyTwoNum"` + HistoryOneDiff int `json:"historyOneDiff"` + HistoryTwoDiff int `json:"historyTwoDiff"` +} diff --git a/internal/domain/tools/data_analytics/ours_product_loss/client.go b/internal/domain/tools/data_analytics/ours_product_loss/client.go new file mode 100644 index 0000000..9e145b3 --- /dev/null +++ b/internal/domain/tools/data_analytics/ours_product_loss/client.go @@ -0,0 +1,78 @@ +package ours_product_loss + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/l_request" + "context" + "encoding/json" + "fmt" + "strings" +) + +type Client struct { + cfg config.ToolConfig +} + +func New(cfg config.ToolConfig) *Client { + return &Client{ + cfg: cfg, + } +} + +// Call 调用负利润分析接口 +// 支持列表查询和详情查询 +// 列表查询:提供 page, limit, ct[] +// 详情查询:提供 ct[], resellerId +func (c *Client) Call(ctx context.Context, req OursProductLossRequest) (*OursProductLossData, error) { + // 处理数组参数 ct[] + // util.StructToMap 通常不支持数组到 url query array 的转换,这里手动处理查询字符串 + // 或者如果 l_request 支持 map 中的 slice 自动转换最好,假设不支持需手动拼接 + + // 构建 URL 参数 + var queryParams []string + + if req.Page > 0 { + queryParams = append(queryParams, fmt.Sprintf("page=%d", req.Page)) + } + if req.Limit > 0 { + queryParams = append(queryParams, fmt.Sprintf("limit=%d", req.Limit)) + } + if req.ResellerId != "" { + queryParams = append(queryParams, fmt.Sprintf("resellerId=%s", req.ResellerId)) + } + + for _, t := range req.Ct { + // URL 编码处理,这里简单处理,实际应使用 url.QueryEscape + // 假设输入已经是合法的格式 + queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+"))) + } + + queryString := strings.Join(queryParams, "&") + fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString) + + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey), + } + + reqObj := l_request.Request{ + Method: "GET", + Url: fullURL, + Headers: headers, + } + + res, err := reqObj.Send() + if err != nil { + return nil, fmt.Errorf("请求失败,err: %v", err) + } + + var resData OursProductLossResponse + if err := json.Unmarshal([]byte(res.Text), &resData); err != nil { + return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text) + } + + if resData.Code != 200 { + return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg) + } + + return &resData.Data, nil +} diff --git a/internal/domain/tools/data_analytics/ours_product_loss/types.go b/internal/domain/tools/data_analytics/ours_product_loss/types.go new file mode 100644 index 0000000..ceb006a --- /dev/null +++ b/internal/domain/tools/data_analytics/ours_product_loss/types.go @@ -0,0 +1,29 @@ +package ours_product_loss + +// OursProductLossRequest 负利润分析请求参数 +type OursProductLossRequest struct { + Page int `json:"page"` // 页码 + Limit int `json:"limit"` // 每页条数 + Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间] + ResellerId string `json:"reseller_id"` // 经销商ID (详情查询时使用) +} + +// OursProductLossResponse 负利润分析响应结构 +type OursProductLossResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data OursProductLossData `json:"data"` +} + +type OursProductLossData struct { + List []OursProductLossItem `json:"list"` + DataCount int `json:"dataCount"` +} + +type OursProductLossItem struct { + OursProductId int `json:"oursProductId"` + OursProductName string `json:"oursProductName"` + ResellerName string `json:"resellerName"` + ResellerId int `json:"resellerId"` + Loss float64 `json:"loss"` +} diff --git a/internal/domain/tools/data_analytics/profit_ranking/client.go b/internal/domain/tools/data_analytics/profit_ranking/client.go new file mode 100644 index 0000000..251c59e --- /dev/null +++ b/internal/domain/tools/data_analytics/profit_ranking/client.go @@ -0,0 +1,63 @@ +package profit_ranking + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/l_request" + "context" + "encoding/json" + "fmt" + "strings" +) + +type Client struct { + cfg config.ToolConfig +} + +func New(cfg config.ToolConfig) *Client { + return &Client{ + cfg: cfg, + } +} + +// Call 调用利润同比排行榜接口 +func (c *Client) Call(ctx context.Context, req ProfitRankingRequest) (*ProfitRankingData, error) { + // 构建 URL 参数 + var queryParams []string + + for _, t := range req.Ct { + queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+"))) + } + + for _, rid := range req.ResellerIds { + queryParams = append(queryParams, fmt.Sprintf("resellerIds[]=%s", rid)) + } + + queryString := strings.Join(queryParams, "&") + fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString) + + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey), + } + + reqObj := l_request.Request{ + Method: "GET", + Url: fullURL, + Headers: headers, + } + + res, err := reqObj.Send() + if err != nil { + return nil, fmt.Errorf("请求失败,err: %v", err) + } + + var resData ProfitRankingResponse + if err := json.Unmarshal([]byte(res.Text), &resData); err != nil { + return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text) + } + + if resData.Code != 200 { + return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg) + } + + return &resData.Data, nil +} diff --git a/internal/domain/tools/data_analytics/profit_ranking/client_test.go b/internal/domain/tools/data_analytics/profit_ranking/client_test.go new file mode 100644 index 0000000..ad5bc29 --- /dev/null +++ b/internal/domain/tools/data_analytics/profit_ranking/client_test.go @@ -0,0 +1,27 @@ +package profit_ranking + +import ( + "ai_scheduler/internal/config" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClient_Call(t *testing.T) { + cfg := config.ToolConfig{ + BaseURL: "http://test.analysis.com/api/dataanalytics/profitRankingSum", + APIKey: "test_jwt_token", + } + + client := New(cfg) + assert.NotNil(t, client) + + req := ProfitRankingRequest{ + Ct: []string{"2025-01-01 00:00:00", "2025-01-01 23:59:59"}, + ResellerIds: []string{"1001", "1002"}, + } + + t.Logf("Testing Call with req: %+v", req) + // _, err := client.Call(context.Background(), req) + // assert.Error(t, err) +} diff --git a/internal/domain/tools/data_analytics/profit_ranking/types.go b/internal/domain/tools/data_analytics/profit_ranking/types.go new file mode 100644 index 0000000..d0d8552 --- /dev/null +++ b/internal/domain/tools/data_analytics/profit_ranking/types.go @@ -0,0 +1,29 @@ +package profit_ranking + +// ProfitRankingRequest 利润同比排行请求参数 +type ProfitRankingRequest struct { + Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间] + ResellerIds []string `json:"reseller_ids"` // 经销商ID列表 +} + +// ProfitRankingResponse 利润同比排行响应结构 +type ProfitRankingResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data ProfitRankingData `json:"data"` +} + +type ProfitRankingData struct { + List []ProfitRankingItem `json:"list"` + DataCount int `json:"dataCount"` +} + +type ProfitRankingItem struct { + ResellerId string `json:"resellerId"` + ResellerName string `json:"resellerName"` + CurrentProfit float64 `json:"currentProfit"` + HistoryOneProfit float64 `json:"historyOneProfit"` + HistoryTwoProfit float64 `json:"historyTwoProfit"` + HistoryOneDiff float64 `json:"historyOneDiff"` + HistoryTwoDiff float64 `json:"historyTwoDiff"` +} From 8deb91dea93030eafe563d385531c0cf72d10ea8 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Tue, 30 Dec 2025 09:15:58 +0800 Subject: [PATCH 02/27] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=88=86?= =?UTF-8?q?=E9=94=80=E5=95=86=E8=B4=9F=E5=88=A9=E6=B6=A6=E8=AF=A6=E6=83=85?= =?UTF-8?q?excel=E5=A1=AB=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tools/bbxt/bbxt.go | 96 ++++++++++++++++++++++++++++++- internal/tools/bbxt/bbxt_test.go | 2 +- tmpl/excel_temp/kshj_gt.xlsx | Bin 0 -> 9469 bytes 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100755 tmpl/excel_temp/kshj_gt.xlsx diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index 8fe2885..cd1024f 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -4,6 +4,7 @@ import ( "ai_scheduler/internal/pkg" "fmt" "reflect" + "sort" "time" "github.com/xuri/excelize/v2" @@ -92,7 +93,7 @@ func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { for _, v := range resellerMap { if v.Total <= -100 { total = append(total, []string{ - fmt.Sprintf("%d", v.ResellerName), + fmt.Sprintf("%s", v.ResellerName), fmt.Sprintf("%.2f", v.Total), }) } @@ -103,12 +104,13 @@ func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { //总量生成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) + err = b.SimpleFillExcel(b.excelTempDir+"/"+"kshj_total_v2.xlsx", filePath, 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) + // err = b.SimpleFillExcel(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, total) + err = b.resellerDetailFillExcel(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) } return err } @@ -166,3 +168,91 @@ func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice i // 6. 保存 return f.SaveAs(outputPath) } + +// 分销商负利润详情填充excel +// 1.使用模板文件作为输出文件 +// 2.分销商总计使用第二行样式(宽高、背景、颜色等) +// 3.商品详情使用第三行样式(宽高、背景、颜色等) +// 4.保存为新文件 +func (b *BbxtTools) resellerDetailFillExcel(templatePath, outputPath string, dataSlice []*ResellerLoss) error { + // 1. 读取模板 + f, err := excelize.OpenFile(templatePath) + if err != nil { + return err + } + defer f.Close() + + sheet := f.GetSheetName(0) + + // 获取模板样式1:第二行-分销商总计 + resellerTplRow := 2 + styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow)) + if err != nil { + // 如果获取失败,就不应用样式,或者记录日志,这里选择忽略错误继续 + styleIDReseller = 0 + } + rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow) + if err != nil { + rowHeightReseller = 31 // 默认高度 + } + // 获取模板样式2:第三行-产品亏损明细 + productTplRow := 3 + styleIDProduct, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", productTplRow)) + if err != nil { + // 如果获取失败,就不应用样式,或者记录日志,这里选择忽略错误继续 + styleIDProduct = 0 + } + rowHeightProduct, err := f.GetRowHeight(sheet, productTplRow) + if err != nil { + rowHeightProduct = 25 // 默认高度 + } + + currentRow := 2 + + for _, reseller := range dataSlice { + // 3. 填充经销商数据 (ResellerName, Total) + // 设置行高 + f.SetRowHeight(sheet, currentRow, rowHeightReseller) + + // 设置单元格值 + f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), reseller.ResellerName) + f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), reseller.Total) + + // 应用样式 + if styleIDReseller != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller) + } + + currentRow++ + + // 4. 填充产品亏损明细 + // 先对 ProductLoss 进行排序 + var products []ProductLoss + for _, p := range reseller.ProductLoss { + products = append(products, p) + } + // 按 Loss 升序排序 (亏损越多越靠前,负数越小) + sort.Slice(products, func(i, j int) bool { + return products[i].Loss < products[j].Loss + }) + + for _, p := range products { + // 设置行高 + f.SetRowHeight(sheet, currentRow, rowHeightProduct) + + // 设置单元格值 + f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), p.ProductName) + f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.Loss) + + // 应用样式 + if styleIDProduct != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDProduct) + } + + currentRow++ + } + } + + // 6. 保存 + return f.SaveAs(outputPath) +} diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go index 80e6b29..628c675 100644 --- a/internal/tools/bbxt/bbxt_test.go +++ b/internal/tools/bbxt/bbxt_test.go @@ -7,7 +7,7 @@ func Test_StatisOursProductLossSumApiTotal(t *testing.T) { if err != nil { panic(err) } - err = o.StatisOursProductLossSumTotal() + err = o.StatisOursProductLossSumTotal([]string{"2025-12-28+00:00:00", "2025-12-28+23:59:59.999"}) t.Log(err) diff --git a/tmpl/excel_temp/kshj_gt.xlsx b/tmpl/excel_temp/kshj_gt.xlsx new file mode 100755 index 0000000000000000000000000000000000000000..9ab76f5e450a7e10ea61eded595909e5f46182a2 GIT binary patch literal 9469 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 Date: Tue, 30 Dec 2025 09:36:37 +0800 Subject: [PATCH 03/27] =?UTF-8?q?fix=EF=BC=9A1.=E4=BB=85=E5=88=86=E9=94=80?= =?UTF-8?q?=E5=95=86=E5=AF=BC=E5=87=BA=E4=B9=9F=E4=BD=BF=E7=94=A8=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=202.=E4=BF=AE=E6=94=B9=E5=88=86=E9=94=80=E5=95=86?= =?UTF-8?q?=E3=80=81=E5=88=86=E9=94=80=E5=95=86=E8=AF=A6=E6=83=85=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=203.=E5=A2=9E=E5=8A=A0=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tools/bbxt/bbxt.go | 31 +++++++++++++++++++++++++++---- tmpl/excel_temp/kshj_gt.xlsx | Bin 9469 -> 9473 bytes tmpl/excel_temp/kshj_total.xlsx | Bin 10313 -> 9414 bytes 3 files changed, 27 insertions(+), 4 deletions(-) mode change 100644 => 100755 tmpl/excel_temp/kshj_total.xlsx diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index cd1024f..d7d00ef 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -7,6 +7,7 @@ import ( "sort" "time" + "github.com/gofiber/fiber/v2/log" "github.com/xuri/excelize/v2" ) @@ -104,12 +105,11 @@ func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { //总量生成excel if len(total) > 0 { filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" - err = b.SimpleFillExcel(b.excelTempDir+"/"+"kshj_total_v2.xlsx", filePath, total) + err = b.SimpleFillExcel(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, 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) err = b.resellerDetailFillExcel(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) } return err @@ -126,6 +126,20 @@ func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice i sheet := f.GetSheetName(0) + // 1.1 获取第二行模板样式 + resellerTplRow := 2 + styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow)) + if err != nil { + log.Errorf("获取分销商总计样式失败: %v", err) + styleIDReseller = 0 + } + // 1.2 获取分销商总计行高 + rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow) + if err != nil { + log.Errorf("获取分销商总计行高失败: %v", err) + rowHeightReseller = 31 // 默认高度 + } + // 2. 反射获取切片数据 v := reflect.ValueOf(dataSlice) if v.Kind() != reflect.Slice { @@ -157,12 +171,19 @@ func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice i } else { rowData = []interface{}{item} } + // 4.1 设置行高 + f.SetRowHeight(sheet, currentRow, rowHeightReseller) // 5. 填充到Excel for col, value := range rowData { cell := fmt.Sprintf("%c%d", 'A'+col, currentRow) f.SetCellValue(sheet, cell, value) } + + // 5.1 使用第二行模板样式 + if styleIDReseller != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller) + } } // 6. 保存 @@ -188,22 +209,24 @@ func (b *BbxtTools) resellerDetailFillExcel(templatePath, outputPath string, dat resellerTplRow := 2 styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow)) if err != nil { - // 如果获取失败,就不应用样式,或者记录日志,这里选择忽略错误继续 + log.Errorf("获取分销商总计样式失败: %v", err) styleIDReseller = 0 } rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow) if err != nil { + log.Errorf("获取分销商总计行高失败: %v", err) rowHeightReseller = 31 // 默认高度 } // 获取模板样式2:第三行-产品亏损明细 productTplRow := 3 styleIDProduct, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", productTplRow)) if err != nil { - // 如果获取失败,就不应用样式,或者记录日志,这里选择忽略错误继续 + log.Errorf("获取商品详情样式失败: %v", err) styleIDProduct = 0 } rowHeightProduct, err := f.GetRowHeight(sheet, productTplRow) if err != nil { + log.Errorf("获取商品详情行高失败: %v", err) rowHeightProduct = 25 // 默认高度 } diff --git a/tmpl/excel_temp/kshj_gt.xlsx b/tmpl/excel_temp/kshj_gt.xlsx index 9ab76f5e450a7e10ea61eded595909e5f46182a2..c826635797dcc5b89a8eb2b89a85592c445b16c9 100755 GIT binary patch delta 1709 zcmV;e22%O`Nr6hRr2z%X6x`W2ld1t8f5}e6Fcd}iNc=WWH81cBID z_G@bq53&tu{~k9@LlG=kTKD+)zUOMTQC;#0&P$_flxGAbpt%uJuWIz%&eJQD_`tMa zozYOEEqF9*PERUsDK`!tow4A81fN8c_LSQitwXRBW6#&1n9m}iW4JU?+x8VD5qBmYnVBhyyUk){5{~W(A9-f9#Q>k~m;((gnemdsEBCc{NLELVe8=<6@ zJ~$SPtMG525`3sgxdRqJB=J$YH?Md|Udp@sc8;2Y6z6GPq{VehE@?q1A#W8vP~-h( z5~{JqKa3@5Nlwc%njaevf@bLb^W&Q@v-|?k0S*Ic%-Vqh006iJ000;Olg%U=e~naK zZ`&XgeV?@dfOxO5ois_KI8~FX+N4dY_W4Zkb&QIQ2PE$P{Vou@sWMf4u??Ji&bc3W zb^F{Xas*?gZVQ?vDJ9SfUCMS}(4RjZt`?M7$J>%C-9kbCf~B|Xj~`aOHcz$&a0D4z zThQ9MZpoMxH8k8Nx`P%^Ds39>e{gB`%ytHN8G;(c@-&^XhRc>lhNXEAhps9q;9iSE z1FefB1{8Pb-`29T1KV@?F22(69&;W1oLRh&CnH}rz3|saVYJniON4G%w04>04ZC@j zXCp3%{!^@mo2Nr}g~S~ux06b`zaibh5V#kfzVDO1vx#U=F3t*+%^A4uf5AH4+;hk2 zdKEhGXxMsHO01%9Juy%f^e$U&CKUGr;+KTpo_!O?ci%yQa8Tl?P~t~mr}dKue#WiT za|eKD+}92qPCeT9C758%W7Jp_Y(mHVWzCj3HcNQ)9m4$%B^YCI_qa}1f9!}Y5hujv5|NDv zhN7b~W*n3;;62I+M`e7D%K69}yln=AcXfq-jA=5Pj*wTr-DTB4vr+BBJC-%OmxxNy1)mELKG*D-bqe~Ex?zyYJA z(%KCR9LqYKJ%mkv24BX1?)o2-aReHZ&>{r0kOx`_2|s~;BoG1s05y~I4<&yjZHBaJ zI%=kk7->2}rL?d@Zt~Jtlh~2%k|Kn}3b9}@i>~wFouL6t zdMHsUA7U)S1TrGKJO`$~#9V)5MCq;AVwnpdkxT$omSWp9FK|XELp5P2+Aw$=(-5XS z+-1O24H1wMrQ4SY&1FN2Hq}LEW&xXSk;`t#FqG$P3!fxtJc?-wtJ(vDs9b-a{3=DIZz~Fg zeAJg)qo6F}#hOMc@sRDhrcR@-JqNl?ln%(Y9j8R#-x$?PHLF?;R(1dL@acH?e!Tzs z@Z)g&^7-N0z0R?;{#rL{>CT5#{}9)aRnZzmNYb!uT`!ezV43!yRKQ-{NL8)IPEZeh z@EzATdjq@E53Hcmw+4S^$M-M#9cLQ!oM+bbYUTtVn>h9*F+Qi!Oi4yV!6lDXH{=;0;pd&f~c9Yv9HUXWJ5+o)YKY@NE5CQ-IH3R?v4*&oF000000RSKX z007{VQzSeAlar7nAsYi}%-Vqh006iJ000;O0000000031AOHXWzmv=)D+c5s00000 D-T@zK delta 1702 zcmV;X23h%mO8rT&r2z#-9D=Vlld1t8e_c<)FcgOGP5ci_@3(dXQJQVUk8mZ4i4cu< zO?!aG^&@R{@b4{SOpIQ5*}PAmzUQ=@?NpO|g7ebo3gsC=321JF)SC*emhdT9gTgr`tCuc0UAi*cmq&?-fLfa55#n|&LDCV%@C?RhpK2iPs zdJw9<#XpTxI-XE6J~N&Kb=Ujn$Jbx8{Q}Sd4j{Y=wtfNt0I&rB02lz1(IgswjZ|H4 z+AtJ-pS1sA`JFfk6et3!eyG}{O{%)DXXD%u3n$KOL)w48Ynucrrm8PaVxN1?xgXB* z=A~BT0LDt)tZ0^`lt3eNA)D=r{`&binp0vOZwjt-11tIpmfoyCe_D3h>}&<#2r@Kw zMJwmp1!GoJP;;B;78*P$wW+y(!%wqiwl%G~(xD!v;bxGISL^MYiX9db;4BY*H-a1`BaL4I- z89MM}*m_w=tfFr{F;K4PZML`@Q``@T-x4}|_D&q%`~U^QL4l(}i64QD);kaUid(7Y z4gk-{g_8%k2c?ks3a99A)VT5jY*09V@6{f|&^#GZK*{&Y-D~wn7Oq0oG|A^zvuqOA zG3_DGy88lhTRFs_nBF^oB^&q#2T*vlqWRlBWb`SZ6QLE#;!0}iZvZ7Ve+kbnO3u^d zB$&^ql!#Ew-qDdHhNDaf$LBG8I!lgbqvdzQF`p2QBFFF?leyGm(RC=IZ|Zt!bVqRD z(Y`Oi7_V`R8j6AqXt}?q*&@e236H)-xZj}!V=V3-*6ETRuqEPuh`75%WCMbs=%9=l z2W1TSh%&-a8P`EMA9#beyB^_PjqqnNNv4wl@FmyZ#500UjZK7PLUb$qeUQtKTENjdY7)3%WXEaVYoTMxZj1+4+H zq#`ULDQDoJ_aM>5aWAD z-9dPAw&jJew2M%oRtc4xlV3(;d|Oi}L}%gjY@J3`dk%D)C=d1A zzHJEjH-AL=P>m{AiL21Na!Oi_ z2uT{6&h=6XhmPGD8U^gtwN&M5?F9AE2j3m|c6Zom^#Uho^_-#I^8J3V<<5ex`^=eL z&fVZ+oyOh}<8v7GkYqFzT=H18L!RL(+rP%d8Cu}-G!V5Ddio~giZ(LU;8Z;qGc1EFZJ}N&`=z#8wxLP7OO==?e%>1b`vxePZ$1d+FUlh7j$0dkYyBQ^n w0g{uHBq19hyb88{0ssK81pojT000000000103ZMW0KSvZBr68sAOHXW09#!SRR910 diff --git a/tmpl/excel_temp/kshj_total.xlsx b/tmpl/excel_temp/kshj_total.xlsx old mode 100644 new mode 100755 index 2035ef939d202a1570c39cb8884ac4a456927e40..b29ae47efc9514fb4d9a8dabfbee8e2afc199b5f GIT binary patch delta 4030 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 From 3a77e0e32b66e825ede5f54728e6db2e5fa0f946 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Tue, 30 Dec 2025 10:22:19 +0800 Subject: [PATCH 04/27] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E9=A2=9D?= =?UTF-8?q?=E5=A4=96=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tools/bbxt/bbxt.go | 180 ++-------------------------- internal/tools/bbxt/bbxt_test.go | 7 +- internal/tools/bbxt/excel.go | 194 +++++++++++++++++++++++++++++++ tmpl/excel_temp/kshj_gt.xlsx | Bin 9473 -> 9491 bytes 4 files changed, 210 insertions(+), 171 deletions(-) create mode 100644 internal/tools/bbxt/excel.go diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index d7d00ef..6d593db 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -3,12 +3,8 @@ package bbxt import ( "ai_scheduler/internal/pkg" "fmt" - "reflect" "sort" "time" - - "github.com/gofiber/fiber/v2/log" - "github.com/xuri/excelize/v2" ) type BbxtTools struct { @@ -91,7 +87,18 @@ 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 + }) + + // 构建分组 + for _, v := range resellers { if v.Total <= -100 { total = append(total, []string{ fmt.Sprintf("%s", v.ResellerName), @@ -114,168 +121,3 @@ func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { } return err } - -// 最简单的通用函数 -func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice interface{}) error { - // 1. 打开模板 - f, err := excelize.OpenFile(templatePath) - if err != nil { - return err - } - defer f.Close() - - sheet := f.GetSheetName(0) - - // 1.1 获取第二行模板样式 - resellerTplRow := 2 - styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow)) - if err != nil { - log.Errorf("获取分销商总计样式失败: %v", err) - styleIDReseller = 0 - } - // 1.2 获取分销商总计行高 - rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow) - if err != nil { - log.Errorf("获取分销商总计行高失败: %v", err) - rowHeightReseller = 31 // 默认高度 - } - - // 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} - } - // 4.1 设置行高 - f.SetRowHeight(sheet, currentRow, rowHeightReseller) - - // 5. 填充到Excel - for col, value := range rowData { - cell := fmt.Sprintf("%c%d", 'A'+col, currentRow) - f.SetCellValue(sheet, cell, value) - } - - // 5.1 使用第二行模板样式 - if styleIDReseller != 0 { - f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller) - } - } - - // 6. 保存 - return f.SaveAs(outputPath) -} - -// 分销商负利润详情填充excel -// 1.使用模板文件作为输出文件 -// 2.分销商总计使用第二行样式(宽高、背景、颜色等) -// 3.商品详情使用第三行样式(宽高、背景、颜色等) -// 4.保存为新文件 -func (b *BbxtTools) resellerDetailFillExcel(templatePath, outputPath string, dataSlice []*ResellerLoss) error { - // 1. 读取模板 - f, err := excelize.OpenFile(templatePath) - if err != nil { - return err - } - defer f.Close() - - sheet := f.GetSheetName(0) - - // 获取模板样式1:第二行-分销商总计 - resellerTplRow := 2 - styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow)) - if err != nil { - log.Errorf("获取分销商总计样式失败: %v", err) - styleIDReseller = 0 - } - rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow) - if err != nil { - log.Errorf("获取分销商总计行高失败: %v", err) - rowHeightReseller = 31 // 默认高度 - } - // 获取模板样式2:第三行-产品亏损明细 - productTplRow := 3 - styleIDProduct, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", productTplRow)) - if err != nil { - log.Errorf("获取商品详情样式失败: %v", err) - styleIDProduct = 0 - } - rowHeightProduct, err := f.GetRowHeight(sheet, productTplRow) - if err != nil { - log.Errorf("获取商品详情行高失败: %v", err) - rowHeightProduct = 25 // 默认高度 - } - - currentRow := 2 - - for _, reseller := range dataSlice { - // 3. 填充经销商数据 (ResellerName, Total) - // 设置行高 - f.SetRowHeight(sheet, currentRow, rowHeightReseller) - - // 设置单元格值 - f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), reseller.ResellerName) - f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), reseller.Total) - - // 应用样式 - if styleIDReseller != 0 { - f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller) - } - - currentRow++ - - // 4. 填充产品亏损明细 - // 先对 ProductLoss 进行排序 - var products []ProductLoss - for _, p := range reseller.ProductLoss { - products = append(products, p) - } - // 按 Loss 升序排序 (亏损越多越靠前,负数越小) - sort.Slice(products, func(i, j int) bool { - return products[i].Loss < products[j].Loss - }) - - for _, p := range products { - // 设置行高 - f.SetRowHeight(sheet, currentRow, rowHeightProduct) - - // 设置单元格值 - f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), p.ProductName) - f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.Loss) - - // 应用样式 - if styleIDProduct != 0 { - f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDProduct) - } - - currentRow++ - } - } - - // 6. 保存 - return f.SaveAs(outputPath) -} diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go index 628c675..b6b2275 100644 --- a/internal/tools/bbxt/bbxt_test.go +++ b/internal/tools/bbxt/bbxt_test.go @@ -1,13 +1,16 @@ package bbxt -import "testing" +import ( + "testing" + "time" +) func Test_StatisOursProductLossSumApiTotal(t *testing.T) { o, err := NewBbxtTools() if err != nil { panic(err) } - err = o.StatisOursProductLossSumTotal([]string{"2025-12-28+00:00:00", "2025-12-28+23:59:59.999"}) + err = o.DailyReport(time.Date(2025, 12, 30, 0, 0, 0, 0, time.Local)) t.Log(err) diff --git a/internal/tools/bbxt/excel.go b/internal/tools/bbxt/excel.go new file mode 100644 index 0000000..3711aa0 --- /dev/null +++ b/internal/tools/bbxt/excel.go @@ -0,0 +1,194 @@ +package bbxt + +import ( + "fmt" + "reflect" + "sort" + + "github.com/go-kratos/kratos/v2/log" + "github.com/xuri/excelize/v2" +) + +// 最简单的通用函数 +func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice interface{}) error { + // 1. 打开模板 + f, err := excelize.OpenFile(templatePath) + if err != nil { + return err + } + defer f.Close() + + sheet := f.GetSheetName(0) + + // 1.1 获取第二行模板样式 + resellerTplRow := 2 + styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow)) + if err != nil { + log.Errorf("获取分销商总计样式失败: %v", err) + styleIDReseller = 0 + } + // 1.2 获取分销商总计行高 + rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow) + if err != nil { + log.Errorf("获取分销商总计行高失败: %v", err) + rowHeightReseller = 31 // 默认高度 + } + + // 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} + } + // 4.1 设置行高 + f.SetRowHeight(sheet, currentRow, rowHeightReseller) + + // 5. 填充到Excel + for col, value := range rowData { + cell := fmt.Sprintf("%c%d", 'A'+col, currentRow) + f.SetCellValue(sheet, cell, value) + } + + // 5.1 使用第二行模板样式 + if styleIDReseller != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller) + } + } + + // 6. 保存 + return f.SaveAs(outputPath) +} + +// 分销商负利润详情填充excel +// 1.使用模板文件作为输出文件 +// 2.分销商总计使用第二行样式(宽高、背景、颜色等) +// 3.商品详情使用第三行样式(宽高、背景、颜色等) +// 4.保存为新文件 +func (b *BbxtTools) resellerDetailFillExcel(templatePath, outputPath string, dataSlice []*ResellerLoss) error { + // 1. 读取模板 + f, err := excelize.OpenFile(templatePath) + if err != nil { + return err + } + defer f.Close() + + sheet := f.GetSheetName(0) + + // 获取模板样式1:第二行-分销商总计 + resellerTplRow := 2 + styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow)) + if err != nil { + log.Errorf("获取分销商总计样式失败: %v", err) + styleIDReseller = 0 + } + rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow) + if err != nil { + log.Errorf("获取分销商总计行高失败: %v", err) + rowHeightReseller = 31 // 默认高度 + } + // 获取模板样式2:第三行-产品亏损明细 + productTplRow := 3 + styleIDProduct, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", productTplRow)) + if err != nil { + log.Errorf("获取商品详情样式失败: %v", err) + styleIDProduct = 0 + } + rowHeightProduct, err := f.GetRowHeight(sheet, productTplRow) + if err != nil { + log.Errorf("获取商品详情行高失败: %v", err) + rowHeightProduct = 25 // 默认高度 + } + + currentRow := 2 + + for _, reseller := range dataSlice { + // 3. 填充经销商数据 (ResellerName, Total) + // 设置行高 + f.SetRowHeight(sheet, currentRow, rowHeightReseller) + + // 设置单元格值 + f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), reseller.ResellerName) + f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), reseller.Total) + + // 应用样式 + if styleIDReseller != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller) + } + + currentRow++ + + // 4. 填充产品亏损明细 + // 先对 ProductLoss 进行排序 + var products []ProductLoss + for _, p := range reseller.ProductLoss { + products = append(products, p) + } + // 按 Loss 升序排序 (亏损越多越靠前,负数越小) + sort.Slice(products, func(i, j int) bool { + return products[i].Loss < products[j].Loss + }) + + for _, p := range products { + // 设置行高 + f.SetRowHeight(sheet, currentRow, rowHeightProduct) + + // 设置单元格值 + f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("·%s", p.ProductName)) + f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.Loss) + + // 应用样式 + if styleIDProduct != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDProduct) + } + + currentRow++ + } + } + + // buffer, err := f.WriteToBuffer() + // if err != nil { + // return err + // } + + // return buffer.Bytes(), nil + + // 6. 保存 + return f.SaveAs(outputPath) +} + +// 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 (b *BbxtTools) excel2picPy(templatePath string, excelBytes []byte) ([]byte, error) { + + return nil, nil + // return picBytes, nil +} diff --git a/tmpl/excel_temp/kshj_gt.xlsx b/tmpl/excel_temp/kshj_gt.xlsx index c826635797dcc5b89a8eb2b89a85592c445b16c9..6933f633648e61451b2be2ea5d6371174a19dcf2 100755 GIT binary patch delta 4202 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 MK={HA&p*t60ON@J%>V!Z delta 4159 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 Date: Tue, 30 Dec 2025 10:52:30 +0800 Subject: [PATCH 05/27] =?UTF-8?q?feat:=20=20=E5=A2=9E=E5=8A=A0=E8=B4=9F?= =?UTF-8?q?=E5=88=A9=E6=B6=A6=E5=AF=BC=E5=87=BAV2=E7=89=88=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E5=90=88=E5=B9=B6=E5=8D=95=E5=85=83=E6=A0=BC=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tools/bbxt/bbxt.go | 3 +- internal/tools/bbxt/excel.go | 131 ++++++++++++++++++++++++++++++++ tmpl/excel_temp/kshj_gt.xlsx | Bin 9491 -> 9905 bytes tmpl/excel_temp/kshj_gt.xlsx.v1 | Bin 0 -> 9491 bytes 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100755 tmpl/excel_temp/kshj_gt.xlsx.v1 diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index f7d1e68..77a5f29 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -117,7 +117,8 @@ func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { if len(gt) > 0 { filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" - err = b.resellerDetailFillExcel(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) + // err = b.resellerDetailFillExcel(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) + err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) } return err } diff --git a/internal/tools/bbxt/excel.go b/internal/tools/bbxt/excel.go index 3711aa0..807680b 100644 --- a/internal/tools/bbxt/excel.go +++ b/internal/tools/bbxt/excel.go @@ -181,6 +181,137 @@ func (b *BbxtTools) resellerDetailFillExcel(templatePath, outputPath string, dat return f.SaveAs(outputPath) } +// 分销商负利润详情填充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)) + } + } + + // ---------------- 填充合计行 ---------------- + // 设置行高 + 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) +} + // excel2picPy 将excel转换为图片python // python 接口如下: // curl --location --request POST 'http://192.168.6.109:8010/api/v1/convert' \ diff --git a/tmpl/excel_temp/kshj_gt.xlsx b/tmpl/excel_temp/kshj_gt.xlsx index 6933f633648e61451b2be2ea5d6371174a19dcf2..e270a67aaa52be82e3c03c824e02b53b180b36d8 100755 GIT binary patch delta 7919 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

;^J9-Az?fkgaVmvkfJ?!dvnoj-%K!EOSwq+S zAg%o_HHQ0mLacPP*6aBox3;SG&sF+%2;(YWZi53(4)U#M8!cIX?J9p%)I^`3+uopp zGA-=?@$E0sQKf!t3~mK_(R3WO_iSaBCf=VCVox2Yzgg(Lxtj9&Pnlf zK+sG}p7csY(xjz>m}k7Nn_4uAo-%8u6I6PIl347OahNMHQ&{u)_GrZ-47H@$$yaU6 z`z=ddsKB0UXRt_9p6@V%ZW0ye2398CJYwVFqE-8e8Hyfb`?_$uf}Cudo@cx<(^;b4 z;u5t6%3TkYs%apfcMh^(2c!Rb$68np;3@ql-gQY_bGz&Udw*J(+mvlR&5V9Ql4I+w zIbij^y^g5!**?x~H4OtQMYvl%etsGKbUjFaf)E!I6aiJ}x^T7DF6OHX=f3pxpOo+o z+YbJv@+IX!>^b_&Y#{vgNK02y4jOUz{4)6nDdk$^@73lf$2HmtpE^ zENzzV4#}7G-L~$cu32*_dNbzW*y|$!%&%sVz*mhlC2VqCL2+wV1Pa=EX+usG3sP8x zPyUCvrrNO8rD&wOROO6Zq9>*dfLO4u7SgQ-;zvp1ulwqJF5)ojV}fH|&;QzCg6hAB<URLv>78!bmC=9{D`@#v6gY11(^_TKF5aoLL3t-a5FzAz#dc@h4E{3 zX(@6u!bwDysw?B+kb;>WJWtW56z$!HU!>;pVXx_}b;Nv{Gm_N`U!YuEKk)~AOixU2 zAMP}&XH2g}{zPn2&)Gel_voy-x@|~?HN*`bL|=?J)s0g)EbL3kiXwe*@ zW=V&0g~D;&$D07ya>cjo;%{Rb@Ldl?pA0!a3y&*38TG~Q)G>}t^xh(_0 zX{qqXoOj$?3R8A3@%X|5TkFrl;0_;_d1s*LO2(9*CGCDzl;RGsK%HUDn zzY=v-@6v0-T8Q?b{Ibq8i;fl-kYr&xV8M0sv0Bkr>(ph`bLvTJ0~zyNriiq5;y|2bMl#OQj5`l|njYU^%5T4+yAYPpi4a!M~ zrY%W`-m-ByS|R}J(+-r-C=X~Rn`t`~`x{$DqCxlQKDjdzw2! zJmTf;(gR4WQCUw^+Gd9-(jUs9-oE>KjsDJ};L85B zO#$GCD85+Zlz2>Tn%M=DtvXh%Rxxi{MAO~$U=%mFb!Yo;B%?cPh;Bca zHy8KZ!W@>kxp$GyKd(Q6@#?-QbTUF9(!U;ihLllG$&P~#!NR^O_KY`kG9$S@`K{%& zLw^v)IWt8WuNty*)=;Thv?t<65%d-lR|9~{)zTn0kVqMsz#os#&8&m890V3_jIHKG z;A3s>((g^-D3|e3()1s-XTYiM(k?{R!I(b0OOqkW*Kw@LN7xK0?|D70lRUrhS7j?3 z+9y$I)(`2oT(w8iRg-bC6_X*&98II6)^4|m0v8>Ks@rr=QtLr%m4#aVW_`U!JmQ&+&`@^htcQ10BqdtxVQBM zq>c3c7zb(&u;12*8>qHU0zky)&z)Ckr(Zf+l#=HE;%*k>rn=_-s(~X;YoMh-6&A<% z6Xzz-OLr|m{AU@z{+fW%sl)ezR~Ql{F~*;ab0oh|opTzn%5vjd2f-{Fbrnn_$0S(I z%xcW1f^G)Wf^J)QG~WqN$mdKOYrZ2X`olh$6>i+rigG74`A+%G_n?-PjY3= zcwhB)p5r{@YW_g?lmmG`FKs7DwJjYFcL=<3nXs>x2?B4G)}sN*Hrk&{cpN401TMeE zk6XzRM@}$@mPz2@;24q$7ocU|$%#pHV8@;JzPW$sy>N|w^(hKf?LN8~nc-Rj8+sc3 z@ii&l=zf8Dt^}4cSdHZ(eb;}`mS#2oaewOK>7>l>WL)Bgqx6>td=wS>fPZq*k7+{= zVr%)ua=Yga;r2&hU5YYKTZTbKO`S#@bLBi)Gdwvf*ps~Zuc<<`Dfi%eyYB@O=*nOY z78wpT$@_~V8b7%08D^aCl0nZ$nKb;1)B9-Fka`04UvaHx$%5M090HyWx-8n<2lrr5k{{XYHx#G0)-`xeL3+39}2e=&N9 zzG)KiQkEE?6W~zA(s_m7#9v}q^;t$vsz|n zDBxvriZOJ=2Eue|zb0X4B+_uhS{Ql4y6H3FMYqerpYv#6+V`$hydf0P(- zggjW-36(?k1d5|=S4HelUq!DXejCLzu4&0l zc+|(q%VclIakY-zPlRErvVhaK+qOkCjuZ3=d$ta3lRM$<`*s49R}-cUoWKHMufYdU zw%xb65TYLdM4Ht0J4ZBQ0zt^9F^L%QtU%;O5 zRVjaI@m$zy%q|@J%Traf;=smKhs~|uAe7ACqSs-!etijwYTD&*ZH$usN&(!@{U#vZ z2|y~pr2iWuYq?EMf+>~U0Fp7GE% z31owtZTtOZcLdGm2c0P>p?sF|)3yD?UVY|c>rj|fvA0OAHD5lmUgaFjsAP_1-upv$ zMKc?0%e9)31Qup$xSmOo;OHdGT0bS#t0wqPxQq)YT&Z+tpl8UR&nf6H6Lg&i^sic$ z3(R>Hi7`{u6sCcT0Kqg=F}Kiqn$9eIebhp!h&SoHh*?ujnVEMBP4QS(Rv5e554Ott z*@GofBd-+yL!FEIhOlTTwQRYIN_;F9bw3Bn?WBlrU81EN*0U^~!@E0C&d44go?b5Ez_lnc{Cfb|ylL{E!6q@?COBPNqKZc<7jAw1ARKy;D_Hq;O+ zIzEILYDgyCyMH>kkPA9NivQn)d}-j|z0^#94|q7*7mR?&ffy+N-Ant&e~JJJ2T7t@ zy%gXt+6E+$ffCX~_X?qb266=wL|CMOaME*7{#yn4XZZs*#D|`V^4~SvKLPf5kQVya z@Sh=P^pq6;mf-&hn}M(}Fj4$l4*17!0C8XtgAa$4G0;)|)2V`kd(ra$Gyc2TdL+mi Q5;^3N0T(3=^!NP#0kH67J%~inc&;UnuTQVS(aSpg2VqclU+jQfzT4(4s{OEKn%!?kw)^F2x`H z-}ml)_vW16!5p7hM~*=c6P703aGKMgpl+K;k6q_Go)r?OiHh zrBQcg`W#-TZRr;4aXi;*fS<5ECL|aU1PY6x5qP2O?t0d88u>`Hb@cA%-o{mvZ|w1d zSUW0=1>3no+4@z2eFF$lH9jUkJ$S>_cV><@5R_{YZ;e8o7DS8G+|(7SMOp!DLlKj# zk5j}`%2p>*qJ|KZAKGnZNzE(fP#y;F(Z%DD?d;GXg;Y=P9;l~M7ZI-|-(b+WnTYp1 zg?0-u-z>~}+KX^l+;!)1;qU03jS&K3-$(ON&fut+v{KZ@T$6c!)12=O0B~OIHkwVb<4Smssa)+ zApih)0SHrGdq=;m-6{0!Txn<~0tqDEK&69-v=f5Pzma*p+8Ab1JEnAoGF&p12poAK-x{vP3}YTs5LGDzo(7Wk zWlX(`s9hN=6%v0N4F2YJvZ0H~20W3YBN+*19TP$x;J7luPQPOjQY1_%n}CRPmgpG| zbfFm`6a?!VV+Sfw2rMCd5XK)1NHmd}Q<=U1FmdqYv?0fyv-G@o02DrCBQ-g%O)}fs zEm(#7^e3CJ#~gk6_`P!fcWK52r^zE(3oAwTo#qFyy8-$;czJf{T}8Mi+pFFNJGZwr zT>WT0pnZ({c~#l5_h#(~I8+emAK?6Z5#UegU6^+av-}P?eIjvbawA=>{e>6%NgMCr zM_N@Jrbmg)xOCOmBV8B4o&XKMqijkeKcxS5>-+uB8&c114Mqe22%ep7=3uJo?BM9a zZtCXZ>R`v_X=huLu&ffqfg7-_bWbU}oSUC$#{y|ZM8RJVlG!%IW&+a&CRc-h6F+$f zZ{MH(I6X{n@Vz!TUW1>H3j371GD;)*;vj@!GGr**5}Z_&FQ7P97fXM*RzE=|t3xfh zIHPISbl&K9)fawE?TYB$9j)+!cm6bY>Z=lFhE@I>Ytw9NOz^yWG*N;Q5)rCNuz8%) zTrZ@JT-<4;Q}1ZVA^wZmd*LP>N|}<3ZpxFOiI)X7_$TVY)j&zY-*wSN`?TU7->K3Y zECX?eIm^D4`l1YbvUIoNU4DAs@{M$0E(0tsX7ycN=p0j$OA%%%AuABOg;ES0vXts? zEbaYH*?CeT5^C{990SE7cmIx5*n*oAs$#&sY+HA`8m1O*Kf)6KmV-0Ix+ zE#Q4zZl#T`;4~Wv?Azz`d8uEcD{{q3LlAQRbaGf3f?;ruZl#ElMd?;mZoILTSbOe( zI!4{Ji4ka>CIX?0{PtIp?P2Scx^MVTj>ZmsG;zk)$LoE)DM};vaCKrkw_W^Po7ZJo z7G7QDOB*^-+~1xEOSZPWmJO+#qWbLe6OMTEgozP7HgI>5Hq)2P2IkSb9s>Tb=E4|{J5LXBRH?`a1(*~6;LTV+ z90VFUy}evOSRtu*JOg;-Br*;=ML%BA)Tey<-o1O5bB)`*H5P{`-~91NxkS9m@zq{| zTytHf#YCO*4!~=CktnXApL#0+zX{?Y&v%0TGUvonX{>FRg6o!UG-4D3>_lI4;SrFK z-V$sH71NoujV;$HTT$2kR-K%0YO3 zdi-y{3xB4Kz4Ak6@QgQ|)FwQ5H}Ie|lq~2j?|FgU{_v8X*wpE|Lhj>104oGfbnS{P z{>zKdZ{NAh$r3qNX!~Eh2kXDz^xeQ-vPTQ{a0_gSvf|2!iQ^yF=4JO|f#1o|lI1&* z=QGkeBvRl8xq7paqX|qkZwmhz9#ZB@1_I;xl(uI03AQ=?1r|f)9cnrnh=oI*K&^Z$ z0cz^jhoLxR1I(GC!AcOLZc3!1Ez0NZNevxBBgANrLx^}p1ps=)6q#u=*QJLy?d9hwWcnn zl=C8T@@H#C+Tx|Mh@O|X{ht)}HWZphVM&3bAsh=y)cj=RXP966khnsVH1 z0qf|hA%zc-?v-+A>S-`f5QHhdt)|RlxjvF#Q_AG?JF)nTIua@Q{EWA6xq^*Z*v}K` zPV76SqhE4=0%D86wlRSt;807Z?<08Il zDUqQEkyB{!N-Bdh&|SlIxC6XG?~7DoGy{l}F${TvB5yFYYRk)4h9HXJ#VA$u_2oAM z8(e{_Z9mdJM5A@(!R^bOz=fj@0L7top)BQw53Cm?fR~_?$vO2Yoqc-^z4~GN-p9=8 z#q!CwyhKLhEqoR(-{ID9(ll1BVuac38o_%Ra8qs5-U@c_+P=#lU7LnQLyr?wv(x9s zQBt?}P#39cr=>FoBy0-d(4G4b#jJA{o81>_g)|9E3?#uvIGbNcd@9A-UqdXu@(mM! zZKgejTx=yTBM{7DzwSu|oFZ6J`mEq^pbFd$z0j8p{ao;aq%qs1C_JtC<4B9&QsW{|%Ohw#( z4^0wfh{gN#25R(G)a$gXr^Ww#25I@Vr)uc6l_K45rI=B%*fxGpw2dH1__^E#ts*%( zS)hIFUQ8s}${~}vn0{2<#v;L}h;QpY6fLH#Nmh~Ya?qDMl6-}2yR<7LB_PAQ9KYj1 zj_XQ=jRCv>5$g_XuAdihlAKS=8|6LEbFFiD-^u9`yDGKHDHkqzOD;~;xxVNpPj2X8 z1+3ek4LSNS**>Wtm;EwRDjj_>hqDvBcTOUC=m_({sS!B$Y6879pd&;!r&zxW7UVJP zAqX+e?iY*ENjuTgmKLM+`GA+~L|#laZrU!ehZT_s5la(Byk|vuUeoba|bYt?Sv9(BGV{3dd{yV$(x z#WlMvWK7sI;Q+5F@`irTF3f*EH|WVX%ur*1XgS5AjQDr;Qrt{a?W|W1CjSy`zqX1J zNj?40l3qI^&p~@mGIyA=I$7uV3;#X&P;{~d?N78p$R8=L73>UZ)s-^wZ9C^QI%1<@@)=tMeG*4z`Porvq=LOoKG>^zZf}|x z)+M$26J2eq#*MBWWCGg2N|`B5pV?&M+0J$!+3LNSVbMk!ALeV%>sq_lN!ZYDi#Ed* zffy*UFipQ!7**r8ocy?tfZ;TUCF3|AlI?^c0rh4NpCvAjlBBnt`l=IEAZtOvf3Kq% zG7yD=vYzO~oHy6TRmI8kvr4s6YQ*f&sBGG`${-r)t9e!CD;v@{ny1uPxYqpDW;`^x z%xDcf-|AtpI*c0x(o*biC~^@p!u_yT(ICn-;vnYL_IAQ*|A2Ykp<1uatEtk7#N%qL z;TY3o8G-%eTX%+^0+{>{c@=FUNrOnsjkToP!^e^3f1;f8GrOO-KVCQk_+Qw7$OCcN zJWynQtEVFnF|Un=jE9FBAnZ5qlE<++f-rvu`qr87Jz+W*J^kXfa7(QU4a%7-G=xIeUO)-`TrvvRytH)DcYsC-E8`nYvZ#PGW0&h`>^CL>}*I}o!&1T#=) z{fS|5YQs}iyfhjNe!ViTYa=|B*#jxLTo4FfYwY3BKCn=)LzBFv)P5>@k@jGiOEf9A z9JUm>avrpaBcWw9Zh*}t%P)Ip1Gu9QwuH)+R85)-Qx54iYa5-&)TCY$&dybV*Zh>L zqLdPavpz%-$+>q|bkvaR$?{5Fxkj{)0>#5pTT^iiHXYP_PXLHbNWUHl%ponxeS}{X z$@6^L&87oz3TIUq4nNz#l(UU_66o*Df_F{?#36)=Gx$|TvS+MS5;mM!mLV(Y$D=D4 z=ANw99|t5oFKt&P+Xt~&8*urEti@1H{8(%jZY|L~=grEdf7dw|XrEaC_1Znf!AsDQ zs>Io#H5;T$)WqWT=?}@AD3FaTWf5YozWPhF|0d3sl`brs&xD!unLPiEQao+hT`Y~A z!Db&^ovrLGT>dg=Smn>&AvK@^EA#N+2uzb}l3P_>c1U4ZrEgH-mnH_b(?pG`I*?6n zP(g06Z$zO+^M@j&5(=uiaehN7UEJU&slb$H95y5m+|xz>AMbbhky`NzNU>GovZ@&4>)b-ro1vieb%-)46>Oydc6I$m?^jg;(og{=X5 zlH;ees~nA1!$n!mvTkKn96!hj`82-|Qo{sJY0ktiRP&x|_FHm`Hy4=}Qy11-$dnlZ zSM;1>g=30xGN3m4SWW&7lJbrR8)X)vkA;CnS)=Rx6!2?2~+oCqmuGAWel;_@S?$Lvd3Y>vDNe?ONYLw({IQV^r71kTc}!cRk|MglZ}G zbkG!CYzE~157B>bm##-s4POiZz)u|j!2ZAI$kodh{LcY`>)SajbmKi5<~#-FjQInZ z7f&~r2($F}=@1bTXadM{-survw?o6vVlZMYWSKDPob+p$$Z{O@en|urpzB|QAZ4{y zW5iYm_aw_Yx05ZSvjz#mKKe^zRY$U2@1)KIxJL#>8ej_s&gXlpt(x|;kG-d%N}77? zC6S^B-l7(D(;Cao#$|9#Fky9AQ|&(XVS%?<>|}}F@_qLfP{8?c|E`uCm}4Sf{o_u; zu|kdc%kR6Z98ultmj~4M0qhL6 zak(gWE;*Fs9ft2L zwkFD`73_Y)x|7ac4S6-lt5wFb4C62$(seXbHeg}haWqB-iS@Xx$2Y4{2feGSszFyF z4?M(S6;NwmCw-5`p-$3{gm2|c@4o!G;rjk6zF;CfLFv;*y>e{MjLj-_RyiCCdR9R2 zxPs+#m5Z%HqsezG6#)CJP}wC@*x3W0v98eqm%yz@B@Qv}A>*une!Xca*X(V1R%m*Z zO&wX=>S&eJ3h6b>e*vF!9JE>b7|@`Ya?7v8-9(*vmyKCY)>tf+`JYDo zgZ{X_9>jSFb3%VWaH!Xx?O(vG1zmr;(9L9hn}5R^zfl896*tq(&BRl9XzzZen%|ya z#qN`j9Jg^5;Ij=`fju?Mr)&8P8Yo*qCC3Mc(3U`tcNE4=Q~@C^WkX*lmd-=%h+Znh zOI@VMw%;v^l=)jqG~I&xtEPF{G(pl@xI+|-pp zA$k4aOFTKVb3Amji|6_hk1URpDsrRsNN`;ubLEdt;MiwJuoLl!70nT*;+cT?)S&ZF z>5k^>^U@<}|6a@fk1|ySxN>F+)cC!rl5|dZVrM!JrD8M@wqi7a(U9k#l*`KPFUF*x z!Jf{!kimfvE;(_cNLdjBAQV20awu)zQ}t;qvE1vOhu>FsCOalcAEN`9Kh>2c&rw80m*C7A$&N2hdww0O`rB@l>`2}onmHMsR0rDEdi!@g~R49!wa6?`G zZlzVU>YXrrXm~{n;$N#21r%@D67uN?{DQJ{*3^20>e_%?Tv>4$xG3q4hoYQv5WDj^ zcxY+~=MJX2`!ZGm;T0;A8gnFt#_#PNb!2$nd@37?#w4LJo0a|d+egpQO24QhxAmbO zUAJ|~aAVz#1Zo*Ote0LMXwTQ2rhIhBku5s;>qw?S3H_?n=lk!19;7I==(YJa1Vylx z$8$0f6#7jhwhnP)6QAhn^D7FPV$7;ekdbO4us{MC(JZv?Gxh7eN**o_tnV1S(H=h> zxzNrMX2ZR`fgi{SGdIvMk~|SK%Ht(9$s%!LpF4<(tGQlunZE?uBt01DE(YzX6NVR! zBItgdyUFT~kQu-;Cf zl$$Of!-R#05rTiN4jc)QW}4iW%LHF!A>?f!=BGVMrUCzh}-a4s6qZglKww+e;q80>9JYCZWI=#MO;SwT@F1^#ZffD{DYdUCnyu4nCO^bdI~(a z=Gt$>V<{wwqx$x%lPHBmw^zpoGl$s%6$WY=K2A|6pMUtgn28DV2Bc}bK*;NC<6`VC zoFc1oy`60=)fzp}$K$Jb6aUR7`>@!p9UW@Dq{#vOndP#a<@UvLEQ5+a1v;*?v+xCW zj*c+<;~%VO#U~0d z2{L2pK>73rzjlXCRqSzKgP&3n0ug!m;}B0qVn8K$#e7|wuM#sN_5h-jC=`ef=eaM8 z6+?2In2(jRab1fa(8{|{Rd#yQoWKrSzqxt%&zL%T3;zxiU##qWszuVK@ zpJLn9As!_t*?C{I*&zx#AGUtr)gvO=$?*(zSxY?ju z-H9el4=$%;epkuCoP+T;&YAC@70gB^!ix3(&a0I-sPqNndB`uxv;j(>u_a}k3ii0t z;^_>yi-@E>jyBF(*ZM{3!{Oj?2*rrTz zTl2^Zy*geO(~Wi_{D;w5+``-l2e?R~vC=H!Bwj1(4!KJDK0wN&*T7mWk>n{WVzaSyof>+&;dFY zvi}W9|Dyh7Z}26QfS!Tzf4TO5095$@ieUd?-q0}`Drf-xO9XdlEIk?7e;$PYl_L7j zs4O(-G1@EW0zEG2zoWkA)#Cg`JePmjUWcMFh#(|Gr5LD5|2y^Z|D)QW0Ss?Zuc-d^ F{U5~S37G%@ diff --git a/tmpl/excel_temp/kshj_gt.xlsx.v1 b/tmpl/excel_temp/kshj_gt.xlsx.v1 new file mode 100755 index 0000000000000000000000000000000000000000..6933f633648e61451b2be2ea5d6371174a19dcf2 GIT binary patch literal 9491 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 fKWDA=KX`uy0mw?g{@ASOkf<7X2;Dj11K Date: Tue, 30 Dec 2025 12:00:22 +0800 Subject: [PATCH 06/27] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0excel2picPy?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=9B=BE=E7=89=87=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tools/bbxt/excel.go | 94 +++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/internal/tools/bbxt/excel.go b/internal/tools/bbxt/excel.go index 807680b..4ba932d 100644 --- a/internal/tools/bbxt/excel.go +++ b/internal/tools/bbxt/excel.go @@ -1,7 +1,13 @@ package bbxt import ( + "bytes" "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "path/filepath" "reflect" "sort" @@ -308,6 +314,17 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d // 取消合并合计行的A、B列 // f.MergeCell(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow)) + excelBytes, err := f.WriteToBuffer() + if err != nil { + return fmt.Errorf("write to bytes failed: %v", err) + } + + picBytes, err := b.excel2picPy(templatePath, excelBytes.Bytes()) + if err != nil { + return fmt.Errorf("excel2picPy failed: %v", err) + } + b.SavePic("temp.png", picBytes) + // 6. 保存 return f.SaveAs(outputPath) } @@ -319,7 +336,80 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d // --form 'file=@"C:\\Users\\Administrator\\Downloads\\销售同比分析2025-12-29 0-12点.xlsx"' \ // --form 'sheet_name="销售同比分析"' func (b *BbxtTools) excel2picPy(templatePath string, excelBytes []byte) ([]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() + } - return nil, nil - // return picBytes, nil + // 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) + } + + if err = writer.Close(); err != nil { + return nil, fmt.Errorf("close writer failed: %v", err) + } + + // 3. 发送 HTTP POST 请求 + url := "http://192.168.6.109:8010/api/v1/convert" + req, err := http.NewRequest("POST", url, 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 (b *BbxtTools) 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) } From 1db689bcd1c81a422c2c042d0a09ee8dab198d43 Mon Sep 17 00:00:00 2001 From: wuchao <1272174216@qq.com> Date: Tue, 30 Dec 2025 14:44:36 +0800 Subject: [PATCH 07/27] =?UTF-8?q?fix(zltx):=20=E4=BF=AE=E6=94=B9=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E7=BB=9F=E8=AE=A1=E5=B7=A5=E5=85=B7=E4=B8=ADnumber?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E7=B1=BB=E5=9E=8B=E4=B8=BAinterface{}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- internal/tools/zltx/zltx_statistics.go | 9 ++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index ea6e235..4dd0345 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ docs cmd/server/wire_gen.go __debug* -.bin/ \ No newline at end of file +.bin/ +.idea/ diff --git a/internal/tools/zltx/zltx_statistics.go b/internal/tools/zltx/zltx_statistics.go index 5d71a9b..29e143a 100644 --- a/internal/tools/zltx/zltx_statistics.go +++ b/internal/tools/zltx/zltx_statistics.go @@ -45,7 +45,7 @@ func (z ZltxOrderStatisticsTool) Definition() entitys.ToolDefinition { } type ZltxOrderStatisticsRequest struct { - Number string `json:"number"` + Number interface{} `json:"number"` } func (z ZltxOrderStatisticsTool) Execute(ctx context.Context, rec *entitys.Recognize) error { @@ -53,7 +53,7 @@ func (z ZltxOrderStatisticsTool) Execute(ctx context.Context, rec *entitys.Recog if err := json.Unmarshal([]byte(rec.Match.Parameters), &req); err != nil { return err } - if req.Number == "" { + if req.Number == nil { return fmt.Errorf("number is required") } return z.getZltxOrderStatistics(req.Number, rec) @@ -76,14 +76,13 @@ type ZltxOrderStatisticsData struct { Total int `json:"total"` } -func (z ZltxOrderStatisticsTool) getZltxOrderStatistics(number string, rec *entitys.Recognize) error { +func (z ZltxOrderStatisticsTool) getZltxOrderStatistics(number interface{}, rec *entitys.Recognize) error { ext, err := rec_extra.GetTaskRecExt(rec) if err != nil { return err } //查询订单详情 - - url := fmt.Sprintf("%s%s", z.config.BaseURL, number) + url := fmt.Sprintf("%s%s", z.config.BaseURL, fmt.Sprintf("%v", number)) req := l_request.Request{ Url: url, Headers: map[string]string{ From 87b9599ef89c34450eaabfdc830cc8a99573ca2c Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Tue, 30 Dec 2025 16:17:35 +0800 Subject: [PATCH 08/27] =?UTF-8?q?feat:=201.=E5=A2=9E=E5=8A=A0oss=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E6=96=B9=E6=B3=95=202.=E6=8A=A5=E8=A1=A8=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E6=88=AA=E5=9B=BE=E4=B8=8A=E4=BC=A0oss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config_env.yaml | 6 ++++ config/config_test.yaml | 6 ++++ go.mod | 3 ++ go.sum | 6 ++++ internal/config/config.go | 13 ++++++- internal/pkg/oss/client.go | 57 +++++++++++++++++++++++++++++++ internal/pkg/provider_set.go | 3 ++ internal/tools/bbxt/bbxt.go | 10 ++++-- internal/tools/bbxt/bbxt_test.go | 15 +++++++- internal/tools/bbxt/excel.go | 49 ++++++++++++++++++++------ tmpl/excel_temp/kshj_gt.xlsx | Bin 9905 -> 9938 bytes 11 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 internal/pkg/oss/client.go diff --git a/config/config_env.yaml b/config/config_env.yaml index 21c1faa..28030b6 100644 --- a/config/config_env.yaml +++ b/config/config_env.yaml @@ -43,6 +43,12 @@ redis: db: driver: mysql source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +oss: + access_key: "LTAI5tGGZzjf3tvqWk8SQj2G" + secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq" + bucket: "attachment-public" + domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com" + endpoint: "https://oss-cn-hangzhou.aliyuncs.com" tools: zltxOrderDetail: diff --git a/config/config_test.yaml b/config/config_test.yaml index 7ad9e71..63b4b66 100644 --- a/config/config_test.yaml +++ b/config/config_test.yaml @@ -43,6 +43,12 @@ redis: db: driver: mysql source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +oss: + access_key: "LTAI5tGGZzjf3tvqWk8SQj2G" + secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq" + bucket: "attachment-public" + domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com" + endpoint: "https://oss-cn-hangzhou.aliyuncs.com" tools: zltxOrderDetail: diff --git a/go.mod b/go.mod index fa8498d..374a7f3 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/alibabacloud-go/gateway-dingtalk v1.0.2 // indirect github.com/alibabacloud-go/openapi-util v0.1.1 // indirect github.com/alibabacloud-go/tea-xml v1.1.3 // indirect + github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible // indirect github.com/aliyun/credentials-go v1.4.6 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -95,6 +96,7 @@ require ( github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -120,6 +122,7 @@ require ( golang.org/x/net v0.46.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.5.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index b0d2c51..f101537 100644 --- a/go.sum +++ b/go.sum @@ -94,6 +94,8 @@ github.com/alibabacloud-go/tea-utils/v2 v2.0.6 h1:ZkmUlhlQbaDC+Eba/GARMPy6hKdCLi github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0= github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= @@ -392,6 +394,8 @@ github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWR github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -690,6 +694,8 @@ golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/internal/config/config.go b/internal/config/config.go index 35f2115..8c3a6d2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,8 +3,9 @@ package config import ( "ai_scheduler/pkg" "fmt" - "github.com/spf13/viper" "time" + + "github.com/spf13/viper" ) // Config 应用配置 @@ -19,6 +20,7 @@ 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"` @@ -136,6 +138,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"` diff --git a/internal/pkg/oss/client.go b/internal/pkg/oss/client.go new file mode 100644 index 0000000..225e8d9 --- /dev/null +++ b/internal/pkg/oss/client.go @@ -0,0 +1,57 @@ +package 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.Oss) (*Client, error) { + client, err := oss.New(cfg.Endpoint, cfg.AccessKey, cfg.SecretKey) + if err != nil { + return nil, fmt.Errorf("oss new client failed: %v", err) + } + + bucket, err := client.Bucket(cfg.Bucket) + if err != nil { + return nil, fmt.Errorf("oss get bucket failed: %v", err) + } + + return &Client{ + config: cfg, + client: client, + bucket: bucket, + }, nil +} + +// UploadBytes 上传字节数组到 OSS +// objectKey: OSS 中的文件路径,例如 "ai_scheduler/test.png" +// fileBytes: 文件内容 +// 返回: 文件的访问 URL +func (c *Client) UploadBytes(objectKey string, fileBytes []byte) (string, error) { + err := c.bucket.PutObject(objectKey, bytes.NewReader(fileBytes)) + if err != nil { + log.Errorf("oss PutObject failed: %v", err) + return "", err + } + + // 构造返回 URL + var url string + if c.config.Domain != "" { + url = fmt.Sprintf("%s/%s", c.config.Domain, objectKey) + } else { + // 这里简单处理协议头 + url = fmt.Sprintf("https://%s.%s/%s", c.config.Bucket, c.config.Endpoint, objectKey) + } + return url, nil +} diff --git a/internal/pkg/provider_set.go b/internal/pkg/provider_set.go index 603dedc..1e8bdb4 100644 --- a/internal/pkg/provider_set.go +++ b/internal/pkg/provider_set.go @@ -2,6 +2,7 @@ package pkg import ( "ai_scheduler/internal/pkg/dingtalk" + "ai_scheduler/internal/pkg/oss" "ai_scheduler/internal/pkg/utils_langchain" "ai_scheduler/internal/pkg/utils_ollama" "ai_scheduler/internal/pkg/utils_vllm" @@ -19,4 +20,6 @@ var ProviderSetClient = wire.NewSet( dingtalk.NewOldClient, dingtalk.NewContactClient, dingtalk.NewNotableClient, + + oss.NewClient, ) diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index 77a5f29..9934944 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -1,8 +1,10 @@ package bbxt import ( + "ai_scheduler/internal/pkg/oss" "ai_scheduler/pkg" "fmt" + "math/rand" "sort" "time" ) @@ -10,9 +12,10 @@ import ( type BbxtTools struct { cacheDir string excelTempDir string + ossClient *oss.Client } -func NewBbxtTools() (*BbxtTools, error) { +func NewBbxtTools(ossClient *oss.Client) (*BbxtTools, error) { cache, err := pkg.GetCacheDir() if err != nil { return nil, err @@ -25,6 +28,7 @@ func NewBbxtTools() (*BbxtTools, error) { return &BbxtTools{ cacheDir: cache, excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir), + ossClient: ossClient, }, nil } @@ -111,12 +115,12 @@ func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { } //总量生成excel if len(total) > 0 { - filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" + filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx" err = b.SimpleFillExcel(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total) } if len(gt) > 0 { - filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" + filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx" // err = b.resellerDetailFillExcel(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) } diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go index b6b2275..ad52bb0 100644 --- a/internal/tools/bbxt/bbxt_test.go +++ b/internal/tools/bbxt/bbxt_test.go @@ -1,12 +1,25 @@ package bbxt import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/oss" "testing" "time" ) func Test_StatisOursProductLossSumApiTotal(t *testing.T) { - o, err := NewBbxtTools() + ossClient, err := oss.NewClient(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", + }) + if err != nil { + panic(err) + } + + o, err := NewBbxtTools(ossClient) if err != nil { panic(err) } diff --git a/internal/tools/bbxt/excel.go b/internal/tools/bbxt/excel.go index 4ba932d..f5b52bf 100644 --- a/internal/tools/bbxt/excel.go +++ b/internal/tools/bbxt/excel.go @@ -10,8 +10,10 @@ import ( "path/filepath" "reflect" "sort" + "strings" "github.com/go-kratos/kratos/v2/log" + "github.com/shopspring/decimal" "github.com/xuri/excelize/v2" ) @@ -86,6 +88,22 @@ func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice i } } + excelBytes, err := f.WriteToBuffer() + if err != nil { + return fmt.Errorf("write to bytes failed: %v", err) + } + + picBytes, err := b.excel2picPy(templatePath, excelBytes.Bytes()) + if err != nil { + return fmt.Errorf("excel2picPy failed: %v", err) + } + // b.savePic("temp.png", picBytes) // 本地生成图片,仅测试 + // outputPath 提取文件名(不包含扩展名) + filename := filepath.Base(outputPath) + filename = strings.TrimSuffix(filename, filepath.Ext(filename)) + imgUrl := b.uploadToOSS(filename, picBytes) + log.Infof("imgUrl: %s", imgUrl) + // 6. 保存 return f.SaveAs(outputPath) } @@ -176,13 +194,6 @@ func (b *BbxtTools) resellerDetailFillExcel(templatePath, outputPath string, dat } } - // buffer, err := f.WriteToBuffer() - // if err != nil { - // return err - // } - - // return buffer.Bytes(), nil - // 6. 保存 return f.SaveAs(outputPath) } @@ -294,6 +305,8 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d } // ---------------- 填充合计行 ---------------- + // 四舍五入保留四位小数 + totalLoss, _ = decimal.NewFromFloat(totalLoss).Round(4).Float64() // 设置行高 f.SetRowHeight(sheet, currentRow, rowHeightTotal) @@ -323,7 +336,12 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d if err != nil { return fmt.Errorf("excel2picPy failed: %v", err) } - b.SavePic("temp.png", picBytes) + // b.savePic("temp.png", picBytes) // 本地生成图片,仅测试 + // outputPath 提取文件名(不包含扩展名) + filename := filepath.Base(outputPath) + filename = strings.TrimSuffix(filename, filepath.Ext(filename)) + imgUrl := b.uploadToOSS(filename, picBytes) + log.Infof("imgUrl: %s", imgUrl) // 6. 保存 return f.SaveAs(outputPath) @@ -405,11 +423,22 @@ func (b *BbxtTools) excel2picPy(templatePath string, excelBytes []byte) ([]byte, return picBytes, nil } -// SavePic 保存图片到本地 -func (b *BbxtTools) SavePic(outputPath string, picBytes []byte) error { +// savePic 保存图片到本地 +func (b *BbxtTools) 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 (b *BbxtTools) uploadToOSS(fileName string, fileBytes []byte) string { + objectKey := fmt.Sprintf("ai-scheduler/data-analytics/images/%s.png", fileName) + url, err := b.ossClient.UploadBytes(objectKey, fileBytes) + if err != nil { + log.Errorf("oss upload failed: %v", err) + return "" + } + return url +} diff --git a/tmpl/excel_temp/kshj_gt.xlsx b/tmpl/excel_temp/kshj_gt.xlsx index e270a67aaa52be82e3c03c824e02b53b180b36d8..a74a6f3d90b40ffc765862613bcf47aa2d7f2a4d 100755 GIT binary patch delta 5118 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 IZ2#u}0fXh0#{d8T delta 5055 zcmZ9QXD}S#x5jl=TP1`@lx1~UEr{M(CD{ow;-8z8`+`%sJ2d<$O8woN3KE&Ds=fybG*@_bD0JtZ)jY3)LHiP5*T^nsP-b zH{+jZHPuH!iw5V7=t!n|!s$DyS6cFgyonw3s}e(=%FiGB&M7ke>=14P86+nY35&}A zaaPKoAN6_#O3)ygrz4|z8ORZHbyKz3^mDK`M2PmO+Tj~!d$SKlNzaP;IM4hDqc7RS z+$lr{xx$OwjU3?ga7&{??`%^o{9!x=WeIa970^o%K2y>;=7p5(2VyeLZ`=0iR;SA` zKU41;iuBlDfsf4lHHKQWn|_9fwOyFT4>mccc+3wGT?$>*x54*R;t+p527+c0Kk+?# zzO`H#a#kQz7IeUQoETb$t&cRcT}jL?wpCYav~ccqSdv#}lA9SF9|G>0 zR{EM?(*=`OPuN*&G!rX~^VnsQTM_c*r)s?z`w2&W4{OuzAnl5`hS-ACb75um$?@jYq$?6SV6lIVt0x%>0%+m>(a{1L3DSndKPI zcPm*`#+rU(IoCWdklo`Bv6(i174;*>u<;O%PY$z`SElCqVjWlY5BkKyHa9t3%IiK~ zASzfXc<0B99ZqT{ZGSCSjlP2w4n&>K&1+)XJ@>=wOsdI{UzbQ|(Z#m-JBWRQZas%s zkKKHF#=4y?HlzkA;s>|>ia7T!Nb{d1 z#mm>kTn2uSPNp|KT?e?X;~T7O;ID043)uJWy}kO*I-$~hNl|M$?MdW9-V}n;*jGeC$-r95 zLC<`CJ)Y~PTWS@+sD;R=2aFw|unz-3?Yf9m8!x%FuX&iZ!fMN#gQXk%M8<|8!dyZ? zyJ&^v1~wKJMT!#;3}<5@;VdQA{Kox4C#`JfT=?Y&Jfu16GJRIr?Zpzs^s5~9{P^n| zjP0uD(9M=H?Y4yK03NVT!kr%{KQi^mGP1z)$UHo0I`wlS#Z&nk+(%soemDH6Fh41x zqv%*HJ(dY{4UYWVWCiFP>}}6Dl-&x7hW<(_SHscNAuk*ahQ~g&Ra5&a=0bzBq@rL& zuWk-b)&>|igQ$or$4_)7xaTSm5nIJb8?p!slM0JIz+zjH`twH}nYLAd{=3-4I%l%# zGdrz4<%Mb}?J^d!qs7wVaM7NA)%?osFT`$t67ne7V~~6a(`@qvQXz(&Ya&W7HY&=^ z^ef#qTHjaoz#s1k)mqlX6kN#jBLs?FX=t~hxrRNQOZ0vlB3Dt9HogxCNlOq1|WOQ0co+FIK3CrYtP?R zuh|iDH}}%NDSByc+XY)KUA0KDEFy{%D$(4n+mn}627gj64Q()p@kY|>3wIUj@jC{o z@4}5Kob;N~)X(lCU)wa0Y(wKOq@tREIhZnzi-6ASbE*q4M0Cnwsi6CZONN=}E(Alf zSrASo~1L>bQ81u&}U*up(Zq%NoYsaZi(CVg0ef!XkU9dO1Pd+`XNw z-JYi;P~yXl5*VN~0V?Y;U8j3qeX+KNhFtQL03rZvQ{A^rX?fqdxj`sK zm)J_d{7oV(u*U(MZW4AVOQ27|z?XxOp_UMOeS`l>$LvpP`&DujWPe<;6jS4N|0lPm zvgY_aZ6gSb5zMW#2j<{ky0$XY_t#*AA|l57JmK%R2bATWwC`J>oR<_z48#Nm!+0dj zZoi%{O4Y^gs)Vnp^}G{vp^Z28Y}Db7YgZX8lUpMa;w$K$ga4uf z?7iBt5|ZO8cO2`oBn8_jJ0;th7KNB_ucn%^F39k%zczzgzH3L(bl%&=yvL*xBV-?K zA;(V6BJVB+SPyVwqWr=T%3Y@}R$9e^D0nF3%+-5B+9PBm;Fnr`(jUqD$og4d?2F;% zuDrTc4o>aklJ*8na|G|O8h)|H-#w><)npem-!{8o6l|f%NyQa=pTgS5c9D_JG-hW9 zl`rEfYxE4AC*V$%P+Oer+ZgkYkB}WJ{RgkN;1=7|`Dp4a%N3pn*V_%9MIF#FfPT6D0N7J&D_*P&^qO+;w(a`yGHQ zSEm)vpqjSsBTc;_pQcd~uTn(!-0=76}jIx@v?u5=9`wCTkszQtWuMM?LiIpBU|HOt3&S#-~Y$h<~E=!`Y9lw8SPWI+_VPI2` z)U4B2VmW=v(}H;?GeTt(zQCAdf!J@W{nONd5n7IbpVvC~T5}Z=-EpMW{v+Cr9S`=! z+YRl_R`*^1ewVBsC4Aiaxd|Z-qtVI<&JIeiaF()sZWkldwyHhMRAEy=tHhWFlTo#E65V8k}-;>XUwLDaq5sbjUO$Oe$>mU`NDlJZ1J!o$zgOjfKGibmU)v z46h2e_ot1PgcU?!v+=OFgttkEai4Gvd?yXgc+fxLu`CFTz_lboUW9LCo7A7Lh9!h6 zP@jE-$5*SZ#;b1dpjB9R6$vljwp{?;nirhgy|gay#FbjCc8uSDVv^B?R;b)pN2-}M zE)waieFRJCUw+{BKeJ`ct(r3wr^J^@pt0?;))D4?vH--|uaCfSjT3v5_zA3(sY>l)ie)!`!N>I?q!N z>pPlN6Xt(Inj|65E+B2MfKtr*Zf#DISQ|M|M-inSZGX2~ngDEOyW(6`BxPIq48;;j3#!-pgm*t`rz_4WpsWC&Rr zx+COO*WL-c@6LB5a!v0O{$j$P9+nM%GmViwNzM0(3@7EGBf}U{6#V{5!4 zS^spt37J{#fJ^0WYnU(&A^^`wr3e+f~`8d0O&p@*0{REaBzEP;lu3;-%BYgjwN-QdK|F%)ND)TUu6!`lkI)mjU}x14XW-Gk_%`3j>Ecv_+lpawpRW^oJs7kfr**>Bkd`r=fj zN5>#tq7ynC_n#809t}t@Fpa-K*K4c~0QZ4}&2-?CV(UsV0ds7^u?hmL89|Kv1I=U8z@0~KWPFp z4`|H8=!yQjA3o%y6S<_1;un#0Mapju`aJwiGC~Ynr?4N|txDKj!|8w0N35ty`PkgK zXMB)RYI)4PMii|P7iK$=D!UZ-hQh|uyrcjw$-2Ljv;n+Lp|3L-EjwnXX|0ZSyLRt8 zly&8vsuo_m8`+E2nk6KB-8U?}_N_#pH|WOPR^)Taws3Kz4MxnCkUSED`*nnBZ+hKh z`(s?ma>M#E@j2&!D4g7bv(BJvQxj$Zd9>HZC%|rJ%X_|x-%mqqqP76++iG2>83X!d zk+)%OQw+kTx3qWK0-k)yMjN=Hd$`rzOcoJM>P7EtGYz_A8QkB?)Uxr!Svqd zz4lwRt4jztp-Gpwl@UQ2xw2P3MBFFV5svrynqk6oPqcr_eeB7?53~i#qvkS&&6aup z8Txj<+W-$$29Whre z5=b~H;(%#DIRw^;RQ44VM9GkzXpBJBN6z?Hd8JhTC}+)|q7uQ33ZI?TVxvhJ`*{g2 zC&WDLQSlavr1$F7L!oT0wR#KZKzGC<=fR6;hk!&D8moyRMqFG}m2cERIXc z#A-`}&PAYeDmr=VmX0X5f*EOKNfHp>e?*NckVQ54j^kW?NPEhs7@~`V?k(<#oEE!D zO3u5-O?=W&$S8w{bHkjn29m+HPGMmc!j4MOVNLm;+u0uL5O!TkBbx~JV#+R?3ieHk z5SWAE|EH&i0ELB(#r#0KluuxB?5dPnun6`{%5N|`!~ceBJS;5We~b_Bf4urBob0Cm Q=eCr Date: Tue, 30 Dec 2025 16:49:53 +0800 Subject: [PATCH 09/27] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=88=A9?= =?UTF-8?q?=E6=B6=A6=E6=8E=92=E8=A1=8C=E5=92=8C=E5=95=86=E5=93=81=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tools/bbxt/api.go | 37 +++- internal/tools/bbxt/bbxt.go | 294 ++++++++++++++++++++++++++++++- internal/tools/bbxt/bbxt_test.go | 22 +++ pkg/func.go | 8 + tmpl/excel_temp/lrtb_rank.xlsx | Bin 0 -> 9572 bytes tmpl/excel_temp/xstb_ana.xlsx | Bin 0 -> 9602 bytes 6 files changed, 348 insertions(+), 13 deletions(-) create mode 100644 tmpl/excel_temp/lrtb_rank.xlsx create mode 100644 tmpl/excel_temp/xstb_ana.xlsx diff --git a/internal/tools/bbxt/api.go b/internal/tools/bbxt/api.go index 64632cb..b07ce1e 100644 --- a/internal/tools/bbxt/api.go +++ b/internal/tools/bbxt/api.go @@ -53,19 +53,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 利润同比分销商排行榜 @@ -144,6 +144,29 @@ 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) diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index f7d1e68..bfee143 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -3,8 +3,14 @@ package bbxt import ( "ai_scheduler/pkg" "fmt" + "reflect" + "regexp" "sort" + "strings" "time" + + "github.com/go-kratos/kratos/v2/log" + "github.com/xuri/excelize/v2" ) type BbxtTools struct { @@ -28,11 +34,11 @@ func NewBbxtTools() (*BbxtTools, error) { }, nil } -func (b *BbxtTools) DailyReport(today time.Time) (err error) { +func (b *BbxtTools) DailyReport(now 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"), + err = b.StatisOursProductLossSum([]string{ + time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"), + time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).Format("2006-01-02 15:04:05"), }) if err != nil { return @@ -40,8 +46,8 @@ func (b *BbxtTools) DailyReport(today time.Time) (err error) { return } -// OursProductLossSum 负利润分析 -func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { +// StatisOursProductLossSumTotal 负利润分析 +func (b *BbxtTools) StatisOursProductLossSum(ct []string) (err error) { data, err := StatisOursProductLossSumApi(&StatisOursProductLossSumReq{ Ct: ct, }) @@ -121,3 +127,279 @@ func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { } return err } + +// GetProfitRankingSum 利润同比分销商排行榜 +func (b *BbxtTools) GetProfitRankingSum(now time.Time) (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"), + now.Format(time.DateTime), + } + + 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("${color: FF0000;horizontal:center;vertical:center}↑%.4f", v.HistoryOneDiff) + } else { + diff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%.4f", 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 { + filePath := b.cacheDir + "/lrtb_rank" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" + err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"lrtb_rank.xlsx", filePath, total, title) + } + return err +} + +// GetStatisOfficialProductSum 利润同比分销商排行榜 +func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []string) (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"), + now.Format(time.DateTime), + } + 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("${color: FF0000;horizontal:center;vertical:center}↑%d", v.HistoryOneDiff) + } else { + yeterDatyDiff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%d", v.HistoryOneDiff) + } + if v.HistoryTwoDiff > 0 { + lastWeekDiff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%d", v.HistoryTwoDiff) + } else { + lastWeekDiff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%d", 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 { + filePath := b.cacheDir + "/xstb_ana" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" + err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"xstb_ana.xlsx", filePath, total, title) + } + return 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 (b *BbxtTools) SimpleFillExcelWithTitle(templatePath, outputPath string, dataSlice interface{}, title string) error { + // 1. 打开模板 + f, err := excelize.OpenFile(templatePath) + if err != nil { + return err + } + defer f.Close() + + sheet := f.GetSheetName(0) + + // 1.1 获取第三行模板样式 + templateRow := 3 + styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", templateRow)) + if err != nil { + log.Errorf("获取模板样式失败: %v", err) + styleID = 0 + } + + // 1.2 获取模板行高 + rowHeight, err := f.GetRowHeight(sheet, templateRow) + if err != nil { + log.Errorf("获取模板行高失败: %v", err) + rowHeight = 31 // 默认高度 + } + + // 2. 写入标题到第一行 + f.SetCellValue(sheet, "A1", title) + + // 3. 反射获取切片数据 + v := reflect.ValueOf(dataSlice) + if v.Kind() != reflect.Slice { + return fmt.Errorf("dataSlice must be a slice") + } + + if v.Len() == 0 { + return nil + } + + // 4. 从第三行开始填充数据(第二行留空或作为标题行) + startRow := 3 + 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) + + // 5. 填充数据到Excel + for col, value := range rowData { + cell := fmt.Sprintf("%c%d", 'A'+col, currentRow) + // 5.1 应用模板样式到整行(根据实际列数) + 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) + } + 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: + + } + } + + } + + // 6. 保存 + 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 + } + + return f.NewStyle(style) +} diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go index b6b2275..066bb2d 100644 --- a/internal/tools/bbxt/bbxt_test.go +++ b/internal/tools/bbxt/bbxt_test.go @@ -15,3 +15,25 @@ func Test_StatisOursProductLossSumApiTotal(t *testing.T) { t.Log(err) } + +func Test_GetProfitRankingSum(t *testing.T) { + o, err := NewBbxtTools() + if err != nil { + panic(err) + } + err = o.GetProfitRankingSum(time.Now()) + + t.Log(err) + +} + +func Test_GetStatisOfficialProductSum(t *testing.T) { + o, err := NewBbxtTools() + if err != nil { + panic(err) + } + err = o.GetStatisOfficialProductSum(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}) + + t.Log(err) + +} diff --git a/pkg/func.go b/pkg/func.go index 006f16f..9b4f89b 100644 --- a/pkg/func.go +++ b/pkg/func.go @@ -63,3 +63,11 @@ func GetTmplDir() (string, error) { } return path, nil } + +func ReverseSliceNew[T any](s []T) []T { + result := make([]T, len(s)) + for i := 0; i < len(s); i++ { + result[i] = s[len(s)-1-i] + } + return result +} diff --git a/tmpl/excel_temp/lrtb_rank.xlsx b/tmpl/excel_temp/lrtb_rank.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d3ed4849de61f5ef80f449a8121956e38359a068 GIT binary patch literal 9572 zcma)ibzD`=_BV}`Akrn>orjP-;Gs*9kWT6D?(S}s?oJQg-3UmR(v2X(d(iuo_xat= zJAdq$z1FOm@9eeqUZEfj3x^E#*n;IYgdfj;G$hD{p|yd6owbcUivolU3)1oACmA;j z4yQOY6qGOw6cona$@FY&m|ZL_GGjZXx`i= zm4XHog~x*XBVjkx4cYZW<1k|A)TU6Jo$uNd2~d%%vchAyNezI@`7>v1c_ZAJjG34W z@$3%e_=?#5e%-MfZ|FdInX2a5U2q~27+e=`NQ*IB@$Y#!l|HW3;&vB1b>w;%Ig$g! zoQ(i@uxnpC3NLfLVafn4;{nyV*?K&%Q9~}OZ(D2mds+=~!kT;xN1jkm_Tn^@(Cq^? zIoEG5Pk&gpsSDi9e4Q<0+@Et}y6SRzXVcqI{iFzxQ6&q&O7VFa{(WTaMaLnZ&7i`% z7x{|7F3a%cGNaDRt;Xy0UPhfU@AV|~HJk@E@%DDXJE(s`PH)fx%?p7X1cCh7-yt`! zwljLfJTg{9zMB*;=$QIGILIBbE`15KL}lh=7aGbzU=7!1*&~XSvwROO+uw^I5s159 zU;iN()~h`~NCT)OP$uUir-h~u$<;E=xCJ724!nYoqBdHGJSc zHtV^^YOf&4YEe`+ifYC5DcSTgJL;ydu=^mqLPDRLQREvXDL6`-I)Z$cPM7yZgMHlx zF-FoOIa4DmKKbs*3k+0|sF9Sa<)kr>tG4KS;>vK6x_A7>U*TH({#{1RX~yJD;3;EK z%ZkuL+bzsLadyW+fdN8r4uIhN{2w?w+B;ZVJ|bNow=D0^iqWwud-o!(O-u+15v8iE zimDGWatutMG&BdNAv)~9c7N=Ye!i))xy0psvwZB9Jf_`(bI_xkr;1N07$i(vVP4FQ z@9~xij0a=Dpz@|%mE7N5Z;i42Bj^}mIlNO!&qs_lzO%gk2Nqd(`TwkJL5M+eri^*f0Cwc3a7h3SPaPF|YwG3=wZ zB`GQW}(jC1K z;udggRV;{Ri*X+=>(W41k`!%ev`n@fT@EN7VD0a3VE%Rw{iP~r`ey6+5%hmIJFpPY zT`YbYoPQ^H`isEX+RohG)X2!;cjMGsZ(%nG*}Yx_C@8%DqW#Ij_ETY0w5%6du{^Wu z9&~TOwwPZQ$#eE}XQ?ff>YbKqZLK=YFijfBE>^D>tv)LhL?y%Nth$di>*%?GD;a8F zE%VHbIi;1Pa-NmMjJip9zxi#kXsK?< zWHs(VFtfs(74_%IpWK(i;@Eq}`J=~-nUDa3`W6dSB}u76Dbx}z5{;CA z-b%53VPNjU^!o}J+j#Qj zMQ=&7*XIV4C!h807}O=dZ%rS<`+D|%ZDIbZ5g)Q6-kT%501w;xiJ)|ksxf1%unpEZ zZL~13hrVU*Iofu_iedbA$opmYyQGQKPHX*}Jra5h9AqMFsn1W!65Jf3RwokpERNSO znyc&(QHtmhBa|DD66Ff8*0!!`(x~yUVvS-8R;TA+$6pO8Q zub1D%>_>`c+e^*Nsn+-&SNS7X8gmNOk!?iWS_GEF;AH0|Ss~FD-v}Dw(Z1RpMA2N! z1AcS63VX}PM)b}D(AJ8Pr9%zdc{AI(o)V4T?~LlrqkD(xbBuT5-ONaN=)TscFF0F5 z(ZrqtE?s-FE*e)|@Tn`QRd$@(;GoWCrLCHIK&1m;uvHEnExa*kyQ3q9lii?GfWymA zPEW#_vWupKtH^{$zR)7LmeP%`pJXAof8!H{(1jWTx`l)xzIZh3<(G|_U4Uz0(JSZI zLSE%dn%x`a&^#PLa645Zx$azF>UFrq0v5T8D7({u`}=)#a@T#j)BcPKhPltqo|L2wpjse5%$ootCK9oY{1Fc_qiu zTAPMXbBb2~ijop2@;0W)R#-S+bwf!|Hs?v$Ol3!U>hh(WfK>*ju&});xVHUz2fX^7 z!~1HlelsOkjGS^?lA}nw0xpD-?8TJfXVjys)?{uNN^H5Hl+yrCf4bPt`f}&h<}e-= zpuV55U~GC5K2Rz@oq>TpK#!5-Jc07Wsz*EmTOjOoNMVg)g0MDmR?5hes*e*%!OuX! z-GuW!nTTgO`=_BqXFLIFsXgDA&@5G1A|!uelGj|Kaz^P$4k79sKCVG`MeP)qBo7jJtHJc?&Wl}AMUdXQJy^}Bk zH-Pun&;~a4?Z0W5w=QctpCA~XLT-){``ku*2^D%w?O227pbwom4&e=Z7SLEcVAlu} zQ3b_BxUm-U3-L~YOhff}LTm3)pwJA}CgF&58&n0?tdO=kUbVCOm$v4hJcm(ilIC$pxxE$~DEegXswM^jlEt914~DIbF4u zjInMTGS@AatPqx}LTSa9t};aQ;=Qj?d(Z>Abu(d>fZJm|_v;s};gBI=oX;`U^Dt~M}%DV0HLF^P179n!8i$H8p*Y1Q?ct2*`hU5q7OO4U{m)8P3F8?Vo_ zGkB$aJ2L6=bp_ar{0obTGIZarsN+@Y6{W{jxe8JXzKp0gmCN=uay^t9FdRG@# zotMuN*Jy+5_SIb3bi#2R>R6OPk_2Es>DGz55`e<+huz}kV-bgnxb zm;GU}L`u@F;_9LO`7^zb>{Uhlt5-fG?2mpm?45dNzIMFQ^hqC~Q0|b*;eO{1ml~(^ zyTxVgnS_82#cD<4RijvjQ-Ng&w!APkNvD_2PR5Q zh@!VI)E+wXj%YwBadN6E2&;pQ#*)URc40DmGc4+IX|XQaHG<02iouO*CJ2Ax$!Pxh))diY$*_FB zrOB>9?aYK$v&%)uXeEBUCSk#U!yNV!A)$D4UO9;kek)T>0FQmJ`4aJ;ZUGME8@MgV z*OVOM8a(>Jkk6&Psh*vYp|XSBJ1Z0W#~;et>dJvpMOtYl`mvGmS{3#g4h1F9sPvfp z;D~gi3No7QbiINSEwki^wB*R(xOBZri42o!P2=Wv4IMMHBGld}Lxai?EgfCe)I5va z7-&?Mac-CeGzvSEw~f0^odne^{`IyOUc5Mzt{1UM;I(z7Ip8H99F9f+M2U3io!PDLOXrcB>4uR$T(9@>@c^ST1*cdF?4O3!fO0yu0Yxj+tlZnuUh_O|sqcS93U)LK{!iD;}zBB*q+ zmRZo#49{@s6y4~#)r?_4v<6U#)StLVpJ!kWDTw2RP@|HFbNV2mH_)-Fh`><-zg`*1 zaHvmrIl4~7rgpbvn$!@pqV@_dbs-`Vpi!iC)SPvYsyj(4c}{EjMFfDj(2D&g#ED;S z^9tY)8HQNySiXDz<~#AUgo{Jnb|7nAxI!)tTVkodPGHfAr6gH$^sTxi?0G?gd>hF0%y-z*pdh(PzyjRhAm89+!;fn9e0DjN$7j>|J%j>WYu&Y+r z5&@=bIG0MrNW<(w>1Y(>#a+FG+mX8ThBuC@pnZkVQrrMWGls`5ZB|hg8+^qe?KxI;1Q)0X7*43+56V#vV zgCr}k@wvXY!{X`-@==|>9e;BNmJ_S*^4>aQH5TH%nH}Cm|M~%UN88VPcZXzO_V8FA zjI=7fLnKV%d&RN2-#eBwNE0#C;rm+H0{c#=?>>1+PrX?Q=)dM%vu=jm+IFVRAl)y zxOfP}uMh3I8-&%s@Xa?$nw_G0iD!FEDBa?))ph*#zBXFSjHb?daGrwwj++&2iqv4? zrW!DOcT~q!Fir;T`f2$G*e}JXdI7++x3Mo9x4yn?_ssiRSCB`6=UOpMp>a@eGcG+} z5+rFmjbshz&hj+vR>dZdmo1qRdxsK~ z`ZP^6Q@|=G8uXmRAQk*FQszAtOE8>Nw)v6d_u3DeKS`sjB%Lxr_B|7bYIHf22gc zB+Uri<&ib_ao{Pw%*_yJ5raI;feZ*oCl*4>+$|wknms20j2)8z#!N{7Pg}UJKC&Gc zgq#8zoycnK=te(gN=s5EG3QH~@5fz{CmX0&nM%zIkZNTYYDsB^O{^p@F$%PSBCGO* z1NRmy+~X7CjxRUajp8h+f<5cTVV%}uGGPoEkaz>scTQpwDV<=vo`=Y^u=p#+GGW=# z=X=_$k*IfMo*A2*&=oNGLa0JERpU@_GQzUQTLBXT=*SVUZvv zw4gM*#9Ueyn2MY;GhY;b9Q#W0Vw9Q=G3J4A-1ygUPgfqSn2Rm>iZ+kl8)#l(%P?q zfluqUbG0&L(n??C=3eB+Z?by&wDGE7V)H14+m}$$ljITYzLtK?!a4B`*%XNYa!q8b z2aGSJj;hCMdD1+Exv4}u-aM;RG!MCcCD5LaXr8*d=x-w#W+cb)Y>vIX9#Q;l@Mgv> z=FfhLEXc$95$nUwXXB8A#!lnaTF7qWl`hk{rn^E<=^?GgIaP2C(z_06YO7lu+pi* z$0h{IsLSe)-P_KY%RF*tuM+l;LEXumJ!$1!zMjABgUC`JoN4hA+$Z`L$mkQN$x}3GQvg&}q7Ef- z&)-cI1pb-@wjCO8e*Q1poIcmOsARxfhVRQ$_;<83HR!%BxEArm+$4xd1?GBw$KnHJV)s~8KGwWAJdzI{;RS_tLa`B;xY^yqR9wh zB3@BRa)~D{ojgv4b`P!s;P0y-q7(`MFsyiRbNBRIe{Lc_CdI=EQ;6lgNb>vjgQIb6 zL3e`q5VeVXwG@xjo3BDg(0w%WFETyv*1xg{ZC3@(x5KCBMXIrQ%F@xIEQF}}R{Qj% z@7QsZd!f>K(GM?#r1-7W0SQBSZChu=b@7#SsobcH#e#uE1_y*3wUNw|3>lFj{NFHq zdNP2OHrzG=agk)fn*ypEJ+M|7c%akNFW6%eJ!NFQ7VqBx2%j|vVKs+puUi1O(v-HO zsxVDd#d;V$JY+F5AGyhm^`hSg$o`_NVtCUvrBt4zXj0*Z z%{|O-S`c-GpmRz7q%^{_F!$c2QN8^5K*KFp)oc#AKrb2P($2n6fm?GllekoSO1eB- z*l?hcf(M=J++)>*aW>rX3QZEP&wOWRAg~f}n_HcG#&c626ehPqZtbsFi7R61(oDzx z9uU@U+5E%%{|>4`f~HV9c19Mz=N;SuP9Gm2&sHH05z3!5%#W1iapO`wtQdWd`Ouv< zS!e8L3l1#g2eO8qJ}EK}6V%V+)DPRhB8rSNEr`*nH#_O89)_)i%cRve3V zqeTpMgulm!Pwg0&;s^-5<93dM!^h52!DZ#fOlab715=d4K5xHmw`EN2e=GMQj4EgJ zMtFL9x2rRDQnPLt#1}yIe*b}zy}xHG$yJa`2V{4~ROND~NLx~7l9JSxg%sJ++)JOBzNAnlq|uB1 z#!4G9Thcs2YdC8J@bxHYX?YwF!Xh(+SeZjFh4DqU3Ve*^!CC1nPa0C+==OvJTLx5B zYHi(M4KN)`9=MDJI-TZqu=ShJ4FZU`u*u$!XL9E$+9wsqE2jG&J))~vkdHWHlBI8r zod@=Wnv`Lqc{nCsY~cBZIkFp)#R3Fvyc>WGktlqs-kHtJPBAcidR^MbpE=QHKeVK! z#5g>!%Wd9e<3%A*uAgKlSZausr=tb!1t{W9oZotl5 zb#PuJW%-?4Wzp%vi1E8WE3;)fmuk>*Zu!e)#xLTN&$6&n z0+hR45VLxc^74U8>vrFmKQ!FxZ@|PiCUc+@B&qMnd>gW{0K#2nk!OMqWco2gXf2@P zxMs7ij*5uF!I!af2=~jvI4?!y9EyE*t=sLP)OX-chWl>{ug}M-y6fmJhma4))^?dh z7d=b9u3WaoHncN-JnuX=;J$<8V4z{Jeny6VRY?54GmsY;3L1(Oa;$s-1@$lc^lN~B z&v-ssoL}v+*a8odvH3$`{wef#3-rkIOQ^t~JU=z&e+he3$zSbJ*kk)!5y(H~{ayz0 zNc>A4&EMJnQxx($M88)w{KcIHd8Pjp`bT}k@9e)P>i=SgfuxZCV*fL3|GyUfm~{Tt z9vAHl31>mf&HrNfCrXASf3r`&PxN;~_9*8UO5uNJ`!BQh`#8UcApaT%9};`}w}9mD zBm5p~`D+9_%wHq?8F~5tUwb#=Pu4$jjQ@N?e$O)gC2{ZXY=7k(f1mR25yrm;V1euu zWYK?z?SD=Bv1;H~d)(oaUpxGd8iIe${P!w?N8Vq%*7IMyf7KKyNW=eB)@P6!9YhG- JE3!wv{|DYYpJo66 literal 0 HcmV?d00001 diff --git a/tmpl/excel_temp/xstb_ana.xlsx b/tmpl/excel_temp/xstb_ana.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..54fb05616e5819703af3787cfeda33ee4c490927 GIT binary patch literal 9602 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 Date: Tue, 30 Dec 2025 17:34:51 +0800 Subject: [PATCH 10/27] =?UTF-8?q?fix:=20=E6=A8=A1=E6=9D=BF=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + tmpl/excel_temp/kshj_gt.xlsx | Bin 9938 -> 9942 bytes tmpl/excel_temp/kshj_gt.xlsx.v1 | Bin 9491 -> 0 bytes 3 files changed, 1 insertion(+) delete mode 100755 tmpl/excel_temp/kshj_gt.xlsx.v1 diff --git a/.gitignore b/.gitignore index 4dd0345..c953a6c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ cmd/server/wire_gen.go __debug* .bin/ .idea/ +cache/ \ No newline at end of file diff --git a/tmpl/excel_temp/kshj_gt.xlsx b/tmpl/excel_temp/kshj_gt.xlsx index a74a6f3d90b40ffc765862613bcf47aa2d7f2a4d..2b27bfa07c042ba158bc3229982c9138797ccee1 100755 GIT binary patch delta 1195 zcmV;c1XTOdP1a4YlK};W(2ajSlb8V>e_v0-AQZ*lP5cfG?=7^;bdt7(wHjp~!ir z1*>We1v-F3^YZc{=Z5my!jr8HSTDgvfutSfra(LI4aL~;9Vq6K;Lt(b)K)Pce~xyG z4dd@@3pgX>3M=qTFwbxcjLkqqN2K5b(tOye0|<_x0tMPToF*y$_Ij|&{W;JnBd3!7 zU_z&3+%a45UL4$eCkJEGG)Xh*VubuDeqBC3cU5Dl+g))$WiB}77MQPXc`KCE(s|pS zay(MnEmq9=Wmxz|g19^UHlG^;KDjH><3U6r8tsdml$XiO1-2%qNX3)G1Tug10#r>A z6tTO~u7vjb>^hUi!ilrC+rZy5wwsUwtA&@e8J{`l%y_0_Ho$nw+LlLJ2fn z6{6YZ^w-aulQT+;Wlh1PY9OZ%VCco_#)||&&c9+I2riT=?j0g;940~+K8(<4%S{)c^;o1)mifwM6WcfX8LZ|o*;3H z#chNX_Q99i8v^^p({)|cwI<@t!NsUS>1hmhy)#zT*UU1yTKXHf)$xC7SqSW+yLzIb z%;{yiSf^8p2OjcUKxanZiDjD~Ac0#ba8@XBGq6$W&H=w*C~@Q-;3yflVh?MOGN+#= zI7WZH2!2I@cu*LB@6~Sn)ZA)PK*@H}u9f^F3R~s$oc6Krs{0ILTUoR+iDs0jot2`2 zudoM+2|3N25LXge@kxJb;U=7tn%xKYfiX*>d4}oSx9A{~9#TV6c1Xg{E+oy8=s!7F zuouG5Lryx2&Q4zyii1ht5_d5KwR|**BFrKS7SXOyB1P;phdQLkp(I z(pLd7^TX$_rw=$UlUWue|FVK>`|3mlg1KKgf4^bz!j#pZw|+ql;cC~o`^KpmqN5)L zm;XGTEiS_<4+lHAXf4~qH>S6uF+@U%lu6`m)}3{gX2#K>=%%btO6ht&^}N zHUZR==OsV^S(7LxJOPH2WhNmTwkD@Y#R32TzXkvR7ytkO000000RSKX006R+p(ZN^ Jxgr1n005dcK70TG delta 1204 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 fKWDA=KX`uy0mw?g{@ASOkf<7X2;Dj11K Date: Tue, 30 Dec 2025 18:31:20 +0800 Subject: [PATCH 11/27] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=94=B5?= =?UTF-8?q?=E5=95=86=E5=85=85=E5=80=BC=E7=B3=BB=E7=BB=9F-=E6=88=91?= =?UTF-8?q?=E4=BB=AC=E7=9A=84=E5=95=86=E5=93=81=E7=BB=9F=E8=AE=A1=E5=B7=A5?= =?UTF-8?q?=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config_env.yaml | 16 ++-- internal/config/config.go | 2 + .../statistics_ours_product/client.go | 73 +++++++++++++++++++ .../statistics_ours_product/client_test.go | 33 +++++++++ .../recharge/statistics_ours_product/types.go | 30 ++++++++ .../recharge/statistics_ours_product.go | 33 +++++++++ 6 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 internal/domain/tools/recharge/statistics_ours_product/client.go create mode 100644 internal/domain/tools/recharge/statistics_ours_product/client_test.go create mode 100644 internal/domain/tools/recharge/statistics_ours_product/types.go create mode 100644 internal/domain/workflow/recharge/statistics_ours_product.go diff --git a/config/config_env.yaml b/config/config_env.yaml index 83bbfd6..0467cbc 100644 --- a/config/config_env.yaml +++ b/config/config_env.yaml @@ -112,18 +112,20 @@ eino_tools: hytGoodsBrandSearch: base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/brand/list" # == 报表分析 data analytics == - # 负利润分析列表、 负利润分析详情 + # 负利润分析列表 daOursProductLoss: - base_url: "http://test.analysis.com/api/dataanalytics/statisOursProductLossSum" - api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY2OTc4MTQwLCJuYmYiOjE3NjY5NzYzNDAsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.nyQw7_JZd7IfnIkPlfySGJMkFAHy_RtIL6gRqOYdku3ikp_w3x9bt7rrSBDQMi4Z8jC4jL_qxwhJ1D1DV4uLZ_cZBmuuG-gymIDN1TSJKa8hpu4E8L3tPdbT1H6hehXvg10-_ugKAYNBgnvFUbCRnpMl-sNuqCLWZKa_v63L1v3lGZOQiPPyzEGmIulNwqSF8rlzZggJneVTWEQac2BNK4181mpbz0S_m84xuFFO3Qwen2sEntBaPKeKbJ9BAfdSAQPcIlnG9FHF3mttZfxQQF5nbxU7CAGYsu_2kjpcmP-u5x3tT3gCWsF1t84lSZ8mMxIax7rh0-Y55abRoBnrwQ" + base_url: "https://reportapi.1688sup.com/api/dataanalytics/statisOursProductLossSum" # 利润同比排行榜 daProfitRanking: - base_url: "http://test.analysis.com/api/dataanalytics/profitRankingSum" - api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY2OTc4MTQwLCJuYmYiOjE3NjY5NzYzNDAsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.nyQw7_JZd7IfnIkPlfySGJMkFAHy_RtIL6gRqOYdku3ikp_w3x9bt7rrSBDQMi4Z8jC4jL_qxwhJ1D1DV4uLZ_cZBmuuG-gymIDN1TSJKa8hpu4E8L3tPdbT1H6hehXvg10-_ugKAYNBgnvFUbCRnpMl-sNuqCLWZKa_v63L1v3lGZOQiPPyzEGmIulNwqSF8rlzZggJneVTWEQac2BNK4181mpbz0S_m84xuFFO3Qwen2sEntBaPKeKbJ9BAfdSAQPcIlnG9FHF3mttZfxQQF5nbxU7CAGYsu_2kjpcmP-u5x3tT3gCWsF1t84lSZ8mMxIax7rh0-Y55abRoBnrwQ" + base_url: "https://reportapi.1688sup.com/api/dataanalytics/profitRankingSum" # 销售同比分析列表 daOfficialProduct: - base_url: "http://test.analysis.com/api/dataanalytics/statisOfficialProduct" - api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY2OTc4MTQwLCJuYmYiOjE3NjY5NzYzNDAsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.nyQw7_JZd7IfnIkPlfySGJMkFAHy_RtIL6gRqOYdku3ikp_w3x9bt7rrSBDQMi4Z8jC4jL_qxwhJ1D1DV4uLZ_cZBmuuG-gymIDN1TSJKa8hpu4E8L3tPdbT1H6hehXvg10-_ugKAYNBgnvFUbCRnpMl-sNuqCLWZKa_v63L1v3lGZOQiPPyzEGmIulNwqSF8rlzZggJneVTWEQac2BNK4181mpbz0S_m84xuFFO3Qwen2sEntBaPKeKbJ9BAfdSAQPcIlnG9FHF3mttZfxQQF5nbxU7CAGYsu_2kjpcmP-u5x3tT3gCWsF1t84lSZ8mMxIax7rh0-Y55abRoBnrwQ" + 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" dingtalk: api_key: "dingsbbntrkeiyazcfdg" diff --git a/internal/config/config.go b/internal/config/config.go index 83115a3..7e94bfd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -207,6 +207,8 @@ type EinoToolsConfig struct { DaOfficialProduct ToolConfig `mapstructure:"daOfficialProduct"` // 销售同比下滑详情 DaOfficialProductDecline ToolConfig `mapstructure:"daOfficialProductDecline"` + // 我们的商品统计 + RechargeStatisticsOursProduct ToolConfig `mapstructure:"rechargeStatisticsOursProduct"` } // LoggingConfig 日志配置 diff --git a/internal/domain/tools/recharge/statistics_ours_product/client.go b/internal/domain/tools/recharge/statistics_ours_product/client.go new file mode 100644 index 0000000..a9706b9 --- /dev/null +++ b/internal/domain/tools/recharge/statistics_ours_product/client.go @@ -0,0 +1,73 @@ +package statistics_ours_product + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/pkg/l_request" + "context" + "encoding/json" + "fmt" + "strings" + "time" +) + +type Client struct { + cfg config.ToolConfig +} + +func New(cfg config.ToolConfig) *Client { + return &Client{ + cfg: cfg, + } +} + +// Call 调用我们的商品统计接口 +func (c *Client) Call(ctx context.Context, req StatisticsOursProductRequest) ([]StatisticsOursProductItem, error) { + // 构建 URL 参数 + var queryParams []string + + if req.Page > 0 { + queryParams = append(queryParams, fmt.Sprintf("page=%d", req.Page)) + } + if req.Limit > 0 { + queryParams = append(queryParams, fmt.Sprintf("limit=%d", req.Limit)) + } + if req.OursProductId != "" { + queryParams = append(queryParams, fmt.Sprintf("ours_product_id=%s", req.OursProductId)) + } + + for _, s := range req.Serial { + queryParams = append(queryParams, fmt.Sprintf("serial[]=%s", s)) + } + + // 添加 timestamp + queryParams = append(queryParams, fmt.Sprintf("timestamp=%d", time.Now().UnixMilli())) + + queryString := strings.Join(queryParams, "&") + fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString) + + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey), + } + + reqObj := l_request.Request{ + Method: "GET", + Url: fullURL, + Headers: headers, + } + + res, err := reqObj.Send() + if err != nil { + return nil, fmt.Errorf("请求失败,err: %v", err) + } + + var resData StatisticsOursProductResponse + if err := json.Unmarshal([]byte(res.Text), &resData); err != nil { + return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text) + } + + if resData.Code != 200 { + return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg) + } + + return resData.Data, nil +} diff --git a/internal/domain/tools/recharge/statistics_ours_product/client_test.go b/internal/domain/tools/recharge/statistics_ours_product/client_test.go new file mode 100644 index 0000000..4789fba --- /dev/null +++ b/internal/domain/tools/recharge/statistics_ours_product/client_test.go @@ -0,0 +1,33 @@ +package statistics_ours_product + +import ( + "ai_scheduler/internal/config" + "context" + "testing" +) + +func TestClient_Call(t *testing.T) { + cfg := config.ToolConfig{ + BaseURL: "http://admin.1688sup.cn:8001/admin/statistics/oursProduct", + APIKey: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY3MDc3NzcwLCJuYmYiOjE3NjcwNzU5NzAsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.Nuw_aR6iSPmhhh9E5rhyTxHBsgWtaTZvbnc7SFTnUBJXTQvYahnk0LyZaVpsVw6FB3cU0F5xOdX3rmGyWyaiszWO6yi-o1oxGMXwhf39fMiWT2xUI6pAn9Ync8DmZ4tOMCNUTdEk4CaQFzrTwJs0c-VR4yW6LgoPmNPvUVZ-KwmusUpnPz5j9RsJItzIWE3bpGGsfB54e2UERcZdbo9BXxCZIBbpAYKBKdl73KuI8SNaXgKvGTrJ6hEN4ESpnbisJVwT5pp_kuChJlcfjHTHFsEf4RJDjN9gTrtDbBWZyY3OmO2ukqYAM7tZPs6TXJwvQGJQsFRVZUBGxS1nD_6DzQ", + } + + client := New(cfg) + + req := StatisticsOursProductRequest{ + Page: 1, + Limit: 100, + Serial: []string{"2025122300", "2025123123"}, + } + + t.Logf("Testing Call with req: %+v", req) + + // 由于没有真实的后端环境和 Token,这里注释掉实际调用 + // 在开发环境中,你可以取消注释并填入有效的 Token 进行测试 + res, err := client.Call(context.Background(), req) + if err != nil { + t.Logf("Call failed: %v", err) + } else { + t.Logf("Call success, resp: %+v", res) + } +} diff --git a/internal/domain/tools/recharge/statistics_ours_product/types.go b/internal/domain/tools/recharge/statistics_ours_product/types.go new file mode 100644 index 0000000..624406b --- /dev/null +++ b/internal/domain/tools/recharge/statistics_ours_product/types.go @@ -0,0 +1,30 @@ +package statistics_ours_product + +// StatisticsOursProductRequest 我们的商品统计请求参数 +type StatisticsOursProductRequest struct { + Page int `json:"page"` // 页码 + Limit int `json:"limit"` // 每页条数 + Serial []string `json:"serial"` // 流水号范围 (通常是日期格式,如 YYYYMMDDHH) + OursProductId string `json:"ours_product_id"` // 我们的商品ID (可选) +} + +// StatisticsOursProductResponse 我们的商品统计响应结构 +// 注意:接口直接返回数组,而不是包含在 data 字段中的对象 +type StatisticsOursProductResponse struct { + Code int `json:"code"` + Msg string `json:"error"` // 接口返回字段名为 error + Data []StatisticsOursProductItem `json:"data"` // data 是一个数组 +} + +type StatisticsOursProductItem struct { + OursProductId int `json:"ours_product_id"` + ResellerId int `json:"reseller_id"` + TotalPrice string `json:"total_price"` + Count string `json:"count"` + SuccessCount string `json:"success_count"` + SuccessPrice string `json:"success_price"` + FailCount string `json:"fail_count"` + FailPrice string `json:"fail_price"` + Profit string `json:"profit"` + OursProductName string `json:"ours_product_name"` +} diff --git a/internal/domain/workflow/recharge/statistics_ours_product.go b/internal/domain/workflow/recharge/statistics_ours_product.go new file mode 100644 index 0000000..0a7649d --- /dev/null +++ b/internal/domain/workflow/recharge/statistics_ours_product.go @@ -0,0 +1,33 @@ +package recharge + +import ( + "ai_scheduler/internal/config" + toolManager "ai_scheduler/internal/domain/tools" + "ai_scheduler/internal/domain/workflow/runtime" + "ai_scheduler/internal/entitys" + "context" +) + +const WorkflowIDStatisticsOursProduct = "recharge.statisticsOursProduct" + +func init() { + runtime.Register(WorkflowIDStatisticsOursProduct, func(d *runtime.Deps) (runtime.Workflow, error) { + return &statisticsOursProduct{cfg: d.Conf, toolManager: d.ToolManager}, nil + }) +} + +type statisticsOursProduct struct { + cfg *config.Config + toolManager *toolManager.Manager +} + +type StatisticsOursProductWorkflowInput struct { + // 预留字段 +} + +func (w *statisticsOursProduct) ID() string { return WorkflowIDStatisticsOursProduct } + +func (w *statisticsOursProduct) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) { + // 保持流程为空,仅返回 nil + return nil, nil +} From b22f4550ef0c554d2a496b942d355f32e2b4e232 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Wed, 31 Dec 2025 09:30:40 +0800 Subject: [PATCH 12/27] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8=E7=B3=BB=E7=BB=9F=E5=8A=9F=E8=83=BD=E4=B8=8E=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/main.go | 2 +- config/config_test.yaml | 6 +- internal/biz/ding_talk_bot.go | 37 ++- internal/config/config.go | 3 +- internal/pkg/func.go | 312 ++++++++++++------ internal/pkg/provider_set.go | 4 +- internal/pkg/{oss => utils_oss}/client.go | 10 +- internal/services/cron.go | 24 +- internal/services/dtalk_bot.go | 22 -- internal/services/dtalk_bot_test.go | 8 +- internal/tools/bbxt/api.go | 85 +++-- internal/tools/bbxt/bbxt.go | 292 ++++++---------- internal/tools/bbxt/bbxt_test.go | 46 ++- internal/tools/bbxt/entitys.go | 9 + internal/tools/bbxt/excel.go | 385 +++++++--------------- internal/tools/bbxt/upload.go | 153 +++++++++ 16 files changed, 762 insertions(+), 636 deletions(-) rename internal/pkg/{oss => utils_oss}/client.go (84%) create mode 100644 internal/tools/bbxt/upload.go diff --git a/cmd/server/main.go b/cmd/server/main.go index baca30d..c0ff4c8 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -29,6 +29,6 @@ func main() { //钉钉机器人 app.DingBotServer.Run(ctx, *onBot) //定时任务 - app.Cron.Run(ctx) + //app.Cron.Run(ctx) log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port))) } diff --git a/config/config_test.yaml b/config/config_test.yaml index 63b4b66..bdbc26d 100644 --- a/config/config_test.yaml +++ b/config/config_test.yaml @@ -4,7 +4,7 @@ server: host: "0.0.0.0" ollama: - base_url: "http://host.docker.internal:11434" + base_url: "http://127.0.0.1:11434" model: "qwen3-coder:480b-cloud" generate_model: "qwen3-coder:480b-cloud" mapping_model: "deepseek-v3.2:cloud" @@ -194,7 +194,3 @@ llm: temperature: 0.7 max_tokens: 4096 stream: true -#ding_talk_bots: -# public: -# client_id: "dingchg59zwwvmuuvldx", -# client_secret: "ZwetAnRiTQobNFVlNrshRagSMAJIFpBAepWkWI7on7Tt_o617KHtTjBLp8fQfplz", diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index 0cc4090..631fbd0 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -9,7 +9,9 @@ import ( "ai_scheduler/internal/data/model" "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" @@ -44,6 +46,7 @@ type DingTalkBotBiz struct { chatHis *impl.BotChatHisImpl conf *config.Config cardSend *dingtalk.SendCardClient + ossClient *utils_oss.Client } // NewDingTalkBotBiz @@ -58,6 +61,7 @@ func NewDingTalkBotBiz( toolManager *tools.Manager, conf *config.Config, cardSend *dingtalk.SendCardClient, + ossClient *utils_oss.Client, ) *DingTalkBotBiz { return &DingTalkBotBiz{ do: do, @@ -71,6 +75,7 @@ func NewDingTalkBotBiz( chatHis: chatHis, conf: conf, cardSend: cardSend, + ossClient: ossClient, } } @@ -473,12 +478,34 @@ func (d *DingTalkBotBiz) HandleStreamRes(ctx context.Context, data *chatbot.BotC return } -func (d *DingTalkBotBiz) GetReportLists(ctx context.Context) (contentChan chan string, err error) { - contentChan = make(chan string, 10) - defer close(contentChan) - contentChan <- "截止今日23点利润亏损合计:127917.0866元,亏损500元以上的分销商和产品金额如下图:" - contentChan <- "![图片](https://lsxdmgoss.oss-cn-chengdu.aliyuncs.com/MarketingSaaS/image/V2/other/shanghu.png)" +func (d *DingTalkBotBiz) GetReportLists(ctx context.Context) (reports []*bbxt.ReportRes, err error) { + reportList, err := bbxt.NewBbxtTools() + if err != nil { + return + } + reports, err = reportList.DailyReport(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, d.ossClient) + if err != nil { + return + } + + return +} + +func (d *DingTalkBotBiz) SendReport(ctx context.Context, groupInfo model.AiBotGroup, report *bbxt.ReportRes) (err error) { + + reportChan := make(chan string, 10) + defer close(reportChan) + reportChan <- report.Title + reportChan <- fmt.Sprintf("![图片](%s)", report.Url) + err = d.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{ + RobotCode: groupInfo.RobotCode, + ConversationType: constants.ConversationTypeGroup, + ConversationId: groupInfo.ConversationID, + Text: chatbot.BotCallbackDataTextModel{ + Content: report.ReportName, + }, + }, reportChan) return } diff --git a/internal/config/config.go b/internal/config/config.go index 8c3a6d2..510e59a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,8 +24,7 @@ type Config struct { 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 { diff --git a/internal/pkg/func.go b/internal/pkg/func.go index 4d9232b..648996c 100644 --- a/internal/pkg/func.go +++ b/internal/pkg/func.go @@ -10,8 +10,6 @@ import ( "strconv" "strings" "time" - - jsoniter "github.com/json-iterator/go" ) func JsonStringIgonErr(data interface{}) string { @@ -170,131 +168,257 @@ func SafeReplace(template string, replaceTag string, replacements ...string) (st return template, nil } -func StructToMapUsingJsoniter(obj interface{}) (map[string]string, error) { - var json = jsoniter.ConfigCompatibleWithStandardLibrary - - // 转换为JSON - jsonBytes, err := json.Marshal(obj) - if err != nil { - return nil, err - } - - // 解析为map[string]interface{} - var tempMap map[string]interface{} - err = json.Unmarshal(jsonBytes, &tempMap) - if err != nil { - return nil, err - } - - // 转换为map[string]string - result := make(map[string]string) - for k, v := range tempMap { - result[k] = fmt.Sprintf("%v", v) - } - - return result, nil +// 配置选项 +type URLValuesOptions struct { + ArrayFormat string // 数组格式:"brackets" -> name[], "indices" -> name[0], "repeat" -> name=value1&name=value2 + TimeFormat string // 时间格式 } -// 通用结构体转 Query 参数 -func StructToQuery(obj interface{}) (url.Values, error) { - values := url.Values{} - v := reflect.ValueOf(obj) - t := reflect.TypeOf(obj) +var defaultOptions = URLValuesOptions{ + ArrayFormat: "brackets", // 默认使用括号格式 + TimeFormat: time.DateTime, +} - // 如果是指针,获取指向的值 +// StructToURLValues 将结构体转换为 url.Values +func StructToURLValues(input interface{}, options ...URLValuesOptions) (url.Values, error) { + opts := defaultOptions + if len(options) > 0 { + opts = options[0] + } + + values := url.Values{} + + if input == nil { + return values, nil + } + + v := reflect.ValueOf(input) + t := reflect.TypeOf(input) + + // 如果是指针,获取其指向的值 if v.Kind() == reflect.Ptr { + if v.IsNil() { + return values, nil + } v = v.Elem() t = t.Elem() } - // 确保是结构体 + // 确保是结构体类型 if v.Kind() != reflect.Struct { - return values, fmt.Errorf("expected struct, got %v", v.Kind()) + return nil, fmt.Errorf("input must be a struct or pointer to struct") } + // 遍历结构体字段 for i := 0; i < v.NumField(); i++ { - field := v.Field(i) - fieldType := t.Field(i) + field := t.Field(i) + fieldValue := v.Field(i) - // 跳过零值字段(omitempty) - tag := fieldType.Tag.Get("json") - if strings.Contains(tag, "omitempty") && field.IsZero() { + // 跳过非导出字段 + if !field.IsExported() { continue } - // 获取字段名 - fieldName := getFieldName(fieldType) + // 解析 JSON 标签(也可以支持 form 标签) + tag := field.Tag.Get("json") + fieldName, omitempty := parseJSONTag(tag) + if fieldName == "-" { + continue // 忽略该字段 + } if fieldName == "" { + fieldName = field.Name + } + + // 处理指针类型 + if fieldValue.Kind() == reflect.Ptr { + if fieldValue.IsNil() { + if omitempty { + continue + } + // 可以为 nil 指针添加空值 + values.Set(fieldName, "") + continue + } + fieldValue = fieldValue.Elem() + } + + // 处理切片/数组 + if fieldValue.Kind() == reflect.Slice || fieldValue.Kind() == reflect.Array { + if fieldValue.Len() == 0 && omitempty { + continue + } + + // 将切片转换为 URL 参数 + err := addSliceToValues(values, fieldName, fieldValue, opts) + if err != nil { + return nil, err + } continue } - // 处理不同类型的字段 - addFieldToValues(values, fieldName, field) + // 检查是否需要忽略空值 + if omitempty && isEmptyValue(fieldValue) { + continue + } + + // 转换单个值 + str, err := valueToString(fieldValue, opts) + if err != nil { + return nil, err + } + values.Set(fieldName, str) } return values, nil } -func getFieldName(field reflect.StructField) string { - tag := field.Tag.Get("json") - if tag != "" { - parts := strings.Split(tag, ",") - if parts[0] != "-" && parts[0] != "" { - return parts[0] - } - if parts[0] == "-" { - return "" // 跳过该字段 - } - } - return field.Name -} - -func addFieldToValues(values url.Values, name string, field reflect.Value) { - if !field.IsValid() || field.IsZero() { - return +// 解析 JSON 标签 +func parseJSONTag(tag string) (fieldName string, omitempty bool) { + if tag == "" { + return "", false } - switch field.Kind() { - case reflect.String: - values.Add(name, field.String()) + parts := strings.Split(tag, ",") + fieldName = parts[0] - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - values.Add(name, strconv.FormatInt(field.Int(), 10)) - - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - values.Add(name, strconv.FormatUint(field.Uint(), 10)) - - case reflect.Float32, reflect.Float64: - values.Add(name, strconv.FormatFloat(field.Float(), 'f', -1, 64)) - - case reflect.Bool: - values.Add(name, strconv.FormatBool(field.Bool())) - - case reflect.Slice: - // 处理切片,特别是 []string - if field.Type().Elem().Kind() == reflect.String { - for i := 0; i < field.Len(); i++ { - item := field.Index(i).String() - // 特殊处理 ct 字段 - if name == "ct" { - formatted := strings.Replace(item, " ", "+", 1) - if i == 1 && field.Len() >= 2 { - formatted = formatted + ".999" - } - values.Add("ct[]", formatted) - } else { - values.Add(fmt.Sprintf("%s[]", name), item) - } + if len(parts) > 1 { + for _, part := range parts[1:] { + if part == "omitempty" { + omitempty = true } } + } - case reflect.Struct: - // 处理 time.Time - if t, ok := field.Interface().(time.Time); ok { - values.Add(name, t.Format("2006-01-02+15:04:05")) + return fieldName, omitempty +} + +// 添加切片到 values +func addSliceToValues(values url.Values, fieldName string, slice reflect.Value, opts URLValuesOptions) error { + length := slice.Len() + if length == 0 { + return nil + } + + switch opts.ArrayFormat { + case "brackets": + // 格式:field[]=value1&field[]=value2 + for i := 0; i < length; i++ { + item := slice.Index(i) + str, err := valueToString(item, opts) + if err != nil { + return err + } + values.Add(fieldName, str) + } + + case "indices": + // 格式:field[0]=value1&field[1]=value2 + for i := 0; i < length; i++ { + item := slice.Index(i) + str, err := valueToString(item, opts) + if err != nil { + return err + } + values.Set(fmt.Sprintf("%s[%d]", fieldName, i), str) + } + + case "repeat": + // 格式:field=value1&field=value2 + for i := 0; i < length; i++ { + item := slice.Index(i) + str, err := valueToString(item, opts) + if err != nil { + return err + } + values.Add(fieldName, str) } default: - values.Add(name, fmt.Sprintf("%v", field.Interface())) + // 默认使用 brackets 格式 + for i := 0; i < length; i++ { + item := slice.Index(i) + str, err := valueToString(item, opts) + if err != nil { + return err + } + values.Add(fieldName+"[]", str) + } + } + + return nil +} + +// 将值转换为字符串 +func valueToString(v reflect.Value, opts URLValuesOptions) (string, error) { + if !v.IsValid() { + return "", nil + } + + // 处理不同类型 + switch v.Kind() { + case reflect.String: + return v.String(), nil + + case reflect.Bool: + return strconv.FormatBool(v.Bool()), nil + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(v.Int(), 10), nil + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.FormatUint(v.Uint(), 10), nil + + case reflect.Float32, reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + + case reflect.Struct: + // 特殊处理 time.Time + if t, ok := v.Interface().(time.Time); ok { + return t.Format(opts.TimeFormat), nil + } + // 其他结构体递归处理 + // 这里可以扩展为递归处理嵌套结构体 + + default: + // 默认使用 fmt 的字符串表示 + return fmt.Sprintf("%v", v.Interface()), nil + } + + return fmt.Sprintf("%v", v.Interface()), nil +} + +// 检查值是否为空 +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.String: + return v.String() == "" + case reflect.Bool: + return false + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Slice, reflect.Array, reflect.Map: + return v.Len() == 0 + case reflect.Ptr, reflect.Interface: + return v.IsNil() + case reflect.Struct: + if t, ok := v.Interface().(time.Time); ok { + return t.IsZero() + } + return false + default: + return false } } + +// 方便函数:直接生成查询字符串 +func StructToQueryString(input interface{}, options ...URLValuesOptions) (string, error) { + values, err := StructToURLValues(input, options...) + if err != nil { + return "", err + } + return values.Encode(), nil +} diff --git a/internal/pkg/provider_set.go b/internal/pkg/provider_set.go index 1e8bdb4..dcb5d8f 100644 --- a/internal/pkg/provider_set.go +++ b/internal/pkg/provider_set.go @@ -2,9 +2,9 @@ package pkg import ( "ai_scheduler/internal/pkg/dingtalk" - "ai_scheduler/internal/pkg/oss" "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" @@ -21,5 +21,5 @@ var ProviderSetClient = wire.NewSet( dingtalk.NewContactClient, dingtalk.NewNotableClient, - oss.NewClient, + utils_oss.NewClient, ) diff --git a/internal/pkg/oss/client.go b/internal/pkg/utils_oss/client.go similarity index 84% rename from internal/pkg/oss/client.go rename to internal/pkg/utils_oss/client.go index 225e8d9..6db558e 100644 --- a/internal/pkg/oss/client.go +++ b/internal/pkg/utils_oss/client.go @@ -1,4 +1,4 @@ -package oss +package utils_oss import ( "ai_scheduler/internal/config" @@ -16,19 +16,19 @@ type Client struct { } // NewClient 初始化 OSS 客户端 -func NewClient(cfg config.Oss) (*Client, error) { - client, err := oss.New(cfg.Endpoint, cfg.AccessKey, cfg.SecretKey) +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.Bucket) + bucket, err := client.Bucket(cfg.Oss.Bucket) if err != nil { return nil, fmt.Errorf("oss get bucket failed: %v", err) } return &Client{ - config: cfg, + config: cfg.Oss, client: client, bucket: bucket, }, nil diff --git a/internal/services/cron.go b/internal/services/cron.go index 5f624d1..3f1eea8 100644 --- a/internal/services/cron.go +++ b/internal/services/cron.go @@ -3,10 +3,9 @@ package services import ( "ai_scheduler/internal/biz" "ai_scheduler/internal/config" - "ai_scheduler/internal/data/constants" "context" - "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" + "github.com/gofiber/fiber/v2/log" ) type CronService struct { @@ -22,22 +21,23 @@ func NewCronService(config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz) * } func (d *CronService) CronReportSend(ctx context.Context) error { - reportChan, err := d.dingTalkBotBiz.GetReportLists(ctx) + reports, err := d.dingTalkBotBiz.GetReportLists(ctx) if err != nil { return err } - groupId := 23 + groupId := 28 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) + //contentChan <- "截止今日23点利润亏损合计:127917.0866元,亏损500元以上的分销商和产品金额如下图:" + //contentChan <- "![图片](https://lsxdmgoss.oss-cn-chengdu.aliyuncs.com/MarketingSaaS/image/V2/other/shanghu.png)" + for _, report := range reports { + err = d.dingTalkBotBiz.SendReport(ctx, groupInfo, report) + if err != nil { + log.Error(err) + continue + } + } return nil } diff --git a/internal/services/dtalk_bot.go b/internal/services/dtalk_bot.go index 4635b63..b71e40b 100644 --- a/internal/services/dtalk_bot.go +++ b/internal/services/dtalk_bot.go @@ -3,7 +3,6 @@ package services import ( "ai_scheduler/internal/biz" "ai_scheduler/internal/config" - "ai_scheduler/internal/data/constants" "ai_scheduler/internal/entitys" "context" "log" @@ -136,24 +135,3 @@ func (d *DingBotService) runBackgroundTasks(ctx context.Context, data *chatbot.B return nil } - -func (d *DingBotService) CronReportSend(ctx context.Context) error { - reportChan, err := d.dingTalkBotBiz.GetReportLists(ctx) - if err != nil { - return err - } - groupId := 23 - groupInfo, err := d.dingTalkBotBiz.GetGroupInfo(ctx, groupId) - if err != nil { - return err - } - err = d.dingTalkBotBiz.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{ - RobotCode: groupInfo.RobotCode, - ConversationType: constants.ConversationTypeGroup, - ConversationId: groupInfo.ConversationID, - Text: chatbot.BotCallbackDataTextModel{ - Content: "报表", - }, - }, reportChan) - return nil -} diff --git a/internal/services/dtalk_bot_test.go b/internal/services/dtalk_bot_test.go index 2b8223f..10f027d 100644 --- a/internal/services/dtalk_bot_test.go +++ b/internal/services/dtalk_bot_test.go @@ -15,6 +15,7 @@ import ( "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 +28,7 @@ import ( func Test_Report(t *testing.T) { run() - a := dingBotService.CronReportSend(context.Background()) + a := cronService.CronReportSend(context.Background()) t.Log(a) } @@ -35,6 +36,7 @@ var ( configConfig *config.Config err error dingBotService *DingBotService + cronService *CronService ) // run 函数是程序的入口函数,负责初始化和配置各个组件 @@ -99,7 +101,9 @@ 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) + dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, toolRegis, botChatHisImpl, manager, configConfig, sendCardClient, utils_ossClient) // 初始化钉钉机器人服务 + cronService = NewCronService(configConfig, dingTalkBotBiz) dingBotService = NewDingBotService(configConfig, dingTalkBotBiz) } diff --git a/internal/tools/bbxt/api.go b/internal/tools/bbxt/api.go index b07ce1e..8b6c823 100644 --- a/internal/tools/bbxt/api.go +++ b/internal/tools/bbxt/api.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/url" + "strings" ) @@ -40,10 +41,10 @@ func StatisOursProductLossSumApi(param *StatisOursProductLossSumReq) (*StatisOur } type GetProfitRankingSumRequest struct { - Ct []string `protobuf:"bytes,1,rep,name=ct,proto3" json:"ct,omitempty"` - Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` - Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` - ResellerIds []int32 `protobuf:"varint,5,rep,packed,name=reseller_ids,json=resellerIds,proto3" json:"reseller_ids,omitempty"` + Ct []string `json:"ct,omitempty"` + Page int32 `json:"page,omitempty"` + Limit int32 `json:"limit,omitempty"` + ResellerIds []int32 `json:"reseller_ids,omitempty"` } type GetProfitRankingSumResponse struct { @@ -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 销量同比分析 @@ -169,16 +170,19 @@ func GetStatisFilterOfficialProductApi(param *GetStatisFilterOfficialProductRequ 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) } @@ -195,13 +199,50 @@ func request(url string, reqData interface{}, resData interface{}) error { return nil } -func customEncode(params url.Values) string { - encoded := params.Encode() +// FormatPHPURL 将 url.Values 格式化为 PHP 风格的 URL +// 输入:基础URL和url.Values参数 +// 输出:PHP风格的URL字符串 +func FormatPHPURL(baseURL string, values url.Values) string { + if values == nil || len(values) == 0 { + return baseURL + } - // 解码我们想要保留的字符 - encoded = strings.ReplaceAll(encoded, "%5B", "[") // 恢复 [ - encoded = strings.ReplaceAll(encoded, "%5D", "]") // 恢复 ] - encoded = strings.ReplaceAll(encoded, "%2B", "+") // 恢复 + + var queryParts []string - return encoded + // 遍历所有参数 + for key, paramValues := range values { + // 检查这个key是否有多个值(数组参数) + if len(paramValues) > 1 { + // 多值参数,使用PHP数组格式:key[]=value + for _, value := range paramValues { + if value != "" { + // 编码值 + encodedValue := url.QueryEscape(value) + // 使用PHP数组格式 + queryParts = append(queryParts, fmt.Sprintf("%s[]=%s", key, encodedValue)) + } + } + } else if len(paramValues) == 1 && paramValues[0] != "" { + // 单值参数 + encodedValue := url.QueryEscape(paramValues[0]) + queryParts = append(queryParts, fmt.Sprintf("%s=%s", key, encodedValue)) + } + } + + if len(queryParts) == 0 { + return baseURL + } + + // 构建查询字符串 + query := strings.Join(queryParts, "&") + + // 转换为PHP风格:解码中括号和冒号 + query = strings.ReplaceAll(query, "%5B", "[") + query = strings.ReplaceAll(query, "%5D", "]") + query = strings.ReplaceAll(query, "%3A", ":") + + // 注意:保留空格为+号(这是PHP的常见格式) + // query = strings.ReplaceAll(query, "+", "%20") // 如果需要将+转为%20,可以取消注释 + + return baseURL + "?" + query } diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index a5c2aa3..3a1619d 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -1,27 +1,28 @@ package bbxt import ( - "ai_scheduler/internal/pkg/oss" + "ai_scheduler/internal/pkg/utils_oss" "ai_scheduler/pkg" "fmt" - "reflect" - "regexp" "math/rand" - "sort" - "strings" - "time" - "github.com/go-kratos/kratos/v2/log" - "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}" ) type BbxtTools struct { cacheDir string excelTempDir string - ossClient *oss.Client + ossClient *utils_oss.Client } -func NewBbxtTools(ossClient *oss.Client) (*BbxtTools, error) { +func NewBbxtTools() (*BbxtTools, error) { cache, err := pkg.GetCacheDir() if err != nil { return nil, err @@ -34,24 +35,42 @@ func NewBbxtTools(ossClient *oss.Client) (*BbxtTools, error) { return &BbxtTools{ cacheDir: cache, excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir), - ossClient: ossClient, }, nil } -func (b *BbxtTools) DailyReport(now time.Time) (err error) { - - err = b.StatisOursProductLossSum([]string{ - time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"), - time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.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) + uploader := NewUploader(ossClient) + if ossClient != nil { + for _, report := range reports { + _ = uploader.Run(report) + } + } + return } -// StatisOursProductLossSumTotal 负利润分析 -func (b *BbxtTools) StatisOursProductLossSum(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(time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())), + } + data, err := StatisOursProductLossSumApi(&StatisOursProductLossSumReq{ Ct: ct, }) @@ -106,7 +125,10 @@ func (b *BbxtTools) StatisOursProductLossSum(ct []string) (err error) { 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 { @@ -117,27 +139,48 @@ func (b *BbxtTools) StatisOursProductLossSum(ct []string) (err error) { } if v.Total <= -500 { gt = append(gt, v) + totalSum500 += v.Total } + totalSum += v.Total } + report = make([]*ReportRes, 2) + //总量生成excel if len(total) > 0 { filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx" - err = b.SimpleFillExcel(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total) + err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total, "") + report[0] = &ReportRes{ + ReportName: "负利润分析(合计表)", + Title: "截至今日23点利润累计亏损" + fmt.Sprintf("%.2f", totalSum), + Path: filePath, + Data: total, + } + } + if err != nil { + return } - if len(gt) > 0 { filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx" - // err = b.resellerDetailFillExcel(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) + report[1] = &ReportRes{ + ReportName: "负利润分析(亏损500以上)", + Title: "截至今日23点亏顺500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500), + Path: filePath, + Data: total, + } } - return err + if err != nil { + return + } + return report, nil } // GetProfitRankingSum 利润同比分销商排行榜 -func (b *BbxtTools) GetProfitRankingSum(now time.Time) (err error) { +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"), - now.Format(time.DateTime), + adjustedTime(now), } data, err := GetProfitRankingSumApi(&GetProfitRankingSumRequest{ @@ -166,9 +209,9 @@ func (b *BbxtTools) GetProfitRankingSum(now time.Time) (err error) { for _, v := range top { var diff string if v.HistoryOneDiff > 0 { - diff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%.4f", v.HistoryOneDiff) + diff = fmt.Sprintf("%s↑%.4f", RedStyle, v.HistoryOneDiff) } else { - diff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%.4f", v.HistoryOneDiff) + diff = fmt.Sprintf("%s↓%.4f", GreenStyle, v.HistoryOneDiff) } total = append(total, []string{ fmt.Sprintf("%s", v.ResellerName), @@ -178,18 +221,25 @@ func (b *BbxtTools) GetProfitRankingSum(now time.Time) (err error) { }) } //总量生成excel - if len(total) > 0 { - filePath := b.cacheDir + "/lrtb_rank" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" - err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"lrtb_rank.xlsx", filePath, total, title) + if len(total) == 0 { + return } - return err + 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) (err error) { +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"), - now.Format(time.DateTime), + adjustedTime(now), } var ids []int32 if len(productName) > 0 { @@ -215,14 +265,14 @@ func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []str lastWeekDiff string ) if v.HistoryOneDiff > 0 { - yeterDatyDiff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%d", v.HistoryOneDiff) + yeterDatyDiff = fmt.Sprintf("%s↑%d", RedStyle, v.HistoryOneDiff) } else { - yeterDatyDiff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%d", v.HistoryOneDiff) + yeterDatyDiff = fmt.Sprintf("%s↓%d", GreenStyle, v.HistoryOneDiff) } if v.HistoryTwoDiff > 0 { - lastWeekDiff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%d", v.HistoryTwoDiff) + lastWeekDiff = fmt.Sprintf("%s↑%d", RedStyle, v.HistoryTwoDiff) } else { - lastWeekDiff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%d", v.HistoryTwoDiff) + lastWeekDiff = fmt.Sprintf("%s↓%d", GreenStyle, v.HistoryTwoDiff) } total = append(total, []string{ fmt.Sprintf("%s", v.OfficialProductName), @@ -237,11 +287,17 @@ func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []str timeCh := now.Format("1月2日15点") title := "截至" + timeCh + "销售同比分析" //总量生成excel - if len(total) > 0 { - filePath := b.cacheDir + "/xstb_ana" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx" - err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"xstb_ana.xlsx", filePath, total, title) + if len(total) == 0 { + return } - return err + 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) { @@ -262,149 +318,11 @@ func (b *BbxtTools) getProductIdFromProductName(productNames []string) ([]int32, return ids, nil } -func (b *BbxtTools) SimpleFillExcelWithTitle(templatePath, outputPath string, dataSlice interface{}, title string) error { - // 1. 打开模板 - f, err := excelize.OpenFile(templatePath) - if err != nil { - return err - } - defer f.Close() - - sheet := f.GetSheetName(0) - - // 1.1 获取第三行模板样式 - templateRow := 3 - styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", templateRow)) - if err != nil { - log.Errorf("获取模板样式失败: %v", err) - styleID = 0 - } - - // 1.2 获取模板行高 - rowHeight, err := f.GetRowHeight(sheet, templateRow) - if err != nil { - log.Errorf("获取模板行高失败: %v", err) - rowHeight = 31 // 默认高度 - } - - // 2. 写入标题到第一行 - f.SetCellValue(sheet, "A1", title) - - // 3. 反射获取切片数据 - v := reflect.ValueOf(dataSlice) - if v.Kind() != reflect.Slice { - return fmt.Errorf("dataSlice must be a slice") - } - - if v.Len() == 0 { - return nil - } - - // 4. 从第三行开始填充数据(第二行留空或作为标题行) - startRow := 3 - 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) - - // 5. 填充数据到Excel - for col, value := range rowData { - cell := fmt.Sprintf("%c%d", 'A'+col, currentRow) - // 5.1 应用模板样式到整行(根据实际列数) - 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) - } - 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: - - } - } - - } - - // 6. 保存 - 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 - } - - return f.NewStyle(style) +func adjustedTime(t time.Time) string { + adjusted := time.Date( + t.Year(), t.Month(), t.Day(), + t.Hour(), t.Minute(), 59, 999_000_000, + t.Location(), + ) + return adjusted.Format("2006-01-02 15:04:05.999") } diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go index e3cfb73..4a5f700 100644 --- a/internal/tools/bbxt/bbxt_test.go +++ b/internal/tools/bbxt/bbxt_test.go @@ -2,30 +2,44 @@ package bbxt import ( "ai_scheduler/internal/config" - "ai_scheduler/internal/pkg/oss" + "ai_scheduler/internal/pkg/utils_oss" "testing" "time" ) func Test_StatisOursProductLossSumApiTotal(t *testing.T) { - ossClient, err := oss.NewClient(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", - }) + 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(ossClient) + o, err := NewBbxtTools() if err != nil { panic(err) } - err = o.DailyReport(time.Date(2025, 12, 30, 0, 0, 0, 0, time.Local)) + 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) } @@ -34,9 +48,9 @@ func Test_GetProfitRankingSum(t *testing.T) { if err != nil { panic(err) } - err = o.GetProfitRankingSum(time.Now()) + report, err := o.GetProfitRankingSum(time.Now()) - t.Log(err) + t.Log(report, err) } @@ -45,8 +59,8 @@ func Test_GetStatisOfficialProductSum(t *testing.T) { if err != nil { panic(err) } - err = o.GetStatisOfficialProductSum(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}) + report, err := o.GetStatisOfficialProductSum(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}) - t.Log(err) + t.Log(report, err) } diff --git a/internal/tools/bbxt/entitys.go b/internal/tools/bbxt/entitys.go index a3dc743..d886dd6 100644 --- a/internal/tools/bbxt/entitys.go +++ b/internal/tools/bbxt/entitys.go @@ -12,3 +12,12 @@ type ProductLoss struct { ProductName string Loss float64 } + +type ReportRes struct { + ReportName string + Title string + Path string + Url string + Data [][]string + Desc string +} diff --git a/internal/tools/bbxt/excel.go b/internal/tools/bbxt/excel.go index f5b52bf..b6b1b6c 100644 --- a/internal/tools/bbxt/excel.go +++ b/internal/tools/bbxt/excel.go @@ -1,14 +1,9 @@ package bbxt import ( - "bytes" "fmt" - "io" - "mime/multipart" - "net/http" - "os" - "path/filepath" "reflect" + "regexp" "sort" "strings" @@ -17,9 +12,9 @@ import ( "github.com/xuri/excelize/v2" ) -// 最简单的通用函数 -func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice interface{}) error { - // 1. 打开模板 +func (b *BbxtTools) SimpleFillExcelWithTitle(templatePath, outputPath string, dataSlice interface{}, title string) error { + // 打开模板 + f, err := excelize.OpenFile(templatePath) if err != nil { return err @@ -27,175 +22,155 @@ func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice i defer f.Close() sheet := f.GetSheetName(0) - - // 1.1 获取第二行模板样式 - resellerTplRow := 2 - styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow)) - if err != nil { - log.Errorf("获取分销商总计样式失败: %v", err) - styleIDReseller = 0 + startLen := 2 + if len(title) > 0 { + // 写入标题 + f.SetCellValue(sheet, "A1", title) + startLen = 3 } - // 1.2 获取分销商总计行高 - rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow) + // 获取模板样式 + templateRow := startLen + styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", templateRow)) if err != nil { - log.Errorf("获取分销商总计行高失败: %v", err) - rowHeightReseller = 31 // 默认高度 + log.Errorf("获取模板样式失败: %v", err) + styleID = 0 } - // 2. 反射获取切片数据 + // 获取模板行高 + 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") } - // 3. 从第2行开始填充 - row := 2 - for i := 0; i < v.Len(); i++ { - item := v.Index(i).Interface() - currentRow := row + i + if v.Len() == 0 { + return nil + } - // 4. 将item转换为一行数据 + // 从第三行开始填充数据(第二行留空或作为标题行) + 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 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()) + 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} + rowData = []interface{}{item.Interface()} } - // 4.1 设置行高 - f.SetRowHeight(sheet, currentRow, rowHeightReseller) - // 5. 填充到Excel + // 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) - f.SetCellValue(sheet, cell, value) + + 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: + + } } - // 5.1 使用第二行模板样式 - if styleIDReseller != 0 { - f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller) - } } - - excelBytes, err := f.WriteToBuffer() - if err != nil { - return fmt.Errorf("write to bytes failed: %v", err) - } - - picBytes, err := b.excel2picPy(templatePath, excelBytes.Bytes()) - if err != nil { - return fmt.Errorf("excel2picPy failed: %v", err) - } - // b.savePic("temp.png", picBytes) // 本地生成图片,仅测试 - // outputPath 提取文件名(不包含扩展名) - filename := filepath.Base(outputPath) - filename = strings.TrimSuffix(filename, filepath.Ext(filename)) - imgUrl := b.uploadToOSS(filename, picBytes) - log.Infof("imgUrl: %s", imgUrl) - - // 6. 保存 + // 保存 return f.SaveAs(outputPath) } -// 分销商负利润详情填充excel -// 1.使用模板文件作为输出文件 -// 2.分销商总计使用第二行样式(宽高、背景、颜色等) -// 3.商品详情使用第三行样式(宽高、背景、颜色等) -// 4.保存为新文件 -func (b *BbxtTools) resellerDetailFillExcel(templatePath, outputPath string, dataSlice []*ResellerLoss) error { - // 1. 读取模板 - f, err := excelize.OpenFile(templatePath) - if err != nil { - return err - } - defer f.Close() +func SetStyle(styleMap map[string]string, f *excelize.File) (int, error) { - sheet := f.GetSheetName(0) - - // 获取模板样式1:第二行-分销商总计 - resellerTplRow := 2 - styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow)) - if err != nil { - log.Errorf("获取分销商总计样式失败: %v", err) - styleIDReseller = 0 - } - rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow) - if err != nil { - log.Errorf("获取分销商总计行高失败: %v", err) - rowHeightReseller = 31 // 默认高度 - } - // 获取模板样式2:第三行-产品亏损明细 - productTplRow := 3 - styleIDProduct, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", productTplRow)) - if err != nil { - log.Errorf("获取商品详情样式失败: %v", err) - styleIDProduct = 0 - } - rowHeightProduct, err := f.GetRowHeight(sheet, productTplRow) - if err != nil { - log.Errorf("获取商品详情行高失败: %v", err) - rowHeightProduct = 25 // 默认高度 - } - - currentRow := 2 - - for _, reseller := range dataSlice { - // 3. 填充经销商数据 (ResellerName, Total) - // 设置行高 - f.SetRowHeight(sheet, currentRow, rowHeightReseller) - - // 设置单元格值 - f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), reseller.ResellerName) - f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), reseller.Total) - - // 应用样式 - if styleIDReseller != 0 { - f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller) - } - - currentRow++ - - // 4. 填充产品亏损明细 - // 先对 ProductLoss 进行排序 - var products []ProductLoss - for _, p := range reseller.ProductLoss { - products = append(products, p) - } - // 按 Loss 升序排序 (亏损越多越靠前,负数越小) - sort.Slice(products, func(i, j int) bool { - return products[i].Loss < products[j].Loss - }) - - for _, p := range products { - // 设置行高 - f.SetRowHeight(sheet, currentRow, rowHeightProduct) - - // 设置单元格值 - f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("·%s", p.ProductName)) - f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.Loss) - - // 应用样式 - if styleIDProduct != 0 { - f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDProduct) - } - - currentRow++ + 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 + } - // 6. 保存 - return f.SaveAs(outputPath) + // 设置边框(新增) + 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 @@ -327,118 +302,6 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d // 取消合并合计行的A、B列 // f.MergeCell(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow)) - excelBytes, err := f.WriteToBuffer() - if err != nil { - return fmt.Errorf("write to bytes failed: %v", err) - } - - picBytes, err := b.excel2picPy(templatePath, excelBytes.Bytes()) - if err != nil { - return fmt.Errorf("excel2picPy failed: %v", err) - } - // b.savePic("temp.png", picBytes) // 本地生成图片,仅测试 - // outputPath 提取文件名(不包含扩展名) - filename := filepath.Base(outputPath) - filename = strings.TrimSuffix(filename, filepath.Ext(filename)) - imgUrl := b.uploadToOSS(filename, picBytes) - log.Infof("imgUrl: %s", imgUrl) - // 6. 保存 return f.SaveAs(outputPath) } - -// 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 (b *BbxtTools) excel2picPy(templatePath string, excelBytes []byte) ([]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) - } - - if err = writer.Close(); err != nil { - return nil, fmt.Errorf("close writer failed: %v", err) - } - - // 3. 发送 HTTP POST 请求 - url := "http://192.168.6.109:8010/api/v1/convert" - req, err := http.NewRequest("POST", url, 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 (b *BbxtTools) 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 (b *BbxtTools) uploadToOSS(fileName string, fileBytes []byte) string { - objectKey := fmt.Sprintf("ai-scheduler/data-analytics/images/%s.png", fileName) - url, err := b.ossClient.UploadBytes(objectKey, fileBytes) - if err != nil { - log.Errorf("oss upload failed: %v", err) - return "" - } - return url -} diff --git a/internal/tools/bbxt/upload.go b/internal/tools/bbxt/upload.go new file mode 100644 index 0000000..a5d0886 --- /dev/null +++ b/internal/tools/bbxt/upload.go @@ -0,0 +1,153 @@ +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()) + 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) ([]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) + } + + 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 +} From b39d58280cdbd583f31ff1aa19e9ae07880c4359 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 10:17:46 +0800 Subject: [PATCH 13/27] =?UTF-8?q?fix=EF=BC=9A=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=88=91=E4=BB=AC=E7=9A=84=E5=95=86=E5=93=81=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/config/config.go | 2 + internal/domain/repo/repos.go | 12 +- .../tools/common/excel_generator/client.go | 76 +++++++++++ .../tools/common/image_converter/client.go | 58 +++++++++ internal/domain/tools/registry.go | 23 +++- .../recharge/statistics_ours_product.go | 121 +++++++++++++++++- internal/services/dtalk_bot_test.go | 2 +- .../recharge_statistics_ours_product.xlsx | Bin 0 -> 9787 bytes 8 files changed, 284 insertions(+), 10 deletions(-) create mode 100644 internal/domain/tools/common/excel_generator/client.go create mode 100644 internal/domain/tools/common/image_converter/client.go create mode 100755 tmpl/excel_temp/recharge_statistics_ours_product.xlsx diff --git a/internal/config/config.go b/internal/config/config.go index 7e94bfd..add63a1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -209,6 +209,8 @@ type EinoToolsConfig struct { DaOfficialProductDecline ToolConfig `mapstructure:"daOfficialProductDecline"` // 我们的商品统计 RechargeStatisticsOursProduct ToolConfig `mapstructure:"rechargeStatisticsOursProduct"` + // Excel 转图片 + Excel2Pic ToolConfig `mapstructure:"excel2Pic"` } // LoggingConfig 日志配置 diff --git a/internal/domain/repo/repos.go b/internal/domain/repo/repos.go index 40ba3de..a3250ba 100644 --- a/internal/domain/repo/repos.go +++ b/internal/domain/repo/repos.go @@ -1,17 +1,21 @@ package repo import ( + "ai_scheduler/internal/config" "ai_scheduler/internal/data/impl" - "ai_scheduler/utils" + "ai_scheduler/internal/pkg/oss" ) // Repos 聚合所有 Repository type Repos struct { - Session SessionRepo + Session SessionRepo + OssClient *oss.Client } -func NewRepos(sessionImpl *impl.SessionImpl, rdb *utils.Rdb) *Repos { +func NewRepos(sessionImpl *impl.SessionImpl, cfg *config.Config) *Repos { + ossClient, _ := oss.NewClient(cfg.Oss) return &Repos{ - Session: NewSessionAdapter(sessionImpl), + Session: NewSessionAdapter(sessionImpl), + OssClient: ossClient, } } diff --git a/internal/domain/tools/common/excel_generator/client.go b/internal/domain/tools/common/excel_generator/client.go new file mode 100644 index 0000000..eae9451 --- /dev/null +++ b/internal/domain/tools/common/excel_generator/client.go @@ -0,0 +1,76 @@ +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 字节流 +// templatePath: 模板文件路径 +// data: 二维字符串数组,不再使用反射 +// startRow: 数据填充起始行 (默认 2) +// styleRow: 样式参考行 (默认 2) +func (g *Client) Call(templatePath string, data [][]string, startRow int, styleRow int) ([]byte, error) { + if startRow <= 0 { + startRow = 2 + } + if styleRow <= 0 { + styleRow = 2 + } + + f, err := excelize.OpenFile(templatePath) + if err != nil { + return nil, err + } + defer f.Close() + + sheet := f.GetSheetName(0) + + // 获取样式和行高 + styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", styleRow)) + if err != nil { + log.Errorf("获取样式失败: %v", err) + styleID = 0 + } + rowHeight, err := f.GetRowHeight(sheet, styleRow) + if err != nil { + log.Errorf("获取行高失败: %v", err) + rowHeight = 31 // 默认高度 + } + + row := startRow + for i, item := range data { + currentRow := row + i + + // 设置行高 + f.SetRowHeight(sheet, currentRow, rowHeight) + + // 填充数据 + for col, value := range item { + cell := fmt.Sprintf("%c%d", 'A'+col, currentRow) + f.SetCellValue(sheet, cell, value) + } + + // 设置样式 + if styleID != 0 { + endCol := 'A' + len(item) - 1 + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("%c%d", endCol, currentRow), styleID) + } + } + + buf, err := f.WriteToBuffer() + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/internal/domain/tools/common/image_converter/client.go b/internal/domain/tools/common/image_converter/client.go new file mode 100644 index 0000000..5eba199 --- /dev/null +++ b/internal/domain/tools/common/image_converter/client.go @@ -0,0 +1,58 @@ +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) ([]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 + } + + if err = writer.Close(); err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.cfg.BaseURL, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("excel2pic service returned status: %s", resp.Status) + } + + return io.ReadAll(resp.Body) +} diff --git a/internal/domain/tools/registry.go b/internal/domain/tools/registry.go index ad9439d..f6b2193 100644 --- a/internal/domain/tools/registry.go +++ b/internal/domain/tools/registry.go @@ -2,6 +2,8 @@ package tools import ( "ai_scheduler/internal/config" + "ai_scheduler/internal/domain/tools/common/excel_generator" + "ai_scheduler/internal/domain/tools/common/image_converter" "ai_scheduler/internal/domain/tools/hyt/goods_add" "ai_scheduler/internal/domain/tools/hyt/goods_brand_search" "ai_scheduler/internal/domain/tools/hyt/goods_category_add" @@ -10,13 +12,21 @@ import ( "ai_scheduler/internal/domain/tools/hyt/product_upload" "ai_scheduler/internal/domain/tools/hyt/supplier_search" "ai_scheduler/internal/domain/tools/hyt/warehouse_search" + "ai_scheduler/internal/domain/tools/recharge/statistics_ours_product" ) type Manager struct { - Hyt *HytTools + Hyt *HytTools + Recharge *RechargeTools + Common *CommonTools // Zltx *ZltxTools } +type CommonTools struct { + ExcelGenerator *excel_generator.Client + ImageConverter *image_converter.Client +} + type HytTools struct { ProductUpload *product_upload.Client SupplierSearch *supplier_search.Client @@ -28,6 +38,10 @@ type HytTools struct { GoodsBrandSearch *goods_brand_search.Client } +type RechargeTools struct { + StatisticsOursProduct *statistics_ours_product.Client +} + func NewManager(cfg *config.Config) *Manager { return &Manager{ Hyt: &HytTools{ @@ -40,5 +54,12 @@ func NewManager(cfg *config.Config) *Manager { GoodsCategorySearch: goods_category_search.New(cfg.EinoTools.HytGoodsCategorySearch), GoodsBrandSearch: goods_brand_search.New(cfg.EinoTools.HytGoodsBrandSearch), }, + Recharge: &RechargeTools{ + StatisticsOursProduct: statistics_ours_product.New(cfg.EinoTools.RechargeStatisticsOursProduct), + }, + Common: &CommonTools{ + ExcelGenerator: excel_generator.New(), + ImageConverter: image_converter.New(cfg.EinoTools.Excel2Pic), + }, } } diff --git a/internal/domain/workflow/recharge/statistics_ours_product.go b/internal/domain/workflow/recharge/statistics_ours_product.go index 0a7649d..91d5092 100644 --- a/internal/domain/workflow/recharge/statistics_ours_product.go +++ b/internal/domain/workflow/recharge/statistics_ours_product.go @@ -2,32 +2,145 @@ package recharge import ( "ai_scheduler/internal/config" + errorcode "ai_scheduler/internal/data/error" toolManager "ai_scheduler/internal/domain/tools" + "ai_scheduler/internal/domain/tools/recharge/statistics_ours_product" "ai_scheduler/internal/domain/workflow/runtime" "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg/oss" "context" + "errors" + "fmt" + "path/filepath" + "time" + + "github.com/cloudwego/eino/compose" ) const WorkflowIDStatisticsOursProduct = "recharge.statisticsOursProduct" func init() { runtime.Register(WorkflowIDStatisticsOursProduct, func(d *runtime.Deps) (runtime.Workflow, error) { - return &statisticsOursProduct{cfg: d.Conf, toolManager: d.ToolManager}, nil + return &statisticsOursProduct{cfg: d.Conf, toolManager: d.ToolManager, ossClient: d.Repos.OssClient}, nil }) } type statisticsOursProduct struct { cfg *config.Config toolManager *toolManager.Manager + ossClient *oss.Client } type StatisticsOursProductWorkflowInput struct { - // 预留字段 + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` +} + +type StatisticsOursProductWorkflowOutput struct { + ImgUrl string `json:"img_url"` } func (w *statisticsOursProduct) ID() string { return WorkflowIDStatisticsOursProduct } func (w *statisticsOursProduct) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) { - // 保持流程为空,仅返回 nil - return nil, nil + // 构建工作流 + runnable, err := w.buildWorkflow(ctx) + if err != nil { + return nil, err + } + + // 解析参数 (假设参数在 rec.Match.Parameters 中,或者根据实际情况解析) + // 这里简化处理,假设需要解析参数 + // 实际上这里应该根据 LLM 解析的结果来填充 Input + // 暂时假设 ParameterResult 是 JSON 字符串 + input := &StatisticsOursProductWorkflowInput{ + // 默认值,具体应从 rec 解析 + StartTime: time.Now().Format("2006010200"), + EndTime: time.Now().Format("2006010223"), + } + + // 工作流过程调用 + 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 map[string]any{"img_url": output.ImgUrl}, nil +} + +func (w *statisticsOursProduct) buildWorkflow(ctx context.Context) (compose.Runnable[*StatisticsOursProductWorkflowInput, *StatisticsOursProductWorkflowOutput], error) { + c := compose.NewChain[*StatisticsOursProductWorkflowInput, *StatisticsOursProductWorkflowOutput]() + + // 1. 调用工具统计我们的商品 + c.AppendLambda(compose.InvokableLambda(w.callStatisticsTool)) + + // 2. 生成 Excel 并转图片上传 + c.AppendLambda(compose.InvokableLambda(w.generateExcelAndUpload)) + + return c.Compile(ctx) +} + +func (w *statisticsOursProduct) callStatisticsTool(ctx context.Context, input *StatisticsOursProductWorkflowInput) ([]statistics_ours_product.StatisticsOursProductItem, error) { + req := statistics_ours_product.StatisticsOursProductRequest{ + Page: 1, + Limit: 100, // 假设取前100条 + Serial: []string{input.StartTime, input.EndTime}, + } + + return w.toolManager.Recharge.StatisticsOursProduct.Call(ctx, req) +} + +func (w *statisticsOursProduct) generateExcelAndUpload(ctx context.Context, data []statistics_ours_product.StatisticsOursProductItem) (*StatisticsOursProductWorkflowOutput, error) { + // 2. 获取模板路径 (假设在项目根目录的 assets/templates 下) + cwd, _ := filepath.Abs(".") + templatePath := filepath.Join(cwd, "assets", "templates", "statistics_ours_product.xlsx") + fileName := fmt.Sprintf("statistics_ours_product_%d", time.Now().Unix()) + + // 3. 转换数据为 [][]string + excelData := w.convertDataToExcelFormat(data) + + // 4. 生成 Excel + excelBytes, err := w.toolManager.Common.ExcelGenerator.Call(templatePath, excelData, 2, 2) + if err != nil { + return nil, fmt.Errorf("生成 Excel 失败: %v", err) + } + + // 5. Excel 转图片 + picBytes, err := w.toolManager.Common.ImageConverter.Call(fileName+".xlsx", excelBytes) + if err != nil { + return nil, fmt.Errorf("Excel 转图片失败: %v", err) + } + + // 6. 上传 OSS + url, err := w.ossClient.UploadBytes(fileName+".png", picBytes) + if err != nil { + return nil, fmt.Errorf("上传 OSS 失败: %v", err) + } + + return &StatisticsOursProductWorkflowOutput{ImgUrl: url}, 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), + item.Count, + item.TotalPrice, + item.SuccessCount, + item.SuccessPrice, + item.FailCount, + item.FailPrice, + item.Profit, + } + result = append(result, row) + } + return result } diff --git a/internal/services/dtalk_bot_test.go b/internal/services/dtalk_bot_test.go index 2b8223f..6180016 100644 --- a/internal/services/dtalk_bot_test.go +++ b/internal/services/dtalk_bot_test.go @@ -60,7 +60,7 @@ func run() { // 初始化Redis数据库连接 rdb := utils.NewRdb(configConfig) // 初始化仓库层 - repos := repo.NewRepos(sessionImpl, rdb) + repos := repo.NewRepos(sessionImpl, configConfig) // 初始化包级别的Redis连接 pkgRdb := pkg.NewRdb(configConfig) diff --git a/tmpl/excel_temp/recharge_statistics_ours_product.xlsx b/tmpl/excel_temp/recharge_statistics_ours_product.xlsx new file mode 100755 index 0000000000000000000000000000000000000000..8d5b5a301642c7c87f355460c49b3ecf5a7d3d09 GIT binary patch literal 9787 zcma)i1z1#D*FW7cv~+h%O1E@}bcYO`LnugtNJ^JKEJ)rn!VRr%5u;!NDz-LSaD6{@%mRo1b;EJH&u4BcW`D^29sff2M~Uc zHSo-pvq3^Ya6&;q0RBm4?BKuxvb9NzZj*&($CkYGdjNd0=U}E#eM;{a!VjS!=@jUJK(c&Y1Q(WzOVDa5bR+>(C=Shr%e!rhaMzZ*+z5FAP8-e0FRfFnua?Khiy~O{SZZ0IkYDfXmBw{~a_bmy zH@>aztltM#uJQ{*&n@apANa2@59=sf2_oKb8wllY=)i3gIM2(L=eEr*bd8Vh&BLCp z-Cds?oU^d3GTrQyCUk5o??JQgK&$M-HTZR6ywh*b?IvjV(2$Pwflvx+6%=n*;C;&l zk(Vake7{z=qs8<6+U^AQcXS!z_Tq)X=mNp$V*C?bQ+p@#M|j`G7%Fv;VRtPn+@hf# zF^?9q=NznM?DtXA3|o0qI@DqyQ~SDozZiEbp1NM4Om+52brxq|-r>83+0rnTmEiEh zWXp=~w~;+-)XABlJ9$Icx7RHada*Lr$S2GgV4mf8yiI`2LU$}fg+CI&JjM+(z;b-Go4A_7|gpF8*bRVZ_hv1W*YA4hxWK{Fq~q^j#rUq-BSYszwE({)i+K z;cNEx5czi*&`nPrt#K~6-S|4wKIJ~?9BcY!%~@lcQe7z|HK=s5ce+|wp3E?DAPZ51 zM4#x;D!yJsSQw`wRClPp5;|`CaZ`wCy?q90U*~Q-W5%204UAYc6Y~N+r2k}0y6uF5nR{S;7epes17mD9=E+W~g z#<=ixKG;d6C1`aeIJ7_W_TGwNF$`LI=8(%z|I}9yhKME6hVSD*>w2D(>57%w+q}cv z6?|SN)cEVKlAie+y>UBrZcJj^DEsg5@c`B}TjHgHDY)DdvRsqhucy%ZYYv{UQm4SN zpim(MMk&nqwh;?E8f`M8ZPxZmeTq$6vWv)`8k#`A8hZ&p!G#ZE_RnI1#;)67pw|mO zo>#b~a#9HgXvb#BYHHcH;wyE*@Me+N?9Fsde?qeuiyWCR5Xh`6@}mns5Wlm=$&Lz| z?2+79r<3Z!>ZZSCP+HYqj}*Jt-riiDzL=9AEXgmkuhF|Zrlccdp-I;-dQNw`(=t0+ zsi1OcvkNpu-+8@zT{1rAP2z{zj1G~G%bB5ckM?Nb|L$d=!H|P&eyqvA6QKPhaJP2? zI$N2WyZq^k%yw{sEWm3g4+{bDOT}Q z9>`9;PJhQij_a0Mjr#K8F2Yv&9M4#mxL>OgA%dCBIJ{N*=?r}y@E(lRhIMaaxt8pmSA+jJkKB3-rAJw z^2(ZL&=nVi^R~()TcE`xc_5x9An6Vvs1QhRWZtPLzK0b8+acU|{YM z*+g=lDy^5B0$HAzx-+5rVqN}#!T63TRqGe!>`Z>Pw6TMS*8?%%yHq?M+BD_Bq}(BQ7kMQi-jor5%Yx~;$5d6IO?rMolu*1#&yMf z{nE++Ychf20q?ZGLfj2trg!6F!?k}h!XZ`85x<4MK7()dv6d;8oHA5?Q+i_Dh-XYH z0-+MvQR9d!e%AHbQPQTa71rGwdinEW?=%K1SNx{qH1}tkCeTC|dHK5@B`OlaTLnlv zGW4>dcPBk~Rf_cuk!?ontHO0x=>NB&KmMJ94c*1c+}8XL7Zh;w9pwsazRh6!eRM&8 zll>t6VZ8_1Ly?4B*e#SN!r_}shg3s7Zc{eZ*SliwUo>KCcE@B=!wE+~X8a99sMXJ@ zs3b)7qUsz)L~=FPRPU1mDdk?T2L6nl2C0jNoBm!!)13KQ0bt{^w>Nu|1HX#9uo z;qI%&Vk%TJ1ic8YywCv5Ri-u z&!@f}83_6iGfn_H?;FhR+_r5RlmE#Pz-&%J=a;S2_MZQEn)7zYvMX zeV=e7e1Dxkaw|fYl`F~G3_9Jbq-AZs-Ke!E^;&@^LbU;9P`!ZRIo5V~OTChU77lzy zay%2e-4nu#Ne8z`GDVEf-%98#@v4r-&EGv({NWg~eCUwB=E6iMwv*%AIj^H{Pxu(k z`Vxemma{g-;mCNXkCRZPV=aL1d}vXuFm`^J$-hj-n?aY|ZBQVSF3!qNjS2evodWv> z$UPE3Th?RNn`C5)zd$L9g<&s!W3~c#N$(CN#ri27?cxIcq+WhR`cx!_vy%tn>_)0?yGqM0UDemdR&j zE-$1U2>J4D$79HL2g1FaJWx4;TbiB`Q@G_XuP|R>*h<3vep?AD1nuO<*vy152 z1Zfv(Wm~bo5sGOK@T?CNDr8ZQ9}IQQHZaYKfs{k`H=+czUSwj#?@5s;!iTIyKV?s- zxU0x8oX7_SA!o3>FLuCwh^))C`WWMoJin9w(gDsLn?cN&O+uTJsN)R^Ni515)uaXN z7^0pj3N??%E39TSyvg(WszG-$?h@=<@@FBywbla1Eu%>62!VZ2e z(R$KC0GT$4tf!^H=yA>(O5{Lugo%G?^1dyuBR(R~3YSt`KhZ?o9FJy)^To7Zv4K`q zIWXN(AP<00_bh<19r|lXGT;#X`LPqdkARA;rdRcGZAq%$xz-@zYG9-5riOs8^K8*k z-rCaaTKkDPOVPP%wY21plbL}5EzZSMS{MXxzL>csl%L9b-#2@`c$+o@{wWEmFZ@Gw zkZcL~_6$^jL8yc?JqKd_gmsrOQe)QXWt)Orz-b+y~hjnPC6FE|fsnSNFI$+~iQ5=gKl-qkP zsG8J4b_MN5gdFpEr%5i`RWgS*)N}8g z+Q{PTT^CA~k`JXu53sFpmze`qtNpH0xopHD{ud80zn=_}`<%Nm!FyE=@aOR~2L7XP zwla1yH&b(QdTD3r{J76CA61a;5P^VQX<8u;no9{~W~S(KAqPPTwnl|3kR^pCg>ILf z=c9JTSRdq(1^VH_W-|ID@0npbA*)HiGPN|HEFe{<8@qV zj5jZEyT1N&|9Jksv$A}-#sVWLpY^!fW$m1GX16}HjLnDh?vdg7ZhbBS+_ztS2KZkJ zX~*B0L9ay<3^K5McmI0p=+9pmW_WtJB`h+hG!aBl3&62VZcB@p#R!0n%LU| ze{2)gRP0mO0IxHu9*nL9*`r1S>68O$tm2Z3WGh{~l8JczG%BJG!sRFKzuO?}TIOqXfezG=I^>7q7*K?>I72`0A@-(I zNKmhHe7kH;K2qJG1l4=>l&xaFsUocbv;#@4Avm=4Jk9m@SQW16axBR;!??VRbEsH^7A;7>#af=pD$S_xN7sgzv3O>&>1(nJKyxKw|YN$$LYwqS;#9e z7Qh-p9&o`hvFCOU-y^=jnEg0v0Z8Bat{HioH|AzmH6H@{-fs!a2U?h|H+h6y$2(gH zZM@p7L3*VvJ8&jLL8xQ0!e?NF?=X!nTZ@rKg?th@o6$8)bu2Z}sUW4s;~?ee%|U!! zY_{g(pydt}zMj*4RTFkSweuR9Azn69qO7N|K$@qn#jQ0hx`v=YNTyO}EK4m#m-F)n zEw&?Hxji2}w&gM##)RDmjEo`c1h%NueR#|x5}t-3k;4wp%X4acSp&PZDJNc^&x%^|9G7i^$#My zNaH_5bUqm6`k{wReq3(^*;ktLG{h?wDN7=YL8(6rtpu)iKI_>zAmJ+&4|3f>m6>^Vb@$5$IYazgUdmVqq$}$0f!vj zc%GU?viDTLSy!)%;|h(NL)7dACK=OZq6_&RVx7|QP%akzx$9l6nx6YmQ+?r2 zOoo08LF<}3y_e5*KM5R?XhS=D&k<{jU*#QnLQe!oE@cXkc*M^UQf9rOERK`aCrP}h zkg%R5aQUYGl&y(WN$P7E|C6NV#gD{agxg%>azx$t&O0*ql8E6Y1TW4iEI@?Co<{D1 z?)w*a1Ilsx7vJ4x!ek02%}46ZDfhN>(+*b6j8JYrPP)>Hw`h!KJ;%J>JSP!Qm&H`6 zKe!MHPZ{UQky~zbDRffvL~VL7PP#ig=bAHNU?BrSxW>jY(H48UDCJrzHxW2 z`F)UCu-!L&amBn7zax@J*DJ0+<0#k%U*(^05}Wy~LOOJ3r#xNQ%U6y#Y3Yey z06>%L3?*1v*~tjnXiPlmnC!t_xiE?_Z?#SX(6&~m@WL+KZpk=b<7qh3El0Q>(?_GZ zU2`@Ys16GuV&|vfHuy6U+qKdMyjxrb5g&`7}zV_ied^O^9l+Wka9^x1%r*~Syb9ZERT44Nmfmbb1Xqd_kxAVyXhM0-O<<+P!sDWE&v0&0 zSqRiHKfeXG8kTdP*CUyz$D>Zw-D?;v7fdB{NosMK&nbws`bb(&%kH^yboemaSzMKO z4Gk#1hfLlDy8Rk?N4?e9I`odKA!)S*mxtqaX3auiwd!^OWe*BCG=%a{UH9Wgc>ywn z93Aj4Q7u_1C%(8rqr@|NZJsIECFJxBD29ENX|_GP-`S$h_B$xgb{i-;OI%)&Y8JTF zc-44pqba~I^^;3ph0z&jqD^n;P(L6{PqxcgIIaXV$`od&FB$HdtTSyz16jN2icFqZUNq{f3>mB$j#!BgFKHAE z^vT64B#x16Oim~^O`lZQyiM>Rd!|ptA`WznZtwD{8P+@DWcPzXx~>C08+Luq+p-)P z^C^hii|8_G5wC(3u5>UPm1LV)-U8lU28xE%twy0ohzPBOyyQ|h-QIo7?hT7~RWIg) zqfeO*>QR1gk=E1alv|w;_lf-1FchOoUUh1s!bd?)qt9mqW@_t&mbTO&&kc0c8@Ws= zB?1+7YbwKx)f=^~D8q|Z@17HqGhmfSTRB!>KKOV&-F}h&hNp1sh3MXkvk4@>x6x0k z5mKnxC&>Jct3(6fpm{V9xi^z5sX1SSE2vCVYjX!aTSbk1lE=d%Z<1JuC5uv{)WJN% z&4gFW)~D`(s8WO@iTfdwj9WeikQ}^#S;h@bC-0BMjAyG1SwT<1iWgR~#dXRFgMfp? zRxgy9cs7h!ttrQjPt@+KgA`RKBuAYq0#k!s7>-lpCaXFwKFb&uE}sk?rLvWm;%I-p z&M|2PL#ZCvX9Gxw#fg!z1K@)%eI#_MVf49S-lS&6*o=f^)SL@l?=df?m35sulEfr= z8L4NU)5W|eV)=Zl_k#X&fIb!dGd*0MZxwY+HoL11h?sbl)F!zSM{nt=-^%ea)FP_l za$h=-u-Y81<}iUf2kqH~4w=W!qp{+1aO@{=_YGw1jLv9Y4gPu#dhCCWUd=m44wm)D za(P;KG`nR^dF<5rjG23e_*&ToqM<)Cgt~3N>C?l}E(`7fZH5hI2WDLO?R+@q;Bb9w zN66MO<+am$zF%vk89etq%GpvBEn4(m0Sn((TRl3uO_GuPI2F%>eYhI~ioMOoXtlOn z0)r|OYom1hY-ggf=ZLM{g{B7D%S%SSl_uj!+a3Q%#$v|(+Sb<59XAzp>r=B0-q&?L zG%L;*M?Dky!l;PK;mSfAG;g2z;rZMWB@W(`u#1+W=e7k$jhlTJ{ra4oXOb}Y{gvK; z05wKrrsykiS$}kyPOE$8%iFfCN<(3!>ksJ!x#U^`gD<)6Hr6XAScgf|n?Xlul^&jx z!C7Y@#!G>bxt4p+*GG#{G?~S9|+?hIuU>?ib&^DlZ>!$1~C3#Gfm!j>zzR zUs<{i_CcUOW}+3R;Cz@{j})n!JLnPM5xA6o2^8}NxM7lo2Ef|*Zkl0xGLwbrFa`D? z$b*nkFaeUba72U~yoh(W0MRiK0TR+E8ralfAs@CT!c6hiRmf}MHyca&7^hTa#%#Grrk)og zUX@=SQ0G=XAj6QITD=S=cd{*2JV-E$0GJa>irJ?E1nT4j!}bsR*AuiDh>@@wA%Q!V z8R>WHXhz;Q8>`pBM;UjgrbxQVejNS)JnOGORc+bQpdO^NYS@UGPsOVz&}qh^T<81N zAbE|zM9c!LuH1g;Vg?w^ER8Jd<0yZqAiY2i^hAl`QR~g|xC&;uDx^}lCH9c=u+=9$ zvXjTY_;pADSMuP|pdK5Ikhw=f(1vDUufpTd^D-KVG6+VM0iCbx$d`GDY$;YaI;fG_ zvw3s{G-C`^1SFp;*tL`u9+kDCloWFr@6lBtT?K3qy1XKS=2rNRyup|OHmB!+pYrUo zG_1IcNHyunonSQ~Ltr)E6wM+9B>vFXWAn&T@;HU*fd5Rv4wW(?!aEbV%%A5qo*C&+ zYG#o2)JI9CidKkkB*rO8tgOBLi<%k0%@ihpoQsGPiREiYG)coCYh7fd25?T@EHIeg zj@x~j!U>&I@u?6RMjFf@E1lFU&>|U%wp!VdOef|s`p6UKr=f>ap)%77Pu;A1J14@4 zSleV$HYzDkD7pL0VYlRCkc0E(x78JHMC0j@V7Fw_`{MBG3b+oOjvM zceP(RuU+wEvFcLa+$USMag9u+H0Zl5VZk|y?f@^CXL>ZMWzPiZOQmwPlj3VAsNpWg`RUm=q{hd7K|EF>Z#!Q72rOKW-B9l z5Z!Th4L|%i`TTpH1rB`jF?2Gw`7^)b#p~Ai0KT^j&aoi>O~dj?SsF7U+sOv#KJq&P zY&I*n<6z9Wuu|+Rn7#5%lz$kd!HUs7Xl@c!VVbOmk4(DWOj-6eYam=CGhok)X!LVc z@e{G{b!%2R_6EIJ^ph@Wnu7LFcW8%c*gZA*9^k(z@P3|!Dq7!Lb@XX4KYi@Wu^n|~ zkqmw=$^2)+rj-8NO?Jw5{ijmh86?UI0$ocCAzlq|j=X`5*Y@^Bd&4V&N!j8f9E=<4 z(P^$WQ8?lL-r2J)fOAPWxMw3yu0jJx?y|upr54Pnl6rxOYGDgs^;#U6l6v$MNyDf= z3|)&%Ol-BcMUUxL4M_6^P-pEuP(AJG+=%xO;xd$WI%O_)|9{5%{|(Mwn>nX_FpTwJ zI0?X4a0uro*<*t1uOP&K&{eSRU;_kR1}=#XHpi#MtIBTJ$dZ4V7W9qkjt^x<96IZ% zEq5SyB+DVRhp~rix*U@JvN*yU!YV%rU;cqX7O<^Q(R76E%~|fQL>5wA>xn>uBQIT1 zY;Rv<&ucZD(0d*&;dYYK%HCs1-^WYDg+rb-lE$5*;v8QXtCHe>_=v7bUM~EkWx9zy zPEOM+h%tF~+J__J`5K-p%)zaYbXE!JwU@onAyWBh+HDyuPbqr`PA-eP`O`+5o%_G& zt1=DD7;yvJ9bQukl$yjl2^9lTrjk*X$%%W(Pea#VP&{*wU_0IKnQHm{Kwfa7<7^2I z(V&8JB9HkHU2%Im7jruoLv;@ab7%d>Fiq+E0YxZw?4V1U=8ha)4mAhkaATuY^FpdM z=*f##&a?P*zvGK^dLt;&H{mHVd!%$Njj+9L;`K+?cFE#nvWppcBMvZ^~cwJJ?9ToTpQyN$<<|07U6* zAY!J@;|>T{nG_@_T7`llm%)bkT1lYg@PFSqumoIhjQzvN_qgK_^B;r>&? zpYgk263)So{V3t@Xx{(-vn#>>VErpy`@4nwnXvt((e0mXztXpVPWjIu^e+MH;Cct} zl>Z6af1C7Ub;Qs1xWd6dSNM_n?^=o9XZ~lk#3S#|Rjd48yuWHEl;vO_w+G;ABn$|6 LFdYfS Date: Wed, 31 Dec 2025 10:18:12 +0800 Subject: [PATCH 14/27] =?UTF-8?q?feat=EF=BC=9A=E8=BF=BD=E5=8A=A0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config_env.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/config_env.yaml b/config/config_env.yaml index 0467cbc..4348d2d 100644 --- a/config/config_env.yaml +++ b/config/config_env.yaml @@ -126,6 +126,8 @@ eino_tools: 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" From 5d120a6c05ba3567d7977e633f1b75674e258473 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 10:26:17 +0800 Subject: [PATCH 15/27] =?UTF-8?q?fix=EF=BC=9A=E5=8C=85=E5=90=8D=E6=94=B9?= =?UTF-8?q?=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/repo/repos.go | 6 +++--- .../domain/workflow/recharge/statistics_ours_product.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/domain/repo/repos.go b/internal/domain/repo/repos.go index a3250ba..0350d95 100644 --- a/internal/domain/repo/repos.go +++ b/internal/domain/repo/repos.go @@ -3,17 +3,17 @@ package repo import ( "ai_scheduler/internal/config" "ai_scheduler/internal/data/impl" - "ai_scheduler/internal/pkg/oss" + "ai_scheduler/internal/pkg/utils_oss" ) // Repos 聚合所有 Repository type Repos struct { Session SessionRepo - OssClient *oss.Client + OssClient *utils_oss.Client } func NewRepos(sessionImpl *impl.SessionImpl, cfg *config.Config) *Repos { - ossClient, _ := oss.NewClient(cfg.Oss) + ossClient, _ := utils_oss.NewClient(cfg) return &Repos{ Session: NewSessionAdapter(sessionImpl), OssClient: ossClient, diff --git a/internal/domain/workflow/recharge/statistics_ours_product.go b/internal/domain/workflow/recharge/statistics_ours_product.go index 91d5092..d8bd64a 100644 --- a/internal/domain/workflow/recharge/statistics_ours_product.go +++ b/internal/domain/workflow/recharge/statistics_ours_product.go @@ -7,7 +7,7 @@ import ( "ai_scheduler/internal/domain/tools/recharge/statistics_ours_product" "ai_scheduler/internal/domain/workflow/runtime" "ai_scheduler/internal/entitys" - "ai_scheduler/internal/pkg/oss" + "ai_scheduler/internal/pkg/utils_oss" "context" "errors" "fmt" @@ -28,7 +28,7 @@ func init() { type statisticsOursProduct struct { cfg *config.Config toolManager *toolManager.Manager - ossClient *oss.Client + ossClient *utils_oss.Client } type StatisticsOursProductWorkflowInput struct { From 86713dbb1ac4177f8ce3ea6cb47f8aae92322a32 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Wed, 31 Dec 2025 11:21:14 +0800 Subject: [PATCH 16/27] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8=E5=8A=9F=E8=83=BD=E4=B8=8E=E5=95=86=E5=93=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/ding_talk_bot.go | 74 +++++++++++++++++++++++-- internal/data/model/ai_bot_group.gen.go | 17 +++--- internal/data/model/ai_bot_tools.gen.go | 8 +-- internal/services/cron.go | 7 ++- tmpl/dataTemp/queryTempl.go | 6 ++ 5 files changed, 92 insertions(+), 20 deletions(-) diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index 631fbd0..e065ff2 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -17,6 +17,7 @@ import ( "net/http" "strconv" "time" + "unicode" "ai_scheduler/internal/config" "context" @@ -142,10 +143,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 { @@ -159,6 +164,59 @@ func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entit return d.handleMatch(ctx, rec) } +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) { group, err = d.botGroupImpl.GetByConversationIdAndRobotCode(conversationId, robotCode) if err != nil { @@ -478,13 +536,18 @@ func (d *DingTalkBotBiz) HandleStreamRes(ctx context.Context, data *chatbot.BotC return } -func (d *DingTalkBotBiz) GetReportLists(ctx context.Context) (reports []*bbxt.ReportRes, err error) { +func (d *DingTalkBotBiz) GetReportLists(ctx context.Context, group *model.AiBotGroup) (reports []*bbxt.ReportRes, err error) { reportList, err := bbxt.NewBbxtTools() if err != nil { return } - reports, err = reportList.DailyReport(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, d.ossClient) + 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 } @@ -582,7 +645,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. **返回格式**: @@ -605,5 +668,6 @@ func (d *DingTalkBotBiz) defaultPrompt() string { 4. 格式强制要求: -所有字段值必须是**字符串**(包括 confidence)。 --parameters 必须是 **转义后的 JSON 字符串**(如 "{\"product_name\": \"京东月卡\"}")。` +-parameters 必须是 **转义后的 JSON 字符串**(如 "{\"product_name\": \"京东月卡\"}")。 +当前时间:` + now } diff --git a/internal/data/model/ai_bot_group.gen.go b/internal/data/model/ai_bot_group.gen.go index 80c50d1..3dec7b5 100644 --- a/internal/data/model/ai_bot_group.gen.go +++ b/internal/data/model/ai_bot_group.gen.go @@ -12,14 +12,15 @@ const TableNameAiBotGroup = "ai_bot_group" // AiBotGroup mapped from table type AiBotGroup struct { - GroupID int32 `gorm:"column:group_id;primaryKey;autoIncrement:true" json:"group_id"` - ConversationID string `gorm:"column:conversation_id;not null;comment:会话ID" json:"conversation_id"` // 会话ID - RobotCode string `gorm:"column:robot_code;not null;comment:绑定机器人code" json:"robot_code"` // 绑定机器人code - Title string `gorm:"column:title;not null;comment:群名称" json:"title"` // 群名称 - ToolList string `gorm:"column:tool_list;not null;comment:开通工具列表" json:"tool_list"` // 开通工具列表 - Status int32 `gorm:"column:status;not null;default:1" json:"status"` - DeleteAt *time.Time `gorm:"column:delete_at" json:"delete_at"` - CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` + GroupID int32 `gorm:"column:group_id;primaryKey;autoIncrement:true" json:"group_id"` + ConversationID string `gorm:"column:conversation_id;not null;comment:会话ID" json:"conversation_id"` // 会话ID + RobotCode string `gorm:"column:robot_code;not null;comment:绑定机器人code" json:"robot_code"` // 绑定机器人code + Title string `gorm:"column:title;not null;comment:群名称" json:"title"` // 群名称 + ToolList string `gorm:"column:tool_list;not null;comment:开通工具列表" json:"tool_list"` // 开通工具列表 + ProductName string `gorm:"column:product_name;not null;comment:针对报表商品筛选快速实现,后期优化" json:"product_name"` // 针对报表商品筛选快速实现,后期优化 + Status int32 `gorm:"column:status;not null;default:1" json:"status"` + DeleteAt time.Time `gorm:"column:delete_at" json:"delete_at"` + CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` } // TableName AiBotGroup's table name diff --git a/internal/data/model/ai_bot_tools.gen.go b/internal/data/model/ai_bot_tools.gen.go index f57889b..0ea3302 100644 --- a/internal/data/model/ai_bot_tools.gen.go +++ b/internal/data/model/ai_bot_tools.gen.go @@ -13,16 +13,16 @@ const TableNameAiBotTool = "ai_bot_tools" // AiBotTool mapped from table type AiBotTool struct { ToolID int32 `gorm:"column:tool_id;primaryKey;autoIncrement:true" json:"tool_id"` - PermissionType int32 `gorm:"column:permission_type;not null;comment:类型,1为公共工具,不需要进行权限管理,反之则为2" json:"permission_type"` // 类型,1为公共工具,不需要进行权限管理,反之则为2 - Config string `gorm:"column:config;not null;comment:类型下所需路由以及参数" json:"config"` // 类型下所需路由以及参数 + PermissionType int32 `gorm:"column:permission_type;not null;default:1;comment:类型,1为公共工具,不需要进行权限管理,反之则为2" json:"permission_type"` // 类型,1为公共工具,不需要进行权限管理,反之则为2 + Config string `gorm:"column:config;comment:类型下所需路由以及参数" json:"config"` // 类型下所需路由以及参数 Type int32 `gorm:"column:type;not null;default:3" json:"type"` - Name string `gorm:"column:name;not null;default:1;comment:工具名称" json:"name"` // 工具名称 + Name string `gorm:"column:name;not null;comment:工具名称" json:"name"` // 工具名称 Index string `gorm:"column:index;not null;comment:索引" json:"index"` // 索引 Desc string `gorm:"column:desc;not null;comment:工具描述" json:"desc"` // 工具描述 TempPrompt string `gorm:"column:temp_prompt;not null;comment:提示词模板" json:"temp_prompt"` // 提示词模板 CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP" json:"updated_at"` - Status int32 `gorm:"column:status;not null" json:"status"` + Status int32 `gorm:"column:status;not null;default:1" json:"status"` DeleteAt time.Time `gorm:"column:delete_at" json:"delete_at"` } diff --git a/internal/services/cron.go b/internal/services/cron.go index 3f1eea8..cf6599f 100644 --- a/internal/services/cron.go +++ b/internal/services/cron.go @@ -21,12 +21,13 @@ func NewCronService(config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz) * } func (d *CronService) CronReportSend(ctx context.Context) error { - reports, err := d.dingTalkBotBiz.GetReportLists(ctx) + + groupId := 28 + groupInfo, err := d.dingTalkBotBiz.GetGroupInfo(ctx, groupId) if err != nil { return err } - groupId := 28 - groupInfo, err := d.dingTalkBotBiz.GetGroupInfo(ctx, groupId) + reports, err := d.dingTalkBotBiz.GetReportLists(ctx, &groupInfo) if err != nil { return err } diff --git a/tmpl/dataTemp/queryTempl.go b/tmpl/dataTemp/queryTempl.go index 7185bc2..2761318 100644 --- a/tmpl/dataTemp/queryTempl.go +++ b/tmpl/dataTemp/queryTempl.go @@ -189,3 +189,9 @@ func (k DataTemp) UpdateByCond(cond *builder.Cond, data interface{}) (err error) err = model.Where(query).Updates(data).Error return } + +func (k DataTemp) UpdateById(id int32, data interface{}) (err error) { + err = k.Db.Model(k.Model).Where("id = ?", id).Updates(data).Error + + return +} From e77da7875e82750789e91acb11027452cc04f417 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 12:02:12 +0800 Subject: [PATCH 17/27] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=94=B5?= =?UTF-8?q?=E5=95=86=E5=85=85=E5=80=BC=E7=B3=BB=E7=BB=9F=E6=88=91=E4=BB=AC?= =?UTF-8?q?=E7=9A=84=E5=95=86=E5=93=81=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/ding_talk_bot.go | 87 +++++++++++++------ internal/biz/do/handle.go | 2 +- internal/domain/workflow/hyt/goods_add.go | 3 +- .../domain/workflow/hyt/product_upload.go | 3 +- .../recharge/statistics_ours_product.go | 49 ++++++++--- internal/domain/workflow/runtime/registry.go | 9 +- .../zltx/bug_optimization_submit.bak.go | 4 +- .../workflow/zltx/bug_optimization_submit.go | 4 +- .../zltx/order_after_reseller_batch.go | 2 +- internal/server/http.go | 4 +- internal/server/router/router.go | 8 +- internal/services/capability.go | 2 +- internal/services/dtalk_bot_test.go | 5 +- 13 files changed, 127 insertions(+), 55 deletions(-) diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index e065ff2..405d027 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -7,6 +7,8 @@ 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" @@ -35,19 +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 - ossClient *utils_oss.Client + 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 @@ -63,20 +66,22 @@ func NewDingTalkBotBiz( 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, - ossClient: ossClient, + 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, } } @@ -552,6 +557,38 @@ func (d *DingTalkBotBiz) GetReportLists(ctx context.Context, group *model.AiBotG return } + // 追加电商充值系统统计 - 返回统一使用 []*bbxt.ReportRes + rechargeReports, err := d.rechargeDailyReport(ctx, time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, d.ossClient) + 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) { + 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 + } + + 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 } diff --git a/internal/biz/do/handle.go b/internal/biz/do/handle.go index a1a9dcf..afc4b2b 100644 --- a/internal/biz/do/handle.go +++ b/internal/biz/do/handle.go @@ -396,7 +396,7 @@ func (r *Handle) handleEinoWorkflow(ctx context.Context, rec *entitys.Recognize, // 工作流内部输出 workflowId := task.Index - _, err = r.workflowManager.Invoke(ctx, workflowId, rec) + _, err = r.workflowManager.Invoke(ctx, workflowId, &runtime.WorkflowArgs{Recognize: rec}) if err != nil { return err } diff --git a/internal/domain/workflow/hyt/goods_add.go b/internal/domain/workflow/hyt/goods_add.go index 91db063..c41b66b 100644 --- a/internal/domain/workflow/hyt/goods_add.go +++ b/internal/domain/workflow/hyt/goods_add.go @@ -8,7 +8,6 @@ import ( "ai_scheduler/internal/domain/tools/hyt/goods_category_add" "ai_scheduler/internal/domain/tools/hyt/goods_media_add" "ai_scheduler/internal/domain/workflow/runtime" - "ai_scheduler/internal/entitys" "context" "encoding/json" "errors" @@ -42,7 +41,7 @@ type GoodsAddWorkflowInput struct { func (o *goodsAdd) ID() string { return WorkflowIDGoodsAdd } -func (o *goodsAdd) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) { +func (o *goodsAdd) Invoke(ctx context.Context, rec *runtime.WorkflowArgs) (map[string]any, error) { // 构建工作流 runnable, err := o.buildWorkflow(ctx) if err != nil { diff --git a/internal/domain/workflow/hyt/product_upload.go b/internal/domain/workflow/hyt/product_upload.go index 35114ed..b2af877 100644 --- a/internal/domain/workflow/hyt/product_upload.go +++ b/internal/domain/workflow/hyt/product_upload.go @@ -6,7 +6,6 @@ import ( toolManager "ai_scheduler/internal/domain/tools" toolPu "ai_scheduler/internal/domain/tools/hyt/product_upload" "ai_scheduler/internal/domain/workflow/runtime" - "ai_scheduler/internal/entitys" "context" "encoding/json" "errors" @@ -39,7 +38,7 @@ type ProductUploadWorkflowInput struct { func (o *productUpload) ID() string { return WorkflowIDProductUpload } -func (o *productUpload) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) { +func (o *productUpload) Invoke(ctx context.Context, rec *runtime.WorkflowArgs) (map[string]any, error) { // 构建工作流 runnable, err := o.buildWorkflow(ctx) if err != nil { diff --git a/internal/domain/workflow/recharge/statistics_ours_product.go b/internal/domain/workflow/recharge/statistics_ours_product.go index d8bd64a..cb89347 100644 --- a/internal/domain/workflow/recharge/statistics_ours_product.go +++ b/internal/domain/workflow/recharge/statistics_ours_product.go @@ -6,7 +6,6 @@ import ( toolManager "ai_scheduler/internal/domain/tools" "ai_scheduler/internal/domain/tools/recharge/statistics_ours_product" "ai_scheduler/internal/domain/workflow/runtime" - "ai_scheduler/internal/entitys" "ai_scheduler/internal/pkg/utils_oss" "context" "errors" @@ -37,28 +36,31 @@ type StatisticsOursProductWorkflowInput struct { } type StatisticsOursProductWorkflowOutput struct { - ImgUrl string `json:"img_url"` + 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, rec *entitys.Recognize) (map[string]any, error) { +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 } - // 解析参数 (假设参数在 rec.Match.Parameters 中,或者根据实际情况解析) - // 这里简化处理,假设需要解析参数 - // 实际上这里应该根据 LLM 解析的结果来填充 Input - // 暂时假设 ParameterResult 是 JSON 字符串 + // 获取参数时间 + now := args.Args["now"].(time.Time) input := &StatisticsOursProductWorkflowInput{ // 默认值,具体应从 rec 解析 - StartTime: time.Now().Format("2006010200"), - EndTime: time.Now().Format("2006010223"), + StartTime: now.Format("2006010200"), + EndTime: now.Format("2006010215"), } + input.StartTime = "2025122300" + // 工作流过程调用 output, err := runnable.Invoke(ctx, input) if err != nil { @@ -70,11 +72,11 @@ func (w *statisticsOursProduct) Invoke(ctx context.Context, rec *entitys.Recogni return nil, errorcode.WorkflowErr(errStr) } - return map[string]any{"img_url": output.ImgUrl}, nil + return output, nil } -func (w *statisticsOursProduct) buildWorkflow(ctx context.Context) (compose.Runnable[*StatisticsOursProductWorkflowInput, *StatisticsOursProductWorkflowOutput], error) { - c := compose.NewChain[*StatisticsOursProductWorkflowInput, *StatisticsOursProductWorkflowOutput]() +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.callStatisticsTool)) @@ -82,6 +84,9 @@ func (w *statisticsOursProduct) buildWorkflow(ctx context.Context) (compose.Runn // 2. 生成 Excel 并转图片上传 c.AppendLambda(compose.InvokableLambda(w.generateExcelAndUpload)) + // 3. 转map输出 + c.AppendLambda(compose.InvokableLambda(w.convertToMap)) + return c.Compile(ctx) } @@ -98,7 +103,7 @@ func (w *statisticsOursProduct) callStatisticsTool(ctx context.Context, input *S func (w *statisticsOursProduct) generateExcelAndUpload(ctx context.Context, data []statistics_ours_product.StatisticsOursProductItem) (*StatisticsOursProductWorkflowOutput, error) { // 2. 获取模板路径 (假设在项目根目录的 assets/templates 下) cwd, _ := filepath.Abs(".") - templatePath := filepath.Join(cwd, "assets", "templates", "statistics_ours_product.xlsx") + templatePath := filepath.Join(cwd, "tmpl", "excel_temp", "recharge_statistics_ours_product.xlsx") fileName := fmt.Sprintf("statistics_ours_product_%d", time.Now().Unix()) // 3. 转换数据为 [][]string @@ -122,7 +127,14 @@ func (w *statisticsOursProduct) generateExcelAndUpload(ctx context.Context, data return nil, fmt.Errorf("上传 OSS 失败: %v", err) } - return &StatisticsOursProductWorkflowOutput{ImgUrl: url}, nil + res := &StatisticsOursProductWorkflowOutput{ + Path: "", + Url: url, + Data: excelData, + Desc: "", + } + + return res, nil } // convertDataToExcelFormat 将业务数据转换为 Excel 生成器需要的二维字符串数组 @@ -144,3 +156,12 @@ func (w *statisticsOursProduct) convertDataToExcelFormat(data []statistics_ours_ } return result } + +func (w *statisticsOursProduct) convertToMap(ctx context.Context, output *StatisticsOursProductWorkflowOutput) (map[string]any, error) { + return map[string]any{ + "path": output.Path, + "url": output.Url, + "data": output.Data, + "desc": output.Desc, + }, nil +} diff --git a/internal/domain/workflow/runtime/registry.go b/internal/domain/workflow/runtime/registry.go index f804e1d..1d391f2 100644 --- a/internal/domain/workflow/runtime/registry.go +++ b/internal/domain/workflow/runtime/registry.go @@ -15,7 +15,7 @@ import ( type Workflow interface { ID() string // Schema() map[string]any - Invoke(ctx context.Context, requireData *entitys.Recognize) (map[string]any, error) + Invoke(ctx context.Context, requireData *WorkflowArgs) (map[string]any, error) } type Deps struct { @@ -28,6 +28,11 @@ type Deps struct { type Factory func(deps *Deps) (Workflow, error) +type WorkflowArgs struct { + *entitys.Recognize + Args map[string]any +} + var ( regMu sync.RWMutex factories = map[string]Factory{} @@ -69,7 +74,7 @@ func Default() *Registry { return r } -func (r *Registry) Invoke(ctx context.Context, id string, rec *entitys.Recognize) (map[string]any, error) { +func (r *Registry) Invoke(ctx context.Context, id string, rec *WorkflowArgs) (map[string]any, error) { regMu.RLock() f, ok := factories[id] regMu.RUnlock() diff --git a/internal/domain/workflow/zltx/bug_optimization_submit.bak.go b/internal/domain/workflow/zltx/bug_optimization_submit.bak.go index 6ed6bb4..0ab94ee 100644 --- a/internal/domain/workflow/zltx/bug_optimization_submit.bak.go +++ b/internal/domain/workflow/zltx/bug_optimization_submit.bak.go @@ -42,7 +42,7 @@ func (w *bugOptimizationSubmitBak) ID() string { type BugOptimizationSubmitBakInput struct { Ch chan entitys.Response - RequireData *entitys.Recognize + RequireData *runtime.WorkflowArgs } type BugOptimizationSubmitBakOutput struct { @@ -54,7 +54,7 @@ type contextWithTaskBak struct { TaskID string } -func (w *bugOptimizationSubmitBak) Invoke(ctx context.Context, recognize *entitys.Recognize) (map[string]any, error) { +func (w *bugOptimizationSubmitBak) Invoke(ctx context.Context, recognize *runtime.WorkflowArgs) (map[string]any, error) { chain, err := w.buildWorkflow(ctx) if err != nil { return nil, err diff --git a/internal/domain/workflow/zltx/bug_optimization_submit.go b/internal/domain/workflow/zltx/bug_optimization_submit.go index 30ad0bc..ccf6812 100644 --- a/internal/domain/workflow/zltx/bug_optimization_submit.go +++ b/internal/domain/workflow/zltx/bug_optimization_submit.go @@ -42,7 +42,7 @@ func (w *bugOptimizationSubmit) ID() string { type BugOptimizationSubmitInput struct { Ch chan entitys.Response - RequireData *entitys.Recognize + RequireData *runtime.WorkflowArgs } type BugOptimizationSubmitOutput struct { @@ -54,7 +54,7 @@ type contextWithTask struct { TaskID string } -func (w *bugOptimizationSubmit) Invoke(ctx context.Context, recognize *entitys.Recognize) (map[string]any, error) { +func (w *bugOptimizationSubmit) Invoke(ctx context.Context, recognize *runtime.WorkflowArgs) (map[string]any, error) { chain, err := w.buildWorkflow(ctx) if err != nil { return nil, err diff --git a/internal/domain/workflow/zltx/order_after_reseller_batch.go b/internal/domain/workflow/zltx/order_after_reseller_batch.go index eee022a..620d636 100644 --- a/internal/domain/workflow/zltx/order_after_reseller_batch.go +++ b/internal/domain/workflow/zltx/order_after_reseller_batch.go @@ -78,7 +78,7 @@ type OrderAfterSaleResellerBatchData struct { func (o *orderAfterSaleResellerBatch) ID() string { return "zltx.orderAfterSaleResellerBatch" } // Invoke 调用原有编排工作流并规范化输出 -func (o *orderAfterSaleResellerBatch) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) { +func (o *orderAfterSaleResellerBatch) Invoke(ctx context.Context, rec *runtime.WorkflowArgs) (map[string]any, error) { // 构建工作流 chain, err := o.buildWorkflow(ctx) if err != nil { diff --git a/internal/server/http.go b/internal/server/http.go index 53446c8..3d0b180 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -18,6 +18,7 @@ type HTTPServer struct { callback *services.CallbackService chatHis *services.HistoryService capabilityService *services.CapabilityService + cronService *services.CronService } func NewHTTPServer( @@ -28,10 +29,11 @@ func NewHTTPServer( callback *services.CallbackService, chatHis *services.HistoryService, capabilityService *services.CapabilityService, + cronService *services.CronService, ) *fiber.App { //构建 server app := initRoute() - router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService) + router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService, cronService) return app } diff --git a/internal/server/router/router.go b/internal/server/router/router.go index 091c85f..3c33b42 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -21,12 +21,13 @@ type RouterServer struct { gateway *gateway.Gateway chatHist *services.HistoryService capabilityService *services.CapabilityService + cronService *services.CronService } // SetupRoutes 设置路由 func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionService *services.SessionService, task *services.TaskService, gateway *gateway.Gateway, callbackService *services.CallbackService, chatHist *services.HistoryService, - capabilityService *services.CapabilityService, + capabilityService *services.CapabilityService, cronService *services.CronService, ) { app.Use(func(c *fiber.Ctx) error { // 设置 CORS 头 @@ -94,6 +95,11 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi // 能力 r.Post("/capability/product/ingest", capabilityService.ProductIngest) // 商品数据提取 r.Post("/capability/product/ingest/:thread_id/confirm", capabilityService.ProductIngestConfirm) // 商品数据提取确认 + + // 测试任务 + r.Post("/test/cron", func(c *fiber.Ctx) error { + return cronService.CronReportSend(c.Context()) + }) } func routerSocket(app *fiber.App, chatService *services.ChatService) { diff --git a/internal/services/capability.go b/internal/services/capability.go index 759433c..d2c1e9b 100644 --- a/internal/services/capability.go +++ b/internal/services/capability.go @@ -198,7 +198,7 @@ func (s *CapabilityService) ProductIngestConfirm(c *fiber.Ctx) error { // 调用eino工作流,实现商品上传到目标系统 rec := &entitys.Recognize{UserContent: &entitys.RecognizeUserContent{Text: req.Confirmed}} - res, err := s.workflowManager.Invoke(ctx, workflowId, rec) + res, err := s.workflowManager.Invoke(ctx, workflowId, &runtime.WorkflowArgs{Recognize: rec}) if err != nil { return err } diff --git a/internal/services/dtalk_bot_test.go b/internal/services/dtalk_bot_test.go index a9a75e0..bd0ceef 100644 --- a/internal/services/dtalk_bot_test.go +++ b/internal/services/dtalk_bot_test.go @@ -12,6 +12,7 @@ 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" @@ -102,7 +103,9 @@ func run() { handle := do.NewHandle(ollamaService, manager, configConfig, sessionImpl, registry, oldClient, contactClient, notableClient) // 初始化钉钉机器人业务逻辑 utils_ossClient, _ := utils_oss.NewClient(configConfig) - dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, toolRegis, botChatHisImpl, manager, configConfig, sendCardClient, utils_ossClient) + // 初始化工作流管理器 + 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) From 36d9d3f363c4c490a511ed1af124364973d255f3 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 14:15:43 +0800 Subject: [PATCH 18/27] =?UTF-8?q?fix:=201.=E9=99=8D=E4=BD=8E=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=94=9F=E6=88=90=E5=80=8D=E7=8E=87=202.=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tools/common/image_converter/client.go | 10 +++++++++- .../recharge/statistics_ours_product.go | 15 ++++++++------- internal/server/http.go | 4 +--- internal/server/router/router.go | 8 +------- internal/tools/bbxt/upload.go | 12 ++++++++++-- tmpl/excel_temp/kshj_gt.xlsx | Bin 9942 -> 9953 bytes tmpl/excel_temp/kshj_total.xlsx | Bin 9414 -> 9421 bytes .../recharge_statistics_ours_product.xlsx | Bin 9787 -> 9901 bytes 8 files changed, 29 insertions(+), 20 deletions(-) diff --git a/internal/domain/tools/common/image_converter/client.go b/internal/domain/tools/common/image_converter/client.go index 5eba199..2de57d2 100644 --- a/internal/domain/tools/common/image_converter/client.go +++ b/internal/domain/tools/common/image_converter/client.go @@ -21,7 +21,7 @@ func New(cfg config.ToolConfig) *Client { } // Call 将 Excel 文件转换为图片 -func (c *Client) Call(filename string, fileBytes []byte) ([]byte, error) { +func (c *Client) Call(filename string, fileBytes []byte, scale int) ([]byte, error) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -33,6 +33,14 @@ func (c *Client) Call(filename string, fileBytes []byte) ([]byte, error) { 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 } diff --git a/internal/domain/workflow/recharge/statistics_ours_product.go b/internal/domain/workflow/recharge/statistics_ours_product.go index cb89347..ebc33a9 100644 --- a/internal/domain/workflow/recharge/statistics_ours_product.go +++ b/internal/domain/workflow/recharge/statistics_ours_product.go @@ -110,13 +110,13 @@ func (w *statisticsOursProduct) generateExcelAndUpload(ctx context.Context, data excelData := w.convertDataToExcelFormat(data) // 4. 生成 Excel - excelBytes, err := w.toolManager.Common.ExcelGenerator.Call(templatePath, excelData, 2, 2) + excelBytes, err := w.toolManager.Common.ExcelGenerator.Call(templatePath, excelData, 4, 3) if err != nil { return nil, fmt.Errorf("生成 Excel 失败: %v", err) } // 5. Excel 转图片 - picBytes, err := w.toolManager.Common.ImageConverter.Call(fileName+".xlsx", excelBytes) + picBytes, err := w.toolManager.Common.ImageConverter.Call(fileName+".xlsx", excelBytes, 2) if err != nil { return nil, fmt.Errorf("Excel 转图片失败: %v", err) } @@ -143,15 +143,16 @@ func (w *statisticsOursProduct) convertDataToExcelFormat(data []statistics_ours_ for _, item := range data { row := []string{ item.OursProductName, - fmt.Sprintf("%d", item.OursProductId), + // fmt.Sprintf("%d", item.OursProductId), item.Count, - item.TotalPrice, - item.SuccessCount, + // item.TotalPrice, + // item.SuccessCount, item.SuccessPrice, - item.FailCount, - item.FailPrice, + // item.FailCount, + // item.FailPrice, item.Profit, } + result = append(result, row) } return result diff --git a/internal/server/http.go b/internal/server/http.go index 3d0b180..53446c8 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -18,7 +18,6 @@ type HTTPServer struct { callback *services.CallbackService chatHis *services.HistoryService capabilityService *services.CapabilityService - cronService *services.CronService } func NewHTTPServer( @@ -29,11 +28,10 @@ func NewHTTPServer( callback *services.CallbackService, chatHis *services.HistoryService, capabilityService *services.CapabilityService, - cronService *services.CronService, ) *fiber.App { //构建 server app := initRoute() - router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService, cronService) + router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService) return app } diff --git a/internal/server/router/router.go b/internal/server/router/router.go index 3c33b42..091c85f 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -21,13 +21,12 @@ type RouterServer struct { gateway *gateway.Gateway chatHist *services.HistoryService capabilityService *services.CapabilityService - cronService *services.CronService } // SetupRoutes 设置路由 func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionService *services.SessionService, task *services.TaskService, gateway *gateway.Gateway, callbackService *services.CallbackService, chatHist *services.HistoryService, - capabilityService *services.CapabilityService, cronService *services.CronService, + capabilityService *services.CapabilityService, ) { app.Use(func(c *fiber.Ctx) error { // 设置 CORS 头 @@ -95,11 +94,6 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi // 能力 r.Post("/capability/product/ingest", capabilityService.ProductIngest) // 商品数据提取 r.Post("/capability/product/ingest/:thread_id/confirm", capabilityService.ProductIngestConfirm) // 商品数据提取确认 - - // 测试任务 - r.Post("/test/cron", func(c *fiber.Ctx) error { - return cronService.CronReportSend(c.Context()) - }) } func routerSocket(app *fiber.App, chatService *services.ChatService) { diff --git a/internal/tools/bbxt/upload.go b/internal/tools/bbxt/upload.go index a5d0886..c5b6c80 100644 --- a/internal/tools/bbxt/upload.go +++ b/internal/tools/bbxt/upload.go @@ -42,7 +42,7 @@ func (u *Uploader) Run(report *ReportRes) (err error) { return fmt.Errorf("write to bytes failed: %v", err) } - picBytes, err := u.excel2picPy(report.Path, excelBytes.Bytes()) + picBytes, err := u.excel2picPy(report.Path, excelBytes.Bytes(), 2) if err != nil { return fmt.Errorf("excel2picPy failed: %v", err) } @@ -62,7 +62,7 @@ func (u *Uploader) Run(report *ReportRes) (err error) { // --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) ([]byte, error) { +func (u *Uploader) excel2picPy(templatePath string, excelBytes []byte, scale int) ([]byte, error) { // 1. 获取 Sheet Name // 尝试从 excelBytes 解析,如果失败则使用默认值 "Sheet1" sheetName := "Sheet1" @@ -99,6 +99,14 @@ func (u *Uploader) excel2picPy(templatePath string, excelBytes []byte) ([]byte, 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) } diff --git a/tmpl/excel_temp/kshj_gt.xlsx b/tmpl/excel_temp/kshj_gt.xlsx index 2b27bfa07c042ba158bc3229982c9138797ccee1..957d183be77472f2dee09f9b090d878285dbc25d 100755 GIT binary patch delta 4551 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 F0046xBuoGR delta 3696 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 PvSGk{v?M9q|G@tPMQ29H delta 4844 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 XiGM3s<6}qgnXvD;sR*H*f9d}L{4^I$ From d32df33478db0175b1b7d8abcf23a66b925a647c Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 14:31:58 +0800 Subject: [PATCH 19/27] =?UTF-8?q?fix=EF=BC=9A=E8=B0=83=E6=95=B4=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tmpl/excel_temp/kshj_gt.xlsx | Bin 9953 -> 9955 bytes tmpl/excel_temp/kshj_total.xlsx | Bin 9421 -> 9420 bytes .../recharge_statistics_ours_product.xlsx | Bin 9901 -> 9899 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tmpl/excel_temp/kshj_gt.xlsx b/tmpl/excel_temp/kshj_gt.xlsx index 957d183be77472f2dee09f9b090d878285dbc25d..5aeeab5426eec2cc207e4cbeaa18909223284550 100755 GIT binary patch delta 4566 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 delta 4533 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 TFMtcO!;_pQK?cMk00000l4qm&p5 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 delta 3615 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 diff --git a/tmpl/excel_temp/recharge_statistics_ours_product.xlsx b/tmpl/excel_temp/recharge_statistics_ours_product.xlsx index 347e88684725c83f1e414ce5681787d3a4636766..8294b55c1ec333246ac2e62f22330931b0516048 100755 GIT binary patch delta 4584 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 delta 4607 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 c0RZ-YOn>9QzD82a1SuybLy& Date: Wed, 31 Dec 2025 15:42:26 +0800 Subject: [PATCH 20/27] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8=E7=B3=BB=E7=BB=9F=E5=8A=9F=E8=83=BD=E4=B8=8E=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/ding_talk_bot.go | 79 ++++++++++++++++++++++++++++++-- internal/data/constants/const.go | 1 + internal/entitys/types.go | 4 ++ internal/services/cron.go | 2 +- internal/tools/bbxt/bbxt.go | 11 +++-- internal/tools/bbxt/upload.go | 11 +++++ 6 files changed, 98 insertions(+), 10 deletions(-) diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index e065ff2..af040c2 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -161,7 +161,7 @@ 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) { @@ -350,7 +350,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 { @@ -375,6 +375,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: @@ -483,6 +485,75 @@ func handleCozeWorkflowEvents(ctx context.Context, resp coze.Stream[coze.Workflo fmt.Printf("done, log:%s\n", resp.Response().LogID()) } +func (d *DingTalkBotBiz) handleReport(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool, group *model.AiBotGroup) error { + var configData entitys.ConfigDataReport + err := json.Unmarshal([]byte(rec.Match.Parameters), &configData) + if err != nil { + return err + } + t, err := time.Parse(time.DateTime, configData.Time) + if err != nil { + log.Infof("时间识别失败:%s", configData.Time) + entitys.ResText(rec.Ch, "", "时间识别失败了!可以给我一份比较具体的时间吗,例如“2025-12-31 12:00,抱歉抱歉😀") + return nil + } + rep, err := bbxt.NewBbxtTools() + uploader := bbxt.NewUploader(d.ossClient) + if err != nil { + return err + } + var reports []*bbxt.ReportRes + switch rec.Match.Index { + case "report_loss_analysis": + repo, _err := rep.StatisOursProductLossSum(t) + if _err != nil { + return _err + } + reports = append(reports, repo...) + case "report_sales_analysis": + + repo, _err := rep.GetProfitRankingSum(t) + if _err != nil { + return _err + } + reports = append(reports, repo) + case "report_ranking_of_distributors": + product := strings.Split(group.ProductName, ",") + repo, _err := rep.GetStatisOfficialProductSum(t, product) + if _err != nil { + return _err + } + reports = append(reports, repo) + case "report_daily": + product := strings.Split(group.ProductName, ",") + repo, _err := rep.DailyReport(t, product, nil) + if _err != nil { + return _err + } + reports = append(reports, repo...) + default: + return fmt.Errorf("未找到的报表:%s", rec.Match.Index) + } + + for _, report := range reports { + err = uploader.Run(report) + if err != nil { + log.Error(err) + continue + } + + entitys.ResText(rec.Ch, "", fmt.Sprintf("%s![图片](%s)", report.Title, report.Url)) + //rec.Ch <- report.Title + //reportChan <- fmt.Sprintf("![图片](%s)", report.Url) + //err = d.SendReport(ctx, group, report) + //if err != nil { + // log.Error(err) + // continue + //} + } + return nil +} + func (d *DingTalkBotBiz) handleTask(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool) (err error) { var configData entitys.ConfigDataTool err = json.Unmarshal([]byte(task.Config), &configData) @@ -555,7 +626,7 @@ func (d *DingTalkBotBiz) GetReportLists(ctx context.Context, group *model.AiBotG return } -func (d *DingTalkBotBiz) SendReport(ctx context.Context, groupInfo model.AiBotGroup, report *bbxt.ReportRes) (err error) { +func (d *DingTalkBotBiz) SendReport(ctx context.Context, groupInfo *model.AiBotGroup, report *bbxt.ReportRes) (err error) { reportChan := make(chan string, 10) defer close(reportChan) @@ -669,5 +740,5 @@ func (d *DingTalkBotBiz) defaultPrompt() string { 4. 格式强制要求: -所有字段值必须是**字符串**(包括 confidence)。 -parameters 必须是 **转义后的 JSON 字符串**(如 "{\"product_name\": \"京东月卡\"}")。 -当前时间:` + now +当前时间:` + now + `,所有的时间识别精确到秒` } diff --git a/internal/data/constants/const.go b/internal/data/constants/const.go index b3c6ef0..f5ac22a 100644 --- a/internal/data/constants/const.go +++ b/internal/data/constants/const.go @@ -17,6 +17,7 @@ const ( TaskTypeBot TaskType = 4 TaskTypeEinoWorkflow TaskType = 5 // eino 工作流 TaskTypeCozeWorkflow TaskType = 6 // coze 工作流 + TaskTypeReport TaskType = 7 //报表 ) type UseFul int32 diff --git a/internal/entitys/types.go b/internal/entitys/types.go index 601f50e..815eeba 100644 --- a/internal/entitys/types.go +++ b/internal/entitys/types.go @@ -92,6 +92,10 @@ type ConfigDataTool struct { Tool string `json:"tool"` } +type ConfigDataReport struct { + Time string `json:"time"` +} + // Message 消息 type Message struct { Role string `json:"role"` diff --git a/internal/services/cron.go b/internal/services/cron.go index cf6599f..4358781 100644 --- a/internal/services/cron.go +++ b/internal/services/cron.go @@ -34,7 +34,7 @@ func (d *CronService) CronReportSend(ctx context.Context) error { //contentChan <- "截止今日23点利润亏损合计:127917.0866元,亏损500元以上的分销商和产品金额如下图:" //contentChan <- "![图片](https://lsxdmgoss.oss-cn-chengdu.aliyuncs.com/MarketingSaaS/image/V2/other/shanghu.png)" for _, report := range reports { - err = d.dingTalkBotBiz.SendReport(ctx, groupInfo, report) + err = d.dingTalkBotBiz.SendReport(ctx, &groupInfo, report) if err != nil { log.Error(err) continue diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index 3a1619d..4a0893b 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -54,8 +54,9 @@ func (b *BbxtTools) DailyReport(now time.Time, productName []string, ossClient * } reports = append(reports, productLossReport...) reports = append(reports, statisOfficialProductSum, profitRankingSum) - uploader := NewUploader(ossClient) + if ossClient != nil { + uploader := NewUploader(ossClient) for _, report := range reports { _ = uploader.Run(report) } @@ -68,7 +69,7 @@ func (b *BbxtTools) DailyReport(now time.Time, productName []string, ossClient * 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(time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())), + adjustedTime(now), //adjustedTime(time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())), } data, err := StatisOursProductLossSumApi(&StatisOursProductLossSumReq{ @@ -144,14 +145,14 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes 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%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx" err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total, "") report[0] = &ReportRes{ ReportName: "负利润分析(合计表)", - Title: "截至今日23点利润累计亏损" + fmt.Sprintf("%.2f", totalSum), + Title: "截至今日" + timeCh + "利润累计亏损" + fmt.Sprintf("%.2f", totalSum), Path: filePath, Data: total, } @@ -164,7 +165,7 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) report[1] = &ReportRes{ ReportName: "负利润分析(亏损500以上)", - Title: "截至今日23点亏顺500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500), + Title: "截至今日" + timeCh + "亏顺500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500), Path: filePath, Data: total, } diff --git a/internal/tools/bbxt/upload.go b/internal/tools/bbxt/upload.go index a5d0886..c485c49 100644 --- a/internal/tools/bbxt/upload.go +++ b/internal/tools/bbxt/upload.go @@ -151,3 +151,14 @@ func (u *Uploader) uploadToOSS(fileName string, fileBytes []byte) string { } 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 +//} From ad3c5bc4b11c6c440c9a5e6886ba1ca45249c02e Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 15:44:13 +0800 Subject: [PATCH 21/27] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96excel=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=94=9F=E6=88=90=E6=96=B9=E6=B3=95=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=8A=A8=E6=80=81title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config_test.yaml | 7 ++ .../tools/common/excel_generator/client.go | 29 ++--- .../tools/common/excel_generator/type.go | 9 ++ .../recharge/statistics_ours_product.go | 118 ++++++++++++------ 4 files changed, 109 insertions(+), 54 deletions(-) create mode 100644 internal/domain/tools/common/excel_generator/type.go diff --git a/config/config_test.yaml b/config/config_test.yaml index bdbc26d..35a2888 100644 --- a/config/config_test.yaml +++ b/config/config_test.yaml @@ -124,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.1688sup.cn:8001/admin/statistics/oursProduct" + api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY3MDc3NzcwLCJuYmYiOjE3NjcwNzU5NzAsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.Nuw_aR6iSPmhhh9E5rhyTxHBsgWtaTZvbnc7SFTnUBJXTQvYahnk0LyZaVpsVw6FB3cU0F5xOdX3rmGyWyaiszWO6yi-o1oxGMXwhf39fMiWT2xUI6pAn9Ync8DmZ4tOMCNUTdEk4CaQFzrTwJs0c-VR4yW6LgoPmNPvUVZ-KwmusUpnPz5j9RsJItzIWE3bpGGsfB54e2UERcZdbo9BXxCZIBbpAYKBKdl73KuI8SNaXgKvGTrJ6hEN4ESpnbisJVwT5pp_kuChJlcfjHTHFsEf4RJDjN9gTrtDbBWZyY3OmO2ukqYAM7tZPs6TXJwvQGJQsFRVZUBGxS1nD_6DzQ" + excel2pic: + base_url: "http://192.168.6.109:8010/api/v1/convert" dingtalk: api_key: "dingsbbntrkeiyazcfdg" diff --git a/internal/domain/tools/common/excel_generator/client.go b/internal/domain/tools/common/excel_generator/client.go index eae9451..f2cd74f 100644 --- a/internal/domain/tools/common/excel_generator/client.go +++ b/internal/domain/tools/common/excel_generator/client.go @@ -15,19 +15,15 @@ func New() *Client { } // Call 根据模板和数据生成 Excel 字节流 -// templatePath: 模板文件路径 -// data: 二维字符串数组,不再使用反射 -// startRow: 数据填充起始行 (默认 2) -// styleRow: 样式参考行 (默认 2) -func (g *Client) Call(templatePath string, data [][]string, startRow int, styleRow int) ([]byte, error) { - if startRow <= 0 { - startRow = 2 +func (g *Client) Call(req *ExcelGeneratorRequest) ([]byte, error) { + if req.StartRow <= 0 { + req.StartRow = 2 } - if styleRow <= 0 { - styleRow = 2 + if req.StyleRow <= 0 { + req.StyleRow = 2 } - f, err := excelize.OpenFile(templatePath) + f, err := excelize.OpenFile(req.TemplatePath) if err != nil { return nil, err } @@ -35,20 +31,25 @@ func (g *Client) Call(templatePath string, data [][]string, startRow int, styleR sheet := f.GetSheetName(0) + // 若提供标题,替换第一行表格标题 + if len(req.Title) > 0 { + f.SetCellValue(sheet, "A1", req.Title) + } + // 获取样式和行高 - styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", styleRow)) + 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, styleRow) + rowHeight, err := f.GetRowHeight(sheet, req.StyleRow) if err != nil { log.Errorf("获取行高失败: %v", err) rowHeight = 31 // 默认高度 } - row := startRow - for i, item := range data { + row := req.StartRow + for i, item := range req.ExcelData { currentRow := row + i // 设置行高 diff --git a/internal/domain/tools/common/excel_generator/type.go b/internal/domain/tools/common/excel_generator/type.go new file mode 100644 index 0000000..3a1b7b4 --- /dev/null +++ b/internal/domain/tools/common/excel_generator/type.go @@ -0,0 +1,9 @@ +package excel_generator + +type ExcelGeneratorRequest struct { + TemplatePath string // 模板文件路径 + ExcelData [][]string // 二维字符串数组 + StartRow int // 数据填充起始行 (默认 2) + StyleRow int // 样式参考行 (默认 2) + Title string // 表格标题(仅替换) +} diff --git a/internal/domain/workflow/recharge/statistics_ours_product.go b/internal/domain/workflow/recharge/statistics_ours_product.go index ebc33a9..8c78209 100644 --- a/internal/domain/workflow/recharge/statistics_ours_product.go +++ b/internal/domain/workflow/recharge/statistics_ours_product.go @@ -4,16 +4,19 @@ 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" @@ -31,8 +34,7 @@ type statisticsOursProduct struct { } type StatisticsOursProductWorkflowInput struct { - StartTime string `json:"start_time"` - EndTime string `json:"end_time"` + Time time.Time `json:"time"` } type StatisticsOursProductWorkflowOutput struct { @@ -52,15 +54,10 @@ func (w *statisticsOursProduct) Invoke(ctx context.Context, args *runtime.Workfl } // 获取参数时间 - now := args.Args["now"].(time.Time) input := &StatisticsOursProductWorkflowInput{ - // 默认值,具体应从 rec 解析 - StartTime: now.Format("2006010200"), - EndTime: now.Format("2006010215"), + Time: args.Args["now"].(time.Time), } - input.StartTime = "2025122300" - // 工作流过程调用 output, err := runnable.Invoke(ctx, input) if err != nil { @@ -75,66 +72,107 @@ func (w *statisticsOursProduct) Invoke(ctx context.Context, args *runtime.Workfl 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. 调用工具统计我们的商品 + // 1. 参数整理 + c.AppendLambda(compose.InvokableLambda(w.formatContext)) + + // 2. 调用工具统计我们的商品 c.AppendLambda(compose.InvokableLambda(w.callStatisticsTool)) - // 2. 生成 Excel 并转图片上传 + // 3. 生成 Excel 并转图片上传 c.AppendLambda(compose.InvokableLambda(w.generateExcelAndUpload)) - // 3. 转map输出 + // 4. 转map输出 c.AppendLambda(compose.InvokableLambda(w.convertToMap)) return c.Compile(ctx) } -func (w *statisticsOursProduct) callStatisticsTool(ctx context.Context, input *StatisticsOursProductWorkflowInput) ([]statistics_ours_product.StatisticsOursProductItem, error) { - req := statistics_ours_product.StatisticsOursProductRequest{ - Page: 1, - Limit: 100, // 假设取前100条 - Serial: []string{input.StartTime, input.EndTime}, - } +// 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 w.toolManager.Recharge.StatisticsOursProduct.Call(ctx, req) + return &StatisticsOursProductContext{ + Time: time.Now(), + StartTime: startTime, + EndTime: endTime, + Title: fmt.Sprintf("截止 %s 我们的商品统计", endTimeStr), + }, nil } -func (w *statisticsOursProduct) generateExcelAndUpload(ctx context.Context, data []statistics_ours_product.StatisticsOursProductItem) (*StatisticsOursProductWorkflowOutput, error) { - // 2. 获取模板路径 (假设在项目根目录的 assets/templates 下) +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", time.Now().Unix()) + 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)) - // 3. 转换数据为 [][]string - excelData := w.convertDataToExcelFormat(data) + // 2. 转换数据为 [][]string + excelData := w.convertDataToExcelFormat(state.ProductData) - // 4. 生成 Excel - excelBytes, err := w.toolManager.Common.ExcelGenerator.Call(templatePath, excelData, 4, 3) + // 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) } - // 5. Excel 转图片 + // 4. Excel 转图片 picBytes, err := w.toolManager.Common.ImageConverter.Call(fileName+".xlsx", excelBytes, 2) if err != nil { return nil, fmt.Errorf("Excel 转图片失败: %v", err) } - // 6. 上传 OSS + // 5. 上传 OSS url, err := w.ossClient.UploadBytes(fileName+".png", picBytes) if err != nil { return nil, fmt.Errorf("上传 OSS 失败: %v", err) } - res := &StatisticsOursProductWorkflowOutput{ - Path: "", - Url: url, - Data: excelData, - Desc: "", - } + state.ImgUrl = url + state.ExcelData = excelData - return res, nil + return state, nil } // convertDataToExcelFormat 将业务数据转换为 Excel 生成器需要的二维字符串数组 @@ -158,11 +196,11 @@ func (w *statisticsOursProduct) convertDataToExcelFormat(data []statistics_ours_ return result } -func (w *statisticsOursProduct) convertToMap(ctx context.Context, output *StatisticsOursProductWorkflowOutput) (map[string]any, error) { +func (w *statisticsOursProduct) convertToMap(ctx context.Context, state *StatisticsOursProductContext) (map[string]any, error) { return map[string]any{ - "path": output.Path, - "url": output.Url, - "data": output.Data, - "desc": output.Desc, + "path": "", + "url": state.ImgUrl, + "data": state.ExcelData, + "desc": state.Title, }, nil } From 2d725ecd539aed214a671011203f15a522ff8ddc Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 15:55:08 +0800 Subject: [PATCH 22/27] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/workflow/recharge/statistics_ours_product.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/domain/workflow/recharge/statistics_ours_product.go b/internal/domain/workflow/recharge/statistics_ours_product.go index 8c78209..b3917f4 100644 --- a/internal/domain/workflow/recharge/statistics_ours_product.go +++ b/internal/domain/workflow/recharge/statistics_ours_product.go @@ -138,7 +138,7 @@ func (w *statisticsOursProduct) callStatisticsTool(ctx context.Context, state *S 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") + 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 From 06606539d2464f4549ed76edf36ed6c34caf8d2b Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 16:10:08 +0800 Subject: [PATCH 23/27] =?UTF-8?q?feat=EF=BC=9A=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=88=86=E9=94=80=E5=95=86=E9=BB=91=E5=90=8D=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tools/bbxt/bbxt.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index 4a0893b..cb3ebba 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -5,6 +5,7 @@ import ( "ai_scheduler/pkg" "fmt" "math/rand" + "slices" "sort" @@ -16,6 +17,13 @@ const ( GreenStyle = "${color: 00B050;horizontal:center;vertical:center;borderColor:#000000}" ) +var resellerBlackList = []string{ + "悦跑", + "电商-独立", + "蓝星严选连续包月", + "通钱", +} + type BbxtTools struct { cacheDir string excelTempDir string @@ -138,7 +146,7 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes 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 } From fc2499d6276946c1b562e1ed6af5d57fc06e0e9c Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 16:21:05 +0800 Subject: [PATCH 24/27] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=BA=BF?= =?UTF-8?q?=E4=B8=8A=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.yaml | 13 +++++++++++++ config/config_test.yaml | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index 1396c00..5585caf 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -43,6 +43,12 @@ redis: db: driver: mysql source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +oss: + access_key: "LTAI5tGGZzjf3tvqWk8SQj2G" + secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq" + bucket: "attachment-public" + domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com" + endpoint: "https://oss-cn-hangzhou.aliyuncs.com" tools: zltxOrderDetail: @@ -118,6 +124,13 @@ eino_tools: # 货易通商品品牌查询 hytGoodsBrandSearch: base_url: "https://hyt.86698.cn/admin_upload/api/v1/goods/brand/list" + # == 电商充值系统 == + # 我们的商品统计 + rechargeStatisticsOursProduct: + base_url: "http://admin.lanseds.cn/admin/statistics/oursProduct" + api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY3MTc5ODgzLCJuYmYiOjE3NjcxNjkwODMsImp0aSI6IjEiLCJQaG9uZSI6IjE4MjAwMTYwMTQzIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MiwiR3JvdXBDb2RlcyI6IkxTWEREU19TWVNURU0sU1RBVElTVElDQUxTWVNURU1fQURNSU4sUEhZU0lDQUxHT09EU19BRE1JTiIsIkRpbmdVc2VySWQiOiIifQ.ELNF1Iv6yEwA12nCbXGKwXCw-F5Gq4GI2t2nqo1PlSkFdQ5Oz5s5NwV0RUXA66LxCggI-9IjBtFI1MvBHpvTHq9QRlm-HKzVTMcOBkEtKEfCCI6SPKVTAZyntTJlWPKG3u-CJUotT5YW0j2rU1VcpA7uGEiY7gs5VPUOZ80R1uGJ7HBSqVI2DRqar6STa1xryygdCjK7qamUM2d6aJ6r9VPTBt-JO6dkDdw3KHs3wl-PGM3wcbXHZ2aC18WFd_PxLmtjqErpxTEkdUBCnUHOSKDePG0henDJq71Nh3yRdRmY9VvszHMyIxJA2BVGPIPUT_Y5aewaaEMQVEjhiBnn-Q" + excel2pic: + base_url: "http://192.168.6.109:8010/api/v1/convert" dingtalk: api_key: "dingsbbntrkeiyazcfdg" diff --git a/config/config_test.yaml b/config/config_test.yaml index 35a2888..19a7745 100644 --- a/config/config_test.yaml +++ b/config/config_test.yaml @@ -127,8 +127,8 @@ eino_tools: # == 电商充值系统 == # 我们的商品统计 rechargeStatisticsOursProduct: - base_url: "http://admin.1688sup.cn:8001/admin/statistics/oursProduct" - api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY3MDc3NzcwLCJuYmYiOjE3NjcwNzU5NzAsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.Nuw_aR6iSPmhhh9E5rhyTxHBsgWtaTZvbnc7SFTnUBJXTQvYahnk0LyZaVpsVw6FB3cU0F5xOdX3rmGyWyaiszWO6yi-o1oxGMXwhf39fMiWT2xUI6pAn9Ync8DmZ4tOMCNUTdEk4CaQFzrTwJs0c-VR4yW6LgoPmNPvUVZ-KwmusUpnPz5j9RsJItzIWE3bpGGsfB54e2UERcZdbo9BXxCZIBbpAYKBKdl73KuI8SNaXgKvGTrJ6hEN4ESpnbisJVwT5pp_kuChJlcfjHTHFsEf4RJDjN9gTrtDbBWZyY3OmO2ukqYAM7tZPs6TXJwvQGJQsFRVZUBGxS1nD_6DzQ" + 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" From 39776dfcce3ccc0e832c6e51a1b0a30a003fb6f6 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 17:01:16 +0800 Subject: [PATCH 25/27] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=EF=BC=8C=E6=89=93=E5=BC=80=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/main.go | 2 +- deploy.sh | 1 + internal/biz/ding_talk_bot.go | 14 +++++++++++++- .../recharge/statistics_ours_product/types.go | 14 +++++++------- .../workflow/recharge/statistics_ours_product.go | 8 ++++---- internal/tools/bbxt/bbxt.go | 2 +- 6 files changed, 27 insertions(+), 14 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index c0ff4c8..baca30d 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -29,6 +29,6 @@ func main() { //钉钉机器人 app.DingBotServer.Run(ctx, *onBot) //定时任务 - //app.Cron.Run(ctx) + app.Cron.Run(ctx) log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port))) } diff --git a/deploy.sh b/deploy.sh index 2e1e972..3a829de 100644 --- a/deploy.sh +++ b/deploy.sh @@ -36,6 +36,7 @@ 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 \ "${CONTAINER_NAME}" ./server --config "./${CONFIG_FILE}" --bot "${BOT}" docker logs -f ${CONTAINER_NAME} \ No newline at end of file diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index 40f99fb..0475021 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -629,7 +629,11 @@ func (d *DingTalkBotBiz) GetReportLists(ctx context.Context, group *model.AiBotG } // 追加电商充值系统统计 - 返回统一使用 []*bbxt.ReportRes - rechargeReports, err := d.rechargeDailyReport(ctx, time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, d.ossClient) + rechargeReports, err := d.rechargeDailyReport(ctx, time.Now(), product, d.ossClient) + if err != nil || len(rechargeReports) == 0 { + return + } + reports = append(reports, rechargeReports...) return @@ -637,6 +641,12 @@ func (d *DingTalkBotBiz) GetReportLists(ctx context.Context, group *model.AiBotG // 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{ @@ -649,6 +659,8 @@ func (d *DingTalkBotBiz) rechargeDailyReport(ctx context.Context, now time.Time, return } + log.Infof("imgUrl: %s", res["url"].(string)) + reports = []*bbxt.ReportRes{ { ReportName: "我们的商品统计(电商充值系统)", diff --git a/internal/domain/tools/recharge/statistics_ours_product/types.go b/internal/domain/tools/recharge/statistics_ours_product/types.go index 624406b..3d8e86d 100644 --- a/internal/domain/tools/recharge/statistics_ours_product/types.go +++ b/internal/domain/tools/recharge/statistics_ours_product/types.go @@ -19,12 +19,12 @@ type StatisticsOursProductResponse struct { type StatisticsOursProductItem struct { OursProductId int `json:"ours_product_id"` ResellerId int `json:"reseller_id"` - TotalPrice string `json:"total_price"` - Count string `json:"count"` - SuccessCount string `json:"success_count"` - SuccessPrice string `json:"success_price"` - FailCount string `json:"fail_count"` - FailPrice string `json:"fail_price"` - Profit string `json:"profit"` + TotalPrice any `json:"total_price"` + Count any `json:"count"` + SuccessCount any `json:"success_count"` + SuccessPrice any `json:"success_price"` + FailCount any `json:"fail_count"` + FailPrice any `json:"fail_price"` + Profit any `json:"profit"` OursProductName string `json:"ours_product_name"` } diff --git a/internal/domain/workflow/recharge/statistics_ours_product.go b/internal/domain/workflow/recharge/statistics_ours_product.go index b3917f4..8ca32c2 100644 --- a/internal/domain/workflow/recharge/statistics_ours_product.go +++ b/internal/domain/workflow/recharge/statistics_ours_product.go @@ -182,13 +182,13 @@ func (w *statisticsOursProduct) convertDataToExcelFormat(data []statistics_ours_ row := []string{ item.OursProductName, // fmt.Sprintf("%d", item.OursProductId), - item.Count, + fmt.Sprintf("%v", item.Count), // item.TotalPrice, // item.SuccessCount, - item.SuccessPrice, + fmt.Sprintf("%v", item.SuccessPrice), // item.FailCount, - // item.FailPrice, - item.Profit, + // item.FailPrice + fmt.Sprintf("%v", item.Profit), } result = append(result, row) diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index cb3ebba..9bc400e 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -21,7 +21,7 @@ var resellerBlackList = []string{ "悦跑", "电商-独立", "蓝星严选连续包月", - "通钱", + "通钱-2025年12月", } type BbxtTools struct { From bcfdeade46cbff7527541a29741ce9c924365e32 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 17:10:27 +0800 Subject: [PATCH 26/27] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy.sh b/deploy.sh index 3a829de..08c3e63 100644 --- a/deploy.sh +++ b/deploy.sh @@ -37,6 +37,7 @@ docker run -itd \ -e "MODE=${MODE}" \ -p 8090:8090 \ -v ./cache:/app/cache \ + -v ./tmpl:/app/tmpl \ "${CONTAINER_NAME}" ./server --config "./${CONFIG_FILE}" --bot "${BOT}" docker logs -f ${CONTAINER_NAME} \ No newline at end of file From 7c783fa52498b206a54dbdc153b83c2f01a7f8c9 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 31 Dec 2025 17:15:42 +0800 Subject: [PATCH 27/27] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy.sh b/deploy.sh index 08c3e63..ba9952a 100644 --- a/deploy.sh +++ b/deploy.sh @@ -38,6 +38,7 @@ docker run -itd \ -p 8090:8090 \ -v ./cache:/app/cache \ -v ./tmpl:/app/tmpl \ + -v ./go.mod:/app/go.mod \ "${CONTAINER_NAME}" ./server --config "./${CONFIG_FILE}" --bot "${BOT}" docker logs -f ${CONTAINER_NAME} \ No newline at end of file