411 lines
11 KiB
Go
411 lines
11 KiB
Go
package bbxt
|
||
|
||
import (
|
||
"ai_scheduler/internal/pkg/oss"
|
||
"ai_scheduler/pkg"
|
||
"fmt"
|
||
"reflect"
|
||
"regexp"
|
||
"math/rand"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/go-kratos/kratos/v2/log"
|
||
"github.com/xuri/excelize/v2"
|
||
)
|
||
|
||
type BbxtTools struct {
|
||
cacheDir string
|
||
excelTempDir string
|
||
ossClient *oss.Client
|
||
}
|
||
|
||
func NewBbxtTools(ossClient *oss.Client) (*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),
|
||
ossClient: ossClient,
|
||
}, 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%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%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)
|
||
}
|
||
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)
|
||
}
|