diff --git a/internal/tools/bbxt/api.go b/internal/tools/bbxt/api.go index 64632cb..b07ce1e 100644 --- a/internal/tools/bbxt/api.go +++ b/internal/tools/bbxt/api.go @@ -53,19 +53,19 @@ type GetProfitRankingSumResponse struct { type ProfitRankingSumResponse struct { // 分销商ID - ResellerId string `protobuf:"bytes,1,opt,name=reseller_id,json=resellerId,proto3" json:"reseller_id,omitempty"` + ResellerId string `protobuf:"bytes,1,opt,name=reseller_id,json=resellerId,proto3" json:"ResellerId,omitempty"` // 分销商名称 - ResellerName string `protobuf:"bytes,2,opt,name=reseller_name,json=resellerName,proto3" json:"reseller_name,omitempty"` + ResellerName string `protobuf:"bytes,2,opt,name=reseller_name,json=resellerName,proto3" json:"ResellerName,omitempty"` // 当前利润 - CurrentProfit float64 `protobuf:"fixed64,3,opt,name=current_profit,json=currentProfit,proto3" json:"current_profit,omitempty"` + CurrentProfit float64 `protobuf:"fixed64,3,opt,name=current_profit,json=currentProfit,proto3" json:"CurrentProfit,omitempty"` // 昨日同比利润 - HistoryOneProfit float64 `protobuf:"fixed64,4,opt,name=history_one_profit,json=historyOneProfit,proto3" json:"history_one_profit,omitempty"` + HistoryOneProfit float64 `protobuf:"fixed64,4,opt,name=history_one_profit,json=historyOneProfit,proto3" json:"HistoryOneProfit,omitempty"` // 上周同比利润 - HistoryTwoProfit float64 `protobuf:"fixed64,5,opt,name=history_two_profit,json=historyTwoProfit,proto3" json:"history_two_profit,omitempty"` + HistoryTwoProfit float64 `protobuf:"fixed64,5,opt,name=history_two_profit,json=historyTwoProfit,proto3" json:"HistoryTwoProfit,omitempty"` // 昨日同比利润差值 - HistoryOneDiff float64 `protobuf:"fixed64,6,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"history_one_diff,omitempty"` + HistoryOneDiff float64 `protobuf:"fixed64,6,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"HistoryOneDiff,omitempty"` // 上周同比利润差值 - HistoryTwoDiff float64 `protobuf:"fixed64,7,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"history_two_diff,omitempty"` + HistoryTwoDiff float64 `protobuf:"fixed64,7,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"HistoryTwoDiff,omitempty"` } // GetProfitRankingSumApi 利润同比分销商排行榜 @@ -144,6 +144,29 @@ type resCode struct { Error string `json:"error"` } +type GetStatisFilterOfficialProductRequest struct { + OfficialProductId int32 `protobuf:"varint,1,opt,name=official_product_id,json=officialProductId,proto3" json:"official_product_id,omitempty"` +} + +type GetStatisFilterOfficialProductResponse struct { + List []*StatisFilterOfficialProductResponse `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` +} + +type StatisFilterOfficialProductResponse struct { + OfficialProductId int32 `protobuf:"varint,1,opt,name=official_product_id,json=officialProductId,proto3" json:"OfficialProductId,omitempty"` + OfficialProductName string `protobuf:"bytes,2,opt,name=official_product_name,json=officialProductName,proto3" json:"OfficialProductName,omitempty"` +} + +// GetStatisFilterOfficialProductApi 官方商品列表 +func GetStatisFilterOfficialProductApi(param *GetStatisFilterOfficialProductRequest) (*GetStatisFilterOfficialProductResponse, error) { + url := "/dataanalytics/statisFilterOfficialProduct" + var res GetStatisFilterOfficialProductResponse + if err := request(url, param, &res); err != nil { + return nil, err + } + return &res, nil +} + func request(url string, reqData interface{}, resData interface{}) error { reqParam, err := pkg.StructToQuery(reqData) diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index f7d1e68..bfee143 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -3,8 +3,14 @@ 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 { @@ -28,11 +34,11 @@ func NewBbxtTools() (*BbxtTools, error) { }, nil } -func (b *BbxtTools) DailyReport(today time.Time) (err error) { +func (b *BbxtTools) DailyReport(now time.Time) (err error) { - err = b.StatisOursProductLossSumTotal([]string{ - time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, today.Location()).Format("2006-01-02 15:04:05"), - time.Date(today.Year(), today.Month(), today.Day(), 23, 59, 59, 0, today.Location()).Format("2006-01-02 15:04:05"), + 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 @@ -40,8 +46,8 @@ func (b *BbxtTools) DailyReport(today time.Time) (err error) { return } -// OursProductLossSum 负利润分析 -func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { +// StatisOursProductLossSumTotal 负利润分析 +func (b *BbxtTools) StatisOursProductLossSum(ct []string) (err error) { data, err := StatisOursProductLossSumApi(&StatisOursProductLossSumReq{ Ct: ct, }) @@ -121,3 +127,279 @@ func (b *BbxtTools) StatisOursProductLossSumTotal(ct []string) (err error) { } 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) +} diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go index b6b2275..066bb2d 100644 --- a/internal/tools/bbxt/bbxt_test.go +++ b/internal/tools/bbxt/bbxt_test.go @@ -15,3 +15,25 @@ func Test_StatisOursProductLossSumApiTotal(t *testing.T) { t.Log(err) } + +func Test_GetProfitRankingSum(t *testing.T) { + o, err := NewBbxtTools() + if err != nil { + panic(err) + } + err = o.GetProfitRankingSum(time.Now()) + + t.Log(err) + +} + +func Test_GetStatisOfficialProductSum(t *testing.T) { + o, err := NewBbxtTools() + if err != nil { + panic(err) + } + err = o.GetStatisOfficialProductSum(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}) + + t.Log(err) + +} diff --git a/pkg/func.go b/pkg/func.go index 006f16f..9b4f89b 100644 --- a/pkg/func.go +++ b/pkg/func.go @@ -63,3 +63,11 @@ func GetTmplDir() (string, error) { } return path, nil } + +func ReverseSliceNew[T any](s []T) []T { + result := make([]T, len(s)) + for i := 0; i < len(s); i++ { + result[i] = s[len(s)-1-i] + } + return result +} diff --git a/tmpl/excel_temp/lrtb_rank.xlsx b/tmpl/excel_temp/lrtb_rank.xlsx new file mode 100644 index 0000000..d3ed484 Binary files /dev/null and b/tmpl/excel_temp/lrtb_rank.xlsx differ diff --git a/tmpl/excel_temp/xstb_ana.xlsx b/tmpl/excel_temp/xstb_ana.xlsx new file mode 100644 index 0000000..54fb056 Binary files /dev/null and b/tmpl/excel_temp/xstb_ana.xlsx differ