package bbxt import ( "ai_scheduler/internal/pkg/utils_oss" "bytes" "fmt" "io" "mime/multipart" "net/http" "os" "path/filepath" "regexp" "sort" "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 report == nil { return } if len(report.Path) == 0 { return } f, err := excelize.OpenFile(report.Path) if err != nil { return err } defer f.Close() excelBytes, err := f.WriteToBuffer() if err != nil { return fmt.Errorf("write to bytes failed: %v", err) } picBytes, err := u.excel2picPy(report.Path, excelBytes.Bytes(), 2) if err != nil { return fmt.Errorf("excel2picPy failed: %v", err) } // b.savePic("temp.png", picBytes) // 本地生成图片,仅测试 // outputPath 提取文件名(不包含扩展名) filename := filepath.Base(report.Path) filename = strings.TrimSuffix(filename, filepath.Ext(filename)) report.Url = u.uploadToOSS(filename, picBytes) log.Infof("imgUrl: %s", report.Url) return } // excel2picPy 将excel转换为图片python // python 接口如下: // curl --location --request POST 'http://192.168.6.109:8010/api/v1/convert' \ // --header 'Content-Type: multipart/form-data; boundary=--------------------------952147881043913664015069' \ // --form 'file=@"C:\\Users\\Administrator\\Downloads\\销售同比分析2025-12-29 0-12点.xlsx"' \ // --form 'sheet_name="销售同比分析"' func (u *Uploader) excel2picPy(templatePath string, excelBytes []byte, scale int) ([]byte, error) { // 1. 获取 Sheet Name // 尝试从 excelBytes 解析,如果失败则使用默认值 "Sheet1" sheetName := "Sheet1" f, err := excelize.OpenReader(bytes.NewReader(excelBytes)) if err == nil { sheetName = f.GetSheetName(0) if sheetName == "" { sheetName = "Sheet1" } f.Close() } // 2. 构造 Multipart 请求 body := &bytes.Buffer{} writer := multipart.NewWriter(body) // 添加文件字段 // 使用 templatePath 的文件名作为上传文件名,如果没有则用 default.xlsx filename := "default.xlsx" if templatePath != "" { filename = filepath.Base(templatePath) } part, err := writer.CreateFormFile("file", filename) if err != nil { return nil, fmt.Errorf("create form file failed: %v", err) } if _, err = part.Write(excelBytes); err != nil { return nil, fmt.Errorf("write file part failed: %v", err) } // 添加 sheet_name 字段 if err = writer.WriteField("sheet_name", sheetName); err != nil { return nil, fmt.Errorf("write field sheet_name failed: %v", err) } // 添加 scale 字段 if scale <= 0 { scale = 2 } if err = writer.WriteField("scale", fmt.Sprintf("%d", scale)); err != nil { return nil, fmt.Errorf("write field scale failed: %v", err) } if err = writer.Close(); err != nil { return nil, fmt.Errorf("close writer failed: %v", err) } // 3. 发送 HTTP POST 请求 req, err := http.NewRequest("POST", RequestUrl, body) if err != nil { return nil, fmt.Errorf("create request failed: %v", err) } req.Header.Set("Content-Type", writer.FormDataContentType()) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("send request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("api request failed with status: %d, body: %s", resp.StatusCode, string(respBody)) } // 4. 读取响应 Body (图片内容) picBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response body failed: %v", err) } return picBytes, nil } // savePic 保存图片到本地 func (u *Uploader) savePic(outputPath string, picBytes []byte) error { dir := filepath.Dir(outputPath) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("create directory failed: %v", err) } return os.WriteFile(outputPath, picBytes, 0644) } // uploadToOSS 上传至 oss 返回图片url func (u *Uploader) uploadToOSS(fileName string, fileBytes []byte) string { objectKey := fmt.Sprintf("ai-scheduler/data-analytics/images/%s.png", fileName) url, err := u.ossClient.UploadBytes(objectKey, fileBytes) if err != nil { log.Errorf("oss upload failed: %v", err) return "" } return url } //// uploadToOSS 上传至 oss 返回图片url //func (r *ReportRes) To(fileName string, fileBytes []byte) string { // objectKey := fmt.Sprintf("ai-scheduler/data-analytics/images/%s.png", fileName) // url, err := u.ossClient.UploadBytes(objectKey, fileBytes) // if err != nil { // log.Errorf("oss upload failed: %v", err) // return "" // } // return url //} func (b *BbxtTools) OfficialProductSumDeclineExcel(templatePath, outputPath string, sumMap map[int32]ProductSumDecline, title string) error { // 1. 读取模板 f, err := excelize.OpenFile(templatePath) if err != nil { return err } defer f.Close() sheet := f.GetSheetName(0) if len(title) > 0 { // 写入标题 f.SetCellValue(sheet, "A1", title) } // ---------------- 样式获取 ---------------- // 模板第2行:数据行样式 tplRowData := 3 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 } styleD2, err := f.GetCellStyle(sheet, fmt.Sprintf("D%d", tplRowData)) if err != nil { styleC2 = 0 } //styleE2, err := f.GetCellStyle(sheet, fmt.Sprintf("E%d", tplRowData)) //if err != nil { // styleC2 = 0 //} styleF2, err := f.GetCellStyle(sheet, fmt.Sprintf("F%d", tplRowData)) if err != nil { styleC2 = 0 } //styleG2, err := f.GetCellStyle(sheet, fmt.Sprintf("G%d", tplRowData)) //if err != nil { // styleC2 = 0 //} rowHeightData, err := f.GetRowHeight(sheet, tplRowData) if err != nil { rowHeightData = 20 } rowWidth := 25.00 currentRow := 3 pattern := `\$\{(.*?)\}` re := regexp.MustCompile(pattern) for _, product := range sumMap { // 排序 ProductLoss var reseller []ProductSumReseller for _, p := range product.ProductSumReseller { reseller = append(reseller, p) } sort.Slice(reseller, func(i, j int) bool { return reseller[i].HistoryOneDiff < reseller[j].HistoryOneDiff }) startRow := currentRow // 填充该经销商的所有产品 for _, p := range reseller { // 设置行高 var ( oneDiff string twoDiff string ) f.SetRowHeight(sheet, currentRow, rowHeightData) if p.HistoryOneDiff >= 0 { oneDiff = fmt.Sprintf("%s↑%d", RedStyle, p.HistoryOneDiff) } else { oneDiff = fmt.Sprintf("%s↓%d", GreenStyle, p.HistoryOneDiff) } if p.HistoryTwoDiff >= 0 { twoDiff = fmt.Sprintf("%s↑%d", RedStyle, p.HistoryTwoDiff) } else { twoDiff = fmt.Sprintf("%s↓%d", GreenStyle, p.HistoryTwoDiff) } matchesE := re.FindStringSubmatch(oneDiff) styleMap := make(map[string]string) if len(matchesE) != 2 { continue } for _, kv := range strings.Split(matchesE[1], ";") { kvParts := strings.Split(kv, ":") if len(kvParts) == 2 { styleMap[strings.TrimSpace(kvParts[0])] = strings.TrimSpace(kvParts[1]) } } fontStyleIDE, _err := SetStyle(styleMap, f) if _err != nil { log.Errorf("set style failed: %v", _err) } f.SetCellStyle(sheet, fmt.Sprintf("E%d", currentRow), fmt.Sprintf("E%d", currentRow), fontStyleIDE) oneDiffValue := re.ReplaceAllString(oneDiff, "") matchesG := re.FindStringSubmatch(twoDiff) styleMapG := make(map[string]string) if len(matchesG) != 2 { continue } for _, kv := range strings.Split(matchesG[1], ";") { kvParts := strings.Split(kv, ":") if len(kvParts) == 2 { styleMapG[strings.TrimSpace(kvParts[0])] = strings.TrimSpace(kvParts[1]) } } fontStyleIDG, _err := SetStyle(styleMapG, f) if _err != nil { log.Errorf("set style failed: %v", _err) } twoDiffValue := re.ReplaceAllString(twoDiff, "") f.SetCellStyle(sheet, fmt.Sprintf("G%d", currentRow), fmt.Sprintf("G%d", currentRow), fontStyleIDG) // 设置值 //f.SetColWidth(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), product.OfficialProductName) //f.SetColWidth(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), p.ResellerName) f.SetColWidth(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), rowWidth) f.SetColWidth(sheet, fmt.Sprintf("D%d", currentRow), fmt.Sprintf("D%d", currentRow), rowWidth) f.SetColWidth(sheet, fmt.Sprintf("E%d", currentRow), fmt.Sprintf("E%d", currentRow), rowWidth) f.SetColWidth(sheet, fmt.Sprintf("F%d", currentRow), fmt.Sprintf("F%d", currentRow), rowWidth) f.SetColWidth(sheet, fmt.Sprintf("G%d", currentRow), fmt.Sprintf("G%d", currentRow), rowWidth) f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), product.OfficialProductName) f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.ResellerName) f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), p.CurrentNum) f.SetCellValue(sheet, fmt.Sprintf("D%d", currentRow), p.HistoryOneNum) f.SetCellValue(sheet, fmt.Sprintf("E%d", currentRow), oneDiffValue) f.SetCellValue(sheet, fmt.Sprintf("F%d", currentRow), p.HistoryTwoNum) f.SetCellValue(sheet, fmt.Sprintf("G%d", currentRow), twoDiffValue) // 设置样式 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) } if styleD2 != 0 { f.SetCellStyle(sheet, fmt.Sprintf("D%d", currentRow), fmt.Sprintf("D%d", currentRow), styleC2) } if styleF2 != 0 { f.SetCellStyle(sheet, fmt.Sprintf("F%d", currentRow), fmt.Sprintf("F%d", currentRow), styleC2) } currentRow++ } endRow := currentRow - 1 // 合并单元格 (如果多于1行) if endRow > startRow { f.MergeCell(sheet, fmt.Sprintf("A%d", startRow), fmt.Sprintf("A%d", endRow)) } } // 6. 保存 return f.SaveAs(outputPath) }