From 2df24a5ff37c88d086f3e1f5b323a9fbcb99e271 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Sun, 4 Jan 2026 17:16:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E9=94=80=E9=87=8F?= =?UTF-8?q?=E4=B8=8B=E6=BB=91=E6=98=8E=E7=BB=86=E5=88=86=E6=9E=90=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/ding_talk_bot.go | 5 +- internal/tools/bbxt/api.go | 22 ++--- internal/tools/bbxt/bbxt.go | 89 ++++++++++++++++- internal/tools/bbxt/bbxt_test.go | 13 +++ internal/tools/bbxt/entitys.go | 15 +++ internal/tools/bbxt/excel.go | 2 + internal/tools/bbxt/upload.go | 162 +++++++++++++++++++++++++++++++ tmpl/excel_temp/xlxhmx.xlsx | Bin 0 -> 10143 bytes 8 files changed, 290 insertions(+), 18 deletions(-) create mode 100644 tmpl/excel_temp/xlxhmx.xlsx diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index f1583e9..6788449 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -504,7 +504,6 @@ func (d *DingTalkBotBiz) handleReport(ctx context.Context, rec *entitys.Recogniz if err != nil { log.Infof("时间识别失败:%s", configData.Time) entitys.ResText(rec.Ch, "", "时间识别失败了!可以给我一份比较具体的时间吗,例如“2025-12-31 12:00,抱歉抱歉😀") - return nil } } } @@ -537,7 +536,7 @@ func (d *DingTalkBotBiz) handleReport(ctx context.Context, rec *entitys.Recogniz reports = append(reports, repo) case "report_daily": product := strings.Split(group.ProductName, ",") - repo, _err := rep.DailyReport(t, product, nil) + repo, _err := rep.DailyReport(t, bbxt.DownWardValue, product, bbxt.SumFilter, nil) if _err != nil { return _err } @@ -636,7 +635,7 @@ func (d *DingTalkBotBiz) GetReportLists(ctx context.Context, group *model.AiBotG product = strings.Split(group.ProductName, ",") } //[]string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"} - reports, err = reportList.DailyReport(time.Now(), product, d.ossClient) + reports, err = reportList.DailyReport(time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, d.ossClient) if err != nil { return } diff --git a/internal/tools/bbxt/api.go b/internal/tools/bbxt/api.go index 8b6c823..a7b057b 100644 --- a/internal/tools/bbxt/api.go +++ b/internal/tools/bbxt/api.go @@ -113,20 +113,20 @@ func GetStatisOfficialProductSumApi(param *GetStatisOfficialProductSumRequest) ( } type GetStatisOfficialProductSumDeclineResponse struct { - OfficialProductSumDecline []*GetStatisOfficialProductSumDecline `protobuf:"bytes,1,rep,name=official_product_sum_decline,json=officialProductSumDecline,proto3" json:"official_product_sum_decline,omitempty"` - DataCount int32 `protobuf:"varint,2,opt,name=data_count,json=dataCount,proto3" json:"data_count,omitempty"` + OfficialProductSumDecline []*GetStatisOfficialProductSumDecline `protobuf:"bytes,1,rep,name=official_product_sum_decline,json=officialProductSumDecline,proto3" json:"officialProductSumDecline,omitempty"` + DataCount int32 `protobuf:"varint,2,opt,name=data_count,json=dataCount,proto3" json:"dataCount,omitempty"` } type GetStatisOfficialProductSumDecline struct { - ResellerId int32 `protobuf:"varint,1,opt,name=reseller_id,json=resellerId,proto3" json:"reseller_id,omitempty"` - OfficialProductId int32 `protobuf:"varint,2,opt,name=official_product_id,json=officialProductId,proto3" json:"official_product_id,omitempty"` - OfficialProductName string `protobuf:"bytes,3,opt,name=official_product_name,json=officialProductName,proto3" json:"official_product_name,omitempty"` - ResellerName string `protobuf:"bytes,4,opt,name=reseller_name,json=resellerName,proto3" json:"reseller_name,omitempty"` - CurrentNum int32 `protobuf:"varint,5,opt,name=current_num,json=currentNum,proto3" json:"current_num,omitempty"` - HistoryOneNum int32 `protobuf:"varint,6,opt,name=history_one_num,json=historyOneNum,proto3" json:"history_one_num,omitempty"` - HistoryTwoNum int32 `protobuf:"varint,7,opt,name=history_two_num,json=historyTwoNum,proto3" json:"history_two_num,omitempty"` - HistoryOneDiff int32 `protobuf:"varint,8,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"history_one_diff,omitempty"` - HistoryTwoDiff int32 `protobuf:"varint,9,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"history_two_diff,omitempty"` + ResellerId int32 `protobuf:"varint,1,opt,name=reseller_id,json=resellerId,proto3" json:"resellerId,omitempty"` + OfficialProductId int32 `protobuf:"varint,2,opt,name=official_product_id,json=officialProductId,proto3" json:"officialProductId,omitempty"` + OfficialProductName string `protobuf:"bytes,3,opt,name=official_product_name,json=officialProductName,proto3" json:"officialProductName,omitempty"` + ResellerName string `protobuf:"bytes,4,opt,name=reseller_name,json=resellerName,proto3" json:"resellerName,omitempty"` + CurrentNum int32 `protobuf:"varint,5,opt,name=current_num,json=currentNum,proto3" json:"currentNum,omitempty"` + HistoryOneNum int32 `protobuf:"varint,6,opt,name=history_one_num,json=historyOneNum,proto3" json:"historyOneNum,omitempty"` + HistoryTwoNum int32 `protobuf:"varint,7,opt,name=history_two_num,json=historyTwoNum,proto3" json:"historyTwoNum,omitempty"` + HistoryOneDiff int32 `protobuf:"varint,8,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"historyOneDiff,omitempty"` + HistoryTwoDiff int32 `protobuf:"varint,9,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"historyTwoDiff,omitempty"` } // GetStatisOfficialProductSumDeclineApi 销量同比分析 diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index 7c17166..27143dc 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -1,12 +1,12 @@ package bbxt import ( + pkginner "ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg/utils_oss" "ai_scheduler/pkg" "fmt" "math/rand" "slices" - "sort" "time" @@ -17,6 +17,11 @@ const ( GreenStyle = "${color: 00B050;horizontal:center;vertical:center;borderColor:#000000}" ) +var ( + DownWardValue int32 = 1500 + SumFilter int32 = -150 +) + var resellerBlackList = []string{ "悦跑", "电商-独立", @@ -46,7 +51,7 @@ func NewBbxtTools() (*BbxtTools, error) { }, nil } -func (b *BbxtTools) DailyReport(now time.Time, productName []string, ossClient *utils_oss.Client) (reports []*ReportRes, err error) { +func (b *BbxtTools) DailyReport(now time.Time, downWardValue int32, productName []string, sumFilter int32, ossClient *utils_oss.Client) (reports []*ReportRes, err error) { reports = make([]*ReportRes, 0, 4) productLossReport, err := b.StatisOursProductLossSum(now) if err != nil { @@ -60,8 +65,12 @@ func (b *BbxtTools) DailyReport(now time.Time, productName []string, ossClient * if err != nil { return } + statisOfficialProductSumDecline, err := b.GetStatisOfficialProductSumDecline(now, downWardValue, productName, sumFilter) + if err != nil { + return + } reports = append(reports, productLossReport...) - reports = append(reports, statisOfficialProductSum, profitRankingSum) + reports = append(reports, statisOfficialProductSum, profitRankingSum, statisOfficialProductSumDecline) if ossClient != nil { uploader := NewUploader(ossClient) @@ -145,12 +154,12 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes fmt.Sprintf("%s", v.ResellerName), fmt.Sprintf("%.2f", v.Total), }) + totalSum += v.Total } if v.Total <= -500 && !slices.Contains(resellerBlackList, v.ResellerName) { gt = append(gt, v) totalSum500 += v.Total } - totalSum += v.Total } report = make([]*ReportRes, 2) timeCh := now.Format("1月2日15点") @@ -314,6 +323,78 @@ func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []str }, err } +// GetStatisOfficialProductSumDecline 销量下滑明细 +func (b *BbxtTools) GetStatisOfficialProductSumDecline(now time.Time, downWardValue int32, productName []string, sumFilter int32) (report *ReportRes, err error) { + + var productMap = make(map[string]int) + for k, v := range productName { + productMap[v] = k + } + ct := []string{ + time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"), + adjustedTime(now), + } + var ids []int32 + if len(productName) > 0 { + ids, err = b.getProductIdFromProductName(productName) + if err != nil { + return + } + } + + reqParam := &GetStatisOfficialProductSumRequest{ + Ct: ct, + DownwardValue: downWardValue, + OfficialProductId: ids, + Page: 1, + Limit: 1000, + } + + data, err := GetStatisOfficialProductSumDeclineApi(reqParam) + if err != nil { + return + } + var ( + productSumMap = make(map[int32]ProductSumDecline) + ) + for _, v := range data.OfficialProductSumDecline { + if _, ex := productSumMap[v.OfficialProductId]; !ex { + productSumMap[v.OfficialProductId] = ProductSumDecline{ + OfficialProductName: v.OfficialProductName, + OfficialProductId: v.OfficialProductId, + ProductSumReseller: make(map[int32]ProductSumReseller), + } + + } + if v.HistoryOneDiff <= sumFilter || v.HistoryTwoDiff <= sumFilter { + productSumMap[v.OfficialProductId].ProductSumReseller[v.ResellerId] = ProductSumReseller{ + ResellerName: v.ResellerName, + CurrentNum: v.CurrentNum, + HistoryOneNum: v.HistoryOneNum, + HistoryOneDiff: v.HistoryOneDiff, + HistoryTwoNum: v.HistoryTwoNum, + HistoryTwoDiff: v.HistoryTwoDiff, + } + } + + } + timeCh := now.Format("1月2日15点") + //title := "截至" + timeCh + "销量下滑大于" + fmt.Sprintf("%d", downWardValue) + "明细,分销商仅展示差额大于" + fmt.Sprintf("%d", -sumFilter) + title := "截至" + timeCh + "点销量下滑较大商品" + //总量生成excel + if len(productSumMap) == 0 { + return + } + filePath := b.cacheDir + "/xlxhmx" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx" + err = b.OfficialProductSumDeclineExcel(b.excelTempDir+"/"+"/xlxhmx.xlsx", filePath, productSumMap, title) + return &ReportRes{ + ReportName: "销售下滑明细", + Title: title, + Path: filePath, + Desc: pkginner.JsonStringIgonErr(productSumMap), + }, err +} + func (b *BbxtTools) getProductIdFromProductName(productNames []string) ([]int32, error) { data, err := GetStatisFilterOfficialProductApi(&GetStatisFilterOfficialProductRequest{}) if err != nil { diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go index ca0fe68..3640dcd 100644 --- a/internal/tools/bbxt/bbxt_test.go +++ b/internal/tools/bbxt/bbxt_test.go @@ -55,6 +55,19 @@ func Test_GetProfitRankingSum(t *testing.T) { } +func Test_GetStatisOfficialProductSumDecline(t *testing.T) { + o, err := NewBbxtTools() + if err != nil { + panic(err) + } + s := "官方--美团外卖红包5元,官方--美团外卖红包10元,官方--饿了么超级会员月卡,官方--网易云黑胶vip月卡,官方--喜马拉雅巅峰会员月卡,官方--芒果-PC季卡,官方--芒果-PC月卡,官方--芒果-PC周卡,官方--腾讯-周卡,官方--优酷周卡,官方--QQ音乐-绿钻月卡,官方--爱奇艺-周卡,官方--腾讯-月卡,官方--腾讯-季卡,官方--腾讯-年卡,官方--优酷月卡,官方--优酷季卡,官方--优酷年卡,官方--爱奇艺-月卡,官方--爱奇艺-季卡,官方--爱奇艺-年卡" + //s := "官方--QQ音乐-绿钻月卡" + report, err := o.GetStatisOfficialProductSumDecline(time.Now(), 1000, strings.Split(s, ","), -150) + + t.Log(report, err) + +} + func Test_GetStatisOfficialProductSum(t *testing.T) { o, err := NewBbxtTools() if err != nil { diff --git a/internal/tools/bbxt/entitys.go b/internal/tools/bbxt/entitys.go index d886dd6..b9bdf8f 100644 --- a/internal/tools/bbxt/entitys.go +++ b/internal/tools/bbxt/entitys.go @@ -21,3 +21,18 @@ type ReportRes struct { Data [][]string Desc string } + +type ProductSumDecline struct { + OfficialProductId int32 + OfficialProductName string + ProductSumReseller map[int32]ProductSumReseller +} + +type ProductSumReseller struct { + ResellerName string + CurrentNum int32 //今日成功数量 + HistoryOneNum int32 //昨日成功数量 + HistoryOneDiff int32 //同比昨日当前增减量 + HistoryTwoNum int32 //上周成功数量 + HistoryTwoDiff int32 //同比上周当前增减量 +} diff --git a/internal/tools/bbxt/excel.go b/internal/tools/bbxt/excel.go index c1034c4..19550cd 100644 --- a/internal/tools/bbxt/excel.go +++ b/internal/tools/bbxt/excel.go @@ -308,3 +308,5 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d // 6. 保存 return f.SaveAs(outputPath) } + +// OfficialProductSumDeclineExcel diff --git a/internal/tools/bbxt/upload.go b/internal/tools/bbxt/upload.go index 90277e3..9eeb8a5 100644 --- a/internal/tools/bbxt/upload.go +++ b/internal/tools/bbxt/upload.go @@ -9,6 +9,8 @@ import ( "net/http" "os" "path/filepath" + "regexp" + "sort" "strings" "github.com/gofiber/fiber/v2/log" @@ -170,3 +172,163 @@ func (u *Uploader) uploadToOSS(fileName string, fileBytes []byte) string { // } // return url //} + +func (b *BbxtTools) OfficialProductSumDeclineExcel(templatePath, outputPath string, sumMap map[int32]ProductSumDecline, title string) error { + // 1. 读取模板 + f, err := excelize.OpenFile(templatePath) + if err != nil { + return err + } + defer f.Close() + + sheet := f.GetSheetName(0) + if len(title) > 0 { + // 写入标题 + f.SetCellValue(sheet, "A1", title) + } + // ---------------- 样式获取 ---------------- + // 模板第2行:数据行样式 + tplRowData := 3 + styleA2, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowData)) + if err != nil { + styleA2 = 0 + } + // B2和C2通常样式一致,这里取B2作为明细列样式 + styleB2, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowData)) + if err != nil { + styleB2 = 0 + } + styleC2, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowData)) + if err != nil { + styleC2 = 0 + } + styleD2, err := f.GetCellStyle(sheet, fmt.Sprintf("D%d", tplRowData)) + if err != nil { + styleC2 = 0 + } + //styleE2, err := f.GetCellStyle(sheet, fmt.Sprintf("E%d", tplRowData)) + //if err != nil { + // styleC2 = 0 + //} + styleF2, err := f.GetCellStyle(sheet, fmt.Sprintf("F%d", tplRowData)) + if err != nil { + styleC2 = 0 + } + //styleG2, err := f.GetCellStyle(sheet, fmt.Sprintf("G%d", tplRowData)) + //if err != nil { + // styleC2 = 0 + //} + + rowHeightData, err := f.GetRowHeight(sheet, tplRowData) + if err != nil { + rowHeightData = 20 + } + + currentRow := 3 + pattern := `\$\{(.*?)\}` + re := regexp.MustCompile(pattern) + for _, product := range sumMap { + // 排序 ProductLoss + var reseller []ProductSumReseller + for _, p := range product.ProductSumReseller { + reseller = append(reseller, p) + } + sort.Slice(reseller, func(i, j int) bool { + return reseller[i].HistoryOneDiff < reseller[j].HistoryOneDiff + }) + + startRow := currentRow + + // 填充该经销商的所有产品 + for _, p := range reseller { + // 设置行高 + var ( + oneDiff string + twoDiff string + ) + f.SetRowHeight(sheet, currentRow, rowHeightData) + if p.HistoryOneDiff >= 0 { + oneDiff = fmt.Sprintf("%s↑%d", RedStyle, p.HistoryOneDiff) + } else { + oneDiff = fmt.Sprintf("%s↓%d", GreenStyle, p.HistoryOneDiff) + } + if p.HistoryTwoDiff >= 0 { + twoDiff = fmt.Sprintf("%s↑%d", RedStyle, p.HistoryTwoDiff) + } else { + twoDiff = fmt.Sprintf("%s↓%d", GreenStyle, p.HistoryTwoDiff) + } + matchesE := re.FindStringSubmatch(oneDiff) + styleMap := make(map[string]string) + + if len(matchesE) != 2 { + continue + } + for _, kv := range strings.Split(matchesE[1], ";") { + kvParts := strings.Split(kv, ":") + if len(kvParts) == 2 { + styleMap[strings.TrimSpace(kvParts[0])] = strings.TrimSpace(kvParts[1]) + } + } + fontStyleIDE, _err := SetStyle(styleMap, f) + if _err != nil { + log.Errorf("set style failed: %v", _err) + } + f.SetCellStyle(sheet, fmt.Sprintf("E%d", currentRow), fmt.Sprintf("E%d", currentRow), fontStyleIDE) + oneDiffValue := re.ReplaceAllString(oneDiff, "") + matchesG := re.FindStringSubmatch(twoDiff) + styleMapG := make(map[string]string) + if len(matchesG) != 2 { + continue + } + for _, kv := range strings.Split(matchesG[1], ";") { + kvParts := strings.Split(kv, ":") + if len(kvParts) == 2 { + styleMapG[strings.TrimSpace(kvParts[0])] = strings.TrimSpace(kvParts[1]) + } + } + fontStyleIDG, _err := SetStyle(styleMapG, f) + if _err != nil { + log.Errorf("set style failed: %v", _err) + } + twoDiffValue := re.ReplaceAllString(twoDiff, "") + f.SetCellStyle(sheet, fmt.Sprintf("G%d", currentRow), fmt.Sprintf("G%d", currentRow), fontStyleIDG) + // 设置值 + f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), product.OfficialProductName) + f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.ResellerName) + f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), p.CurrentNum) + f.SetCellValue(sheet, fmt.Sprintf("D%d", currentRow), p.HistoryOneNum) + f.SetCellValue(sheet, fmt.Sprintf("E%d", currentRow), oneDiffValue) + f.SetCellValue(sheet, fmt.Sprintf("F%d", currentRow), p.HistoryTwoNum) + f.SetCellValue(sheet, fmt.Sprintf("G%d", currentRow), twoDiffValue) + + // 设置样式 + if styleA2 != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleA2) + } + if styleB2 != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleB2) + } + if styleC2 != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleC2) + } + if styleD2 != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("D%d", currentRow), fmt.Sprintf("D%d", currentRow), styleC2) + } + + if styleF2 != 0 { + f.SetCellStyle(sheet, fmt.Sprintf("F%d", currentRow), fmt.Sprintf("F%d", currentRow), styleC2) + } + + currentRow++ + } + + endRow := currentRow - 1 + // 合并单元格 (如果多于1行) + if endRow > startRow { + f.MergeCell(sheet, fmt.Sprintf("A%d", startRow), fmt.Sprintf("A%d", endRow)) + } + } + + // 6. 保存 + return f.SaveAs(outputPath) +} diff --git a/tmpl/excel_temp/xlxhmx.xlsx b/tmpl/excel_temp/xlxhmx.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..fbfc23c16a89d665a14a2ba3980697ae0f5aaa18 GIT binary patch literal 10143 zcma)i1yo$i(lzeRAi>==xI4jv6Ep<(!QCA)cyM>O;I6?TxDzyJ2<`-aNZz}-H}8FK z{k;}*W-+y^&e=U((_OU{Wg(#uz@A5l!iMnk_1^>j>5UQ4P|+S}>%gq|WCnQp2i#Ay z7OsU#7H}{yb_g&qw7;9Zv$bV%wYEx+>5_qD1xQ@^KcamEvN4h=zohjK<%897B4h(D zcFJMI?u_3uGC7i7!`hq!Rqv75Fvp7$Bv5&C@N z$IZ#%1ryUc!|h&qV$ZJP0VL}lq|!H-7XM!K2;J`dKD=%>HK`~cu-Czz0%G0r-0wNS z3ev^eA2u5IG`Q|=Y)+v6$*${Wb)eD{yEaekqW_&;L!iC!GreB1Z(sM206wkC-=iQO zF^*R_km}+*8og#>t(M%x_iKrGV+Lu~0A#jqXp-0@Pjm1|a}cYZ-{ZZ7I8Zg*4yA&U z&=)6
~0>fue?adjUw|`RzQg>0^rgyc=G5pZgL8Ms82P