feat: 添加利润排行和商品统计功能

This commit is contained in:
renzhiyuan 2025-12-30 16:49:53 +08:00
parent 6fa91e6e43
commit c77e6da296
6 changed files with 348 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.