From ff6f97aa37e55803a8e9ef3b7570a22ea5cf78f2 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Tue, 30 Dec 2025 10:52:30 +0800 Subject: [PATCH 1/4] =?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 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 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 2/4] =?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 3/4] =?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 4/4] =?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 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 Q=eCr