From c77e6da2966779fd6f043c413a87db394ad39938 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Tue, 30 Dec 2025 16:49:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=88=A9=E6=B6=A6?= =?UTF-8?q?=E6=8E=92=E8=A1=8C=E5=92=8C=E5=95=86=E5=93=81=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tools/bbxt/api.go | 37 +++- internal/tools/bbxt/bbxt.go | 294 ++++++++++++++++++++++++++++++- internal/tools/bbxt/bbxt_test.go | 22 +++ pkg/func.go | 8 + tmpl/excel_temp/lrtb_rank.xlsx | Bin 0 -> 9572 bytes tmpl/excel_temp/xstb_ana.xlsx | Bin 0 -> 9602 bytes 6 files changed, 348 insertions(+), 13 deletions(-) create mode 100644 tmpl/excel_temp/lrtb_rank.xlsx create mode 100644 tmpl/excel_temp/xstb_ana.xlsx 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 0000000000000000000000000000000000000000..d3ed4849de61f5ef80f449a8121956e38359a068 GIT binary patch literal 9572 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 JE3!wv{|DYYpJo66 literal 0 HcmV?d00001 diff --git a/tmpl/excel_temp/xstb_ana.xlsx b/tmpl/excel_temp/xstb_ana.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..54fb05616e5819703af3787cfeda33ee4c490927 GIT binary patch literal 9602 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