ai_scheduler/internal/tools/bbxt/upload.go

347 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package bbxt
import (
"ai_scheduler/internal/config"
"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
config *config.Config
}
func NewUploader(oss *utils_oss.Client, config *config.Config) *Uploader {
return &Uploader{
ossClient: oss,
config: config,
}
}
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://excel2pic:8000/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", u.config.EinoTools.Excel2Pic.BaseURL, 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, sumSlice []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 sumSlice {
// 排序 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)
}