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) }