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] =?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