ai_scheduler/internal/tools/bbxt/bbxt.go

406 lines
11 KiB
Go
Raw 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/pkg"
"fmt"
"reflect"
"regexp"
"sort"
"strings"
"time"
"github.com/go-kratos/kratos/v2/log"
"github.com/xuri/excelize/v2"
)
type BbxtTools struct {
cacheDir string
excelTempDir string
}
func NewBbxtTools() (*BbxtTools, error) {
cache, err := pkg.GetCacheDir()
if err != nil {
return nil, err
}
tempDir, err := pkg.GetTmplDir()
if err != nil {
return nil, err
}
return &BbxtTools{
cacheDir: cache,
excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir),
}, nil
}
func (b *BbxtTools) DailyReport(now time.Time) (err error) {
err = b.StatisOursProductLossSum([]string{
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).Format("2006-01-02 15:04:05"),
})
if err != nil {
return
}
return
}
// StatisOursProductLossSumTotal 负利润分析
func (b *BbxtTools) StatisOursProductLossSum(ct []string) (err error) {
data, err := StatisOursProductLossSumApi(&StatisOursProductLossSumReq{
Ct: ct,
})
if err != nil {
return
}
var (
resellerMap = make(map[int32]*ResellerLoss)
total [][]string
gt []*ResellerLoss
)
for _, info := range data.List {
// 检查经销商是否已存在
if _, ok := resellerMap[info.ResellerId]; !ok {
// 创建新的经销商记录
resellerMap[info.ResellerId] = &ResellerLoss{
ResellerId: info.ResellerId,
ResellerName: info.ResellerName,
Total: 0, // 初始化为0后续累加
ProductLoss: make(map[int32]ProductLoss), // 初始化map
}
}
// 获取当前经销商
reseller := resellerMap[info.ResellerId]
// 累加经销商总亏损
reseller.Total += info.Loss
// 检查产品是否已存在
if _, ok := reseller.ProductLoss[info.OursProductId]; !ok {
// 创建新的产品亏损记录
reseller.ProductLoss[info.OursProductId] = ProductLoss{
ProductId: info.OursProductId,
ProductName: info.OursProductName,
Loss: info.Loss, // 初始化为当前产品的亏损
}
} else {
// 已存在产品记录,累加亏损
productLoss := reseller.ProductLoss[info.OursProductId]
productLoss.Loss += info.Loss
reseller.ProductLoss[info.OursProductId] = productLoss
}
}
// 按经销商总亏损排序
resellers := make([]*ResellerLoss, 0, len(resellerMap))
for _, v := range resellerMap {
resellers = append(resellers, v)
}
sort.Slice(resellers, func(i, j int) bool {
return resellers[i].Total < resellers[j].Total
})
// 构建分组
for _, v := range resellers {
if v.Total <= -100 {
total = append(total, []string{
fmt.Sprintf("%s", v.ResellerName),
fmt.Sprintf("%.2f", v.Total),
})
}
if v.Total <= -500 {
gt = append(gt, v)
}
}
//总量生成excel
if len(total) > 0 {
filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d", time.Now().Unix()) + ".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)
}
return err
}
// GetProfitRankingSum 利润同比分销商排行榜
func (b *BbxtTools) GetProfitRankingSum(now time.Time) (err error) {
ct := []string{
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
now.Format(time.DateTime),
}
data, err := GetProfitRankingSumApi(&GetProfitRankingSumRequest{
Ct: ct,
})
timeCh := now.Format("1月2日15点")
title := "截至" + timeCh + "利润同比分销商排行榜"
if err != nil {
return
}
//排序
sort.Slice(data.List, func(i, j int) bool {
return data.List[i].HistoryOneDiff > data.List[j].HistoryOneDiff
})
//取前20和后20
var (
total [][]string
top = data.List[:20]
bottom = data.List[len(data.List)-20:]
)
//合并前20和后20
top = append(top, bottom...)
// 构建分组
for _, v := range top {
var diff string
if v.HistoryOneDiff > 0 {
diff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%.4f", v.HistoryOneDiff)
} else {
diff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%.4f", v.HistoryOneDiff)
}
total = append(total, []string{
fmt.Sprintf("%s", v.ResellerName),
fmt.Sprintf("%.4f", v.CurrentProfit),
fmt.Sprintf("%.4f", v.HistoryOneProfit),
diff,
})
}
//总量生成excel
if len(total) > 0 {
filePath := b.cacheDir + "/lrtb_rank" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx"
err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"lrtb_rank.xlsx", filePath, total, title)
}
return err
}
// GetStatisOfficialProductSum 利润同比分销商排行榜
func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []string) (err error) {
ct := []string{
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
now.Format(time.DateTime),
}
var ids []int32
if len(productName) > 0 {
ids, err = b.getProductIdFromProductName(productName)
if err != nil {
return
}
}
reqParam := &GetStatisOfficialProductSumRequest{
Ct: ct,
}
if len(ids) > 0 {
reqParam.OfficialProductId = ids
}
data, err := GetStatisOfficialProductSumApi(reqParam)
if err != nil {
return
}
var total [][]string
for _, v := range data.OfficialProductSum {
var (
yeterDatyDiff string
lastWeekDiff string
)
if v.HistoryOneDiff > 0 {
yeterDatyDiff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%d", v.HistoryOneDiff)
} else {
yeterDatyDiff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%d", v.HistoryOneDiff)
}
if v.HistoryTwoDiff > 0 {
lastWeekDiff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%d", v.HistoryTwoDiff)
} else {
lastWeekDiff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%d", v.HistoryTwoDiff)
}
total = append(total, []string{
fmt.Sprintf("%s", v.OfficialProductName),
fmt.Sprintf("%d", v.CurrentNum),
fmt.Sprintf("%d", v.HistoryOneNum),
yeterDatyDiff,
fmt.Sprintf("%d", v.HistoryTwoNum),
lastWeekDiff,
})
}
timeCh := now.Format("1月2日15点")
title := "截至" + timeCh + "销售同比分析"
//总量生成excel
if len(total) > 0 {
filePath := b.cacheDir + "/xstb_ana" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx"
err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"xstb_ana.xlsx", filePath, total, title)
}
return err
}
func (b *BbxtTools) getProductIdFromProductName(productNames []string) ([]int32, error) {
data, err := GetStatisFilterOfficialProductApi(&GetStatisFilterOfficialProductRequest{})
if err != nil {
return nil, err
}
var product2IdMap = make(map[string]int32)
for _, v := range data.List {
product2IdMap[v.OfficialProductName] = v.OfficialProductId
}
var ids []int32
for _, v := range productNames {
if id, ok := product2IdMap[v]; ok {
ids = append(ids, id)
}
}
return ids, nil
}
func (b *BbxtTools) SimpleFillExcelWithTitle(templatePath, outputPath string, dataSlice interface{}, title string) error {
// 1. 打开模板
f, err := excelize.OpenFile(templatePath)
if err != nil {
return err
}
defer f.Close()
sheet := f.GetSheetName(0)
// 1.1 获取第三行模板样式
templateRow := 3
styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", templateRow))
if err != nil {
log.Errorf("获取模板样式失败: %v", err)
styleID = 0
}
// 1.2 获取模板行高
rowHeight, err := f.GetRowHeight(sheet, templateRow)
if err != nil {
log.Errorf("获取模板行高失败: %v", err)
rowHeight = 31 // 默认高度
}
// 2. 写入标题到第一行
f.SetCellValue(sheet, "A1", title)
// 3. 反射获取切片数据
v := reflect.ValueOf(dataSlice)
if v.Kind() != reflect.Slice {
return fmt.Errorf("dataSlice must be a slice")
}
if v.Len() == 0 {
return nil
}
// 4. 从第三行开始填充数据(第二行留空或作为标题行)
startRow := 3
pattern := `\$\{(.*?)\}`
re := regexp.MustCompile(pattern)
for i := 0; i < v.Len(); i++ {
currentRow := startRow + i
// 获取当前行数据
item := v.Index(i)
// 处理不同类型的切片
var rowData []interface{}
if item.Kind() == reflect.Slice || item.Kind() == reflect.Array {
// 处理 []string 或 [][]string 中的一行
for j := 0; j < item.Len(); j++ {
if item.Index(j).CanInterface() {
rowData = append(rowData, item.Index(j).Interface())
}
}
} else if item.Kind() == reflect.Interface {
// 处理 interface{} 类型
if actualValue, ok := item.Interface().([]string); ok {
for _, val := range actualValue {
rowData = append(rowData, val)
}
} else {
rowData = []interface{}{item.Interface()}
}
} else {
rowData = []interface{}{item.Interface()}
}
// 4.1 设置行高
f.SetRowHeight(sheet, currentRow, rowHeight)
// 5. 填充数据到Excel
for col, value := range rowData {
cell := fmt.Sprintf("%c%d", 'A'+col, currentRow)
// 5.1 应用模板样式到整行(根据实际列数)
if styleID != 0 && len(rowData) > 0 {
startCol := "A"
endCol := fmt.Sprintf("%c", 'A'+len(rowData)-1)
endCell := fmt.Sprintf("%s%d", endCol, currentRow)
f.SetCellStyle(sheet, fmt.Sprintf("%s%d", startCol, currentRow),
endCell, styleID)
}
switch value.(type) {
case string:
var style = value.(string)
if re.MatchString(style) {
matches := re.FindStringSubmatch(style)
styleMap := make(map[string]string)
//matches = strings.Replace(matches, "$", "", 1)
if len(matches) != 2 {
continue
}
for _, kv := range strings.Split(matches[1], ";") {
kvParts := strings.Split(kv, ":")
if len(kvParts) == 2 {
styleMap[strings.TrimSpace(kvParts[0])] = strings.TrimSpace(kvParts[1])
}
}
fontStyleID, _err := SetStyle(styleMap, f)
if _err == nil {
f.SetCellStyle(sheet, cell, cell, fontStyleID)
}
value = re.ReplaceAllString(style, "")
}
f.SetCellValue(sheet, cell, value)
default:
}
}
}
// 6. 保存
return f.SaveAs(outputPath)
}
func SetStyle(styleMap map[string]string, f *excelize.File) (int, error) {
var style = &excelize.Style{}
if colorHex, exists := styleMap["color"]; exists {
style.Font = &excelize.Font{
Color: colorHex,
}
}
if horizontal, exists := styleMap["horizontal"]; exists {
if style.Alignment == nil {
style.Alignment = &excelize.Alignment{}
}
style.Alignment.Horizontal = horizontal
}
if vertical, exists := styleMap["vertical"]; exists {
if style.Alignment == nil {
style.Alignment = &excelize.Alignment{}
}
style.Alignment.Vertical = vertical
}
return f.NewStyle(style)
}