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/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 bfee143..a5c2aa3 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -1,10 +1,12 @@ package bbxt import ( + "ai_scheduler/internal/pkg/oss" "ai_scheduler/pkg" "fmt" "reflect" "regexp" + "math/rand" "sort" "strings" "time" @@ -16,9 +18,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 @@ -31,6 +34,7 @@ func NewBbxtTools() (*BbxtTools, error) { return &BbxtTools{ cacheDir: cache, excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir), + ossClient: ossClient, }, nil } @@ -117,13 +121,14 @@ func (b *BbxtTools) StatisOursProductLossSum(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" - err = b.resellerDetailFillExcel(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt) + 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) } return err } diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go index 066bb2d..e3cfb73 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 3711aa0..f5b52bf 100644 --- a/internal/tools/bbxt/excel.go +++ b/internal/tools/bbxt/excel.go @@ -1,11 +1,19 @@ package bbxt import ( + "bytes" "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "path/filepath" "reflect" "sort" + "strings" "github.com/go-kratos/kratos/v2/log" + "github.com/shopspring/decimal" "github.com/xuri/excelize/v2" ) @@ -80,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) } @@ -170,12 +194,154 @@ func (b *BbxtTools) resellerDetailFillExcel(templatePath, outputPath string, dat } } - // buffer, err := f.WriteToBuffer() - // if err != nil { - // return err - // } + // 6. 保存 + return f.SaveAs(outputPath) +} - // return buffer.Bytes(), nil +// 分销商负利润详情填充excel-V2 +// 1.使用模板文件作为输出文件,从第二行开始填充 +// 2.整体为3列:1.分销商名称(以ResellerName为分组,分销商名称列使用的样式为) 2.商品名称(p.ProductName) 3.亏损金额(p.Loss) +// 3.分销商名称列使用的样式为 A2;商品名称、亏损金额使用的样式为 B2、C2;样式包括宽高、背景、颜色等 +// 4.以ResellerName分组,合并单元格 +// 5.在文件末尾使用“合计”,合计行样式为模板第四行 +// 6.保存为新文件 +func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, dataSlice []*ResellerLoss) error { + // 1. 读取模板 + f, err := excelize.OpenFile(templatePath) + if err != nil { + return err + } + defer f.Close() + + sheet := f.GetSheetName(0) + + // ---------------- 样式获取 ---------------- + // 模板第2行:数据行样式 + tplRowData := 2 + styleA2, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowData)) + if err != nil { + styleA2 = 0 + } + // B2和C2通常样式一致,这里取B2作为明细列样式 + styleB2, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowData)) + if err != nil { + styleB2 = 0 + } + styleC2, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowData)) + if err != nil { + styleC2 = 0 + } + + rowHeightData, err := f.GetRowHeight(sheet, tplRowData) + if err != nil { + rowHeightData = 20 + } + + // 模板第4行:合计行样式 + tplRowTotal := 4 + styleTotalA, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowTotal)) + if err != nil { + styleTotalA = 0 + } + styleTotalB, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowTotal)) + if err != nil { + styleTotalB = 0 + } + styleTotalC, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowTotal)) + if err != nil { + styleTotalC = 0 + } + rowHeightTotal, err := f.GetRowHeight(sheet, tplRowTotal) + if err != nil { + rowHeightTotal = 30 + } + // ---------------------------------------- + + currentRow := 2 + totalLoss := 0.0 + + for _, reseller := range dataSlice { + // 排序 ProductLoss + var products []ProductLoss + for _, p := range reseller.ProductLoss { + products = append(products, p) + } + sort.Slice(products, func(i, j int) bool { + return products[i].Loss < products[j].Loss + }) + + startRow := currentRow + + // 填充该经销商的所有产品 + for _, p := range products { + // 设置行高 + f.SetRowHeight(sheet, currentRow, rowHeightData) + + // 设置值 + f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), reseller.ResellerName) + f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.ProductName) + f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), p.Loss) + + // 设置样式 + if styleA2 != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleA2) + } + if styleB2 != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleB2) + } + if styleC2 != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleC2) + } + + totalLoss += p.Loss + currentRow++ + } + + endRow := currentRow - 1 + // 合并单元格 (如果多于1行) + if endRow > startRow { + f.MergeCell(sheet, fmt.Sprintf("A%d", startRow), fmt.Sprintf("A%d", endRow)) + } + } + + // ---------------- 填充合计行 ---------------- + // 四舍五入保留四位小数 + totalLoss, _ = decimal.NewFromFloat(totalLoss).Round(4).Float64() + // 设置行高 + f.SetRowHeight(sheet, currentRow, rowHeightTotal) + + f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), "合计") + // B列留空,C列填充总亏损 + f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), totalLoss) + + // 设置合计行样式 + if styleTotalA != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleTotalA) + } + if styleTotalB != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleTotalB) + } + if styleTotalC != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleTotalC) + } + // 取消合并合计行的A、B列 + // f.MergeCell(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow)) + + 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) @@ -188,7 +354,91 @@ func (b *BbxtTools) resellerDetailFillExcel(templatePath, outputPath string, dat // --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) +} + +// 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/zltx/zltx_statistics.go b/internal/tools/zltx/zltx_statistics.go index 5d71a9b..29e143a 100644 --- a/internal/tools/zltx/zltx_statistics.go +++ b/internal/tools/zltx/zltx_statistics.go @@ -45,7 +45,7 @@ func (z ZltxOrderStatisticsTool) Definition() entitys.ToolDefinition { } type ZltxOrderStatisticsRequest struct { - Number string `json:"number"` + Number interface{} `json:"number"` } func (z ZltxOrderStatisticsTool) Execute(ctx context.Context, rec *entitys.Recognize) error { @@ -53,7 +53,7 @@ func (z ZltxOrderStatisticsTool) Execute(ctx context.Context, rec *entitys.Recog if err := json.Unmarshal([]byte(rec.Match.Parameters), &req); err != nil { return err } - if req.Number == "" { + if req.Number == nil { return fmt.Errorf("number is required") } return z.getZltxOrderStatistics(req.Number, rec) @@ -76,14 +76,13 @@ type ZltxOrderStatisticsData struct { Total int `json:"total"` } -func (z ZltxOrderStatisticsTool) getZltxOrderStatistics(number string, rec *entitys.Recognize) error { +func (z ZltxOrderStatisticsTool) getZltxOrderStatistics(number interface{}, rec *entitys.Recognize) error { ext, err := rec_extra.GetTaskRecExt(rec) if err != nil { return err } //查询订单详情 - - url := fmt.Sprintf("%s%s", z.config.BaseURL, number) + url := fmt.Sprintf("%s%s", z.config.BaseURL, fmt.Sprintf("%v", number)) req := l_request.Request{ Url: url, Headers: map[string]string{ diff --git a/tmpl/excel_temp/kshj_gt.xlsx b/tmpl/excel_temp/kshj_gt.xlsx index 6933f63..a74a6f3 100755 Binary files a/tmpl/excel_temp/kshj_gt.xlsx and b/tmpl/excel_temp/kshj_gt.xlsx differ diff --git a/tmpl/excel_temp/kshj_gt.xlsx.v1 b/tmpl/excel_temp/kshj_gt.xlsx.v1 new file mode 100755 index 0000000..6933f63 Binary files /dev/null and b/tmpl/excel_temp/kshj_gt.xlsx.v1 differ