335 lines
9.6 KiB
Go
335 lines
9.6 KiB
Go
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 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
|
||
}
|
||
|
||
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.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)
|
||
}
|