fix: 优化负利润分析报表处理逻辑
This commit is contained in:
parent
03652cb588
commit
6c987b15db
|
|
@ -27,6 +27,7 @@ lsxd:
|
|||
login_url: "https://api.user.1688sup.com/v1/login/phone"
|
||||
phone: "ORlviZN7N06W2+WKLe76xg=="
|
||||
password: "V5Uh8C4bamEM6UQZh4TCeQ=="
|
||||
code: "456789"
|
||||
check_token_url: "https://api.user.1688sup.com/v1/user/welcome"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,10 +26,14 @@ coze:
|
|||
|
||||
lsxd:
|
||||
# 统一登录
|
||||
login_url: "https://api.user.1688sup.com/v1/login/phone"
|
||||
phone: "ORlviZN7N06W2+WKLe76xg=="
|
||||
password: "V5Uh8C4bamEM6UQZh4TCeQ=="
|
||||
check_token_url: "https://api.user.1688sup.com/v1/user/welcome"
|
||||
login_url: "http://api.test.user.1688sup.com/v1/login/phone"
|
||||
phone: "OFJ8UpqOlI7+w3Qklf36ZA=="
|
||||
password: "tEbFegH/DRRh6LutFb7o3g=="
|
||||
code: "123456"
|
||||
check_token_url: "http://api.test.user.1688sup.com/v1/user/welcome"
|
||||
|
||||
zltx:
|
||||
req_url: "https://gateway.dev.cdlsxd.cn/zltx_api"
|
||||
|
||||
sys:
|
||||
session_len: 6
|
||||
|
|
@ -41,7 +45,7 @@ redis:
|
|||
host: 47.97.27.195:6379
|
||||
type: node
|
||||
pass: lansexiongdi@666
|
||||
key: report-api-test
|
||||
key: ai_scheduler-test
|
||||
pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池
|
||||
minIdleConns: 2 #最小空闲连接数
|
||||
maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ type DingTalkBotBiz struct {
|
|||
cardSend *dingtalk.SendCardClient
|
||||
qywxGroupHandle *qywx.Group
|
||||
groupConfigBiz *GroupConfigBiz
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||
}
|
||||
|
||||
// NewDingTalkBotBiz
|
||||
|
|
@ -54,6 +55,7 @@ func NewDingTalkBotBiz(
|
|||
botGroupImpl *impl.BotGroupImpl,
|
||||
dingTalkUser *dingtalk.User,
|
||||
chatHis *impl.BotChatHisImpl,
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||
toolManager *tools.Manager,
|
||||
conf *config.Config,
|
||||
cardSend *dingtalk.SendCardClient,
|
||||
|
|
@ -71,6 +73,7 @@ func NewDingTalkBotBiz(
|
|||
chatHis: chatHis,
|
||||
conf: conf,
|
||||
cardSend: cardSend,
|
||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,6 +200,49 @@ func (d *DingTalkBotBiz) Macro(ctx context.Context, requireData *entitys.Require
|
|||
return
|
||||
}
|
||||
|
||||
if strings.Contains(content, "[负利润分析]获取") {
|
||||
var (
|
||||
data model.AiReportDailyCache
|
||||
value map[int32]*bbxt.ResellerLossSumProductRelation
|
||||
)
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"`index`": bbxt.IndexLossSumDetail})
|
||||
cond = cond.And(builder.Eq{"`key`": time.Now().Format(time.DateOnly)})
|
||||
err = d.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &data)
|
||||
if err != nil {
|
||||
entitys.ResText(requireData.Ch, "", "获取失败")
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(data.Value), &value)
|
||||
if err != nil {
|
||||
entitys.ResText(requireData.Ch, "", "获取失败,格式解析错误")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(content, "[负利润分析]更新") {
|
||||
// 提取冒号后的内容
|
||||
if len(groupConfig.ProductName) == 0 {
|
||||
entitys.ResText(requireData.Ch, "", "暂未设置")
|
||||
} else {
|
||||
entitys.ResText(requireData.Ch, "", groupConfig.ProductName)
|
||||
isFinish = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(content, "[负利润分析]同步") {
|
||||
// 提取冒号后的内容
|
||||
if len(groupConfig.ProductName) == 0 {
|
||||
entitys.ResText(requireData.Ch, "", "暂未设置")
|
||||
} else {
|
||||
entitys.ResText(requireData.Ch, "", groupConfig.ProductName)
|
||||
isFinish = true
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@ import (
|
|||
"ai_scheduler/internal/domain/workflow/recharge"
|
||||
"ai_scheduler/internal/domain/workflow/runtime"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/internal/pkg/lsxd"
|
||||
"ai_scheduler/internal/pkg/utils_oss"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/internal/tools/bbxt"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -31,11 +35,13 @@ import (
|
|||
// AiRouterBiz 智能路由服务
|
||||
type GroupConfigBiz struct {
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||
ossClient *utils_oss.Client
|
||||
workflowManager *runtime.Registry
|
||||
botTools []model.AiBotTool
|
||||
toolManager *tools.Manager
|
||||
conf *config.Config
|
||||
rdb *utils.Rdb
|
||||
}
|
||||
|
||||
// NewDingTalkBotBiz
|
||||
|
|
@ -45,6 +51,8 @@ func NewGroupConfigBiz(
|
|||
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
||||
workflowManager *runtime.Registry,
|
||||
conf *config.Config,
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||
rdb *utils.Rdb,
|
||||
) *GroupConfigBiz {
|
||||
return &GroupConfigBiz{
|
||||
botTools: tools.BootTools,
|
||||
|
|
@ -52,6 +60,8 @@ func NewGroupConfigBiz(
|
|||
botGroupConfigImpl: botGroupConfigImpl,
|
||||
workflowManager: workflowManager,
|
||||
conf: conf,
|
||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||
rdb: rdb,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,12 +82,12 @@ func (g *GroupConfigBiz) GetReportLists(ctx context.Context, groupConfig *model.
|
|||
product = strings.Split(groupConfig.ProductName, ",")
|
||||
}
|
||||
|
||||
reportList, err := bbxt.NewBbxtTools(g.conf)
|
||||
reportList, err := bbxt.NewBbxtTools(g.conf, lsxd.NewLogin(g.conf, g.rdb))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
reports, err = reportList.DailyReport(time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, g.ossClient)
|
||||
reports, err = reportList.DailyReport(ctx, time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, g.ossClient, g.GetReportCache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -145,7 +155,8 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
|
|||
}
|
||||
}
|
||||
}
|
||||
rep, err := bbxt.NewBbxtTools(g.conf)
|
||||
|
||||
rep, err := bbxt.NewBbxtTools(g.conf, lsxd.NewLogin(g.conf, g.rdb))
|
||||
uploader := bbxt.NewUploader(g.ossClient, g.conf)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -153,7 +164,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
|
|||
var reports []*bbxt.ReportRes
|
||||
switch rec.Match.Index {
|
||||
case "report_loss_analysis":
|
||||
repo, _err := rep.StatisOursProductLossSum(t)
|
||||
repo, _err := rep.StatisOursProductLossSum(ctx, t, g.GetReportCache)
|
||||
if _err != nil {
|
||||
return _err
|
||||
}
|
||||
|
|
@ -174,7 +185,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
|
|||
reports = append(reports, repo)
|
||||
case "report_daily":
|
||||
product := strings.Split(groupConfig.ProductName, ",")
|
||||
repo, _err := rep.DailyReport(t, bbxt.DownWardValue, product, bbxt.SumFilter, nil)
|
||||
repo, _err := rep.DailyReport(ctx, t, bbxt.DownWardValue, product, bbxt.SumFilter, nil, g.GetReportCache)
|
||||
if _err != nil {
|
||||
return _err
|
||||
}
|
||||
|
|
@ -404,3 +415,47 @@ func (g *GroupConfigBiz) otherTask(ctx context.Context, rec *entitys.Recognize)
|
|||
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
|
||||
return
|
||||
}
|
||||
|
||||
func (g *GroupConfigBiz) GetReportCache(ctx context.Context, day time.Time, totalDetail []*bbxt.ResellerLoss, bbxtObj *bbxt.BbxtTools) error {
|
||||
var ResellerProductRelation map[int32]*bbxt.ResellerLossSumProductRelation
|
||||
|
||||
dayDate := day.Format(time.DateOnly)
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"index": bbxt.IndexLossSumDetail})
|
||||
cond = cond.And(builder.Eq{"key": dayDate})
|
||||
var cache model.AiReportDailyCache
|
||||
err := g.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache)
|
||||
if err != nil {
|
||||
if errors.Is(sql.ErrNoRows, err) {
|
||||
ResellerProductRelation, err = bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cache = model.AiReportDailyCache{
|
||||
Key: dayDate,
|
||||
Index: bbxt.IndexLossSumDetail,
|
||||
Value: pkg.JsonStringIgonErr(ResellerProductRelation),
|
||||
}
|
||||
_, err = g.reportDailyCacheImpl.Add(&cache)
|
||||
}
|
||||
} else {
|
||||
err = json.Unmarshal([]byte(cache.Value), &ResellerProductRelation)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range totalDetail {
|
||||
if _, ex := ResellerProductRelation[v.ResellerId]; !ex {
|
||||
continue
|
||||
}
|
||||
v.Manager = ResellerProductRelation[v.ResellerId].AfterSaleName
|
||||
for _, vv := range v.ProductLoss {
|
||||
if _, ex := ResellerProductRelation[v.ResellerId].Products[vv.ProductId]; !ex {
|
||||
continue
|
||||
}
|
||||
vv.LossReason = ResellerProductRelation[v.ResellerId].Products[vv.ProductId].LossReason
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ type Config struct {
|
|||
LLM LLM `mapstructure:"llm"`
|
||||
Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
|
||||
Qywx QywxConfig `mapstructure:"qywx"`
|
||||
ZLTX ZLTX `mapstructure:"zltx"`
|
||||
}
|
||||
|
||||
type ZLTX struct {
|
||||
ReqUrl string `mapstructure:"req_url"`
|
||||
}
|
||||
|
||||
type SysPrompt struct {
|
||||
|
|
@ -136,6 +141,7 @@ type LSXDConfig struct {
|
|||
LoginURL string `mapstructure:"login_url"`
|
||||
Phone string `mapstructure:"phone"`
|
||||
Password string `mapstructure:"password"`
|
||||
Code string `mapstructure:"code"`
|
||||
CheckTokenURL string `mapstructure:"check_token_url"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,4 +17,5 @@ var ProviderImpl = wire.NewSet(
|
|||
NewBotGroupImpl,
|
||||
NewBotGroupConfigImpl,
|
||||
NewBotGroupQywxImpl,
|
||||
NewReportDailyCacheImpl,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
)
|
||||
|
||||
type ReportDailyCacheImpl struct {
|
||||
dataTemp.DataTemp
|
||||
}
|
||||
|
||||
func NewReportDailyCacheImpl(db *utils.Db) *ReportDailyCacheImpl {
|
||||
return &ReportDailyCacheImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiReportDailyCache)),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
const TableNameAiReportDailyCache = "ai_report_daily_cache"
|
||||
|
||||
// AiReportDailyCache mapped from table <ai_report_daily_cache>
|
||||
type AiReportDailyCache struct {
|
||||
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
||||
Key string `gorm:"column:key;not null;default:1;comment:索引方式,可以是任意类型" json:"key"` // 索引方式,可以是任意类型
|
||||
Value string `gorm:"column:value;comment:类型下所需路由以及参数" json:"value"` // 类型下所需路由以及参数
|
||||
Index string `gorm:"column:index;not null;comment:类型" json:"index"` // 类型
|
||||
}
|
||||
|
||||
// TableName AiReportDailyCache's table name
|
||||
func (*AiReportDailyCache) TableName() string {
|
||||
return TableNameAiReportDailyCache
|
||||
}
|
||||
|
|
@ -95,30 +95,18 @@ func (l *Login) checkTokenValid(ctx context.Context, token string) bool {
|
|||
|
||||
// 调用登录接口获取token
|
||||
func (l *Login) login(ctx context.Context) (string, error) {
|
||||
// 1.获取配置
|
||||
loginURL := l.config.LSXD.LoginURL
|
||||
phone := l.config.LSXD.Phone
|
||||
password := l.config.LSXD.Password
|
||||
|
||||
// 2.调用登录接口获取token
|
||||
if loginURL == "" {
|
||||
return "", errors.New("login url is empty")
|
||||
}
|
||||
if phone == "" || password == "" {
|
||||
return "", errors.New("phone or password is empty")
|
||||
}
|
||||
|
||||
reqBody := map[string]any{
|
||||
"phone": phone,
|
||||
"password": password,
|
||||
"code": "456789",
|
||||
"phone": l.config.LSXD.Phone,
|
||||
"password": l.config.LSXD.Password,
|
||||
"code": l.config.LSXD.Code,
|
||||
}
|
||||
bodyBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
status, respBody, err := l.doRequestWithBody(ctx, http.MethodPost, loginURL, "", "application/json", bodyBytes)
|
||||
status, respBody, err := l.doRequestWithBody(ctx, http.MethodPost, l.config.LSXD.LoginURL, "", "application/json", bodyBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -158,7 +146,7 @@ func (l *Login) login(ctx context.Context) (string, error) {
|
|||
}
|
||||
|
||||
func (l *Login) getCachedToken(ctx context.Context) (string, error) {
|
||||
token, err := l.redisCli.Get(ctx, constants.CACHE_KEY_LSXD_TOKEN).Result()
|
||||
token, err := l.redisCli.Get(ctx, l.getCacheKey()).Result()
|
||||
if err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
|
@ -168,11 +156,16 @@ func (l *Login) getCachedToken(ctx context.Context) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
func (l *Login) getCacheKey() string {
|
||||
return l.config.Redis.Key + constants.CACHE_KEY_LSXD_TOKEN + l.config.LSXD.Phone // 1.获取配置
|
||||
|
||||
}
|
||||
|
||||
func (l *Login) cacheToken(ctx context.Context, token string) error {
|
||||
if token == "" {
|
||||
return errors.New("token is empty")
|
||||
}
|
||||
return l.redisCli.Set(ctx, constants.CACHE_KEY_LSXD_TOKEN, token, constants.EXPIRE_LSXD_TOKEN).Err()
|
||||
return l.redisCli.Set(ctx, l.getCacheKey(), token, constants.EXPIRE_LSXD_TOKEN).Err()
|
||||
}
|
||||
|
||||
func (l *Login) doRequest(ctx context.Context, method string, url string, authorization string, body []byte) (int, error) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package util
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StructToMap 将结构体转换为 map[string]any
|
||||
func StructToMap(v any) (map[string]any, error) {
|
||||
|
|
@ -12,3 +16,28 @@ func StructToMap(v any) (map[string]any, error) {
|
|||
err = json.Unmarshal(b, &m)
|
||||
return m, err
|
||||
}
|
||||
|
||||
func StructToMapWithReflect(obj interface{}) map[string]interface{} {
|
||||
val := reflect.ValueOf(obj)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
valueField := val.Field(i)
|
||||
typeField := val.Type().Field(i)
|
||||
jsonTag := typeField.Tag.Get("json")
|
||||
if idx := strings.Index(jsonTag, ","); idx != -1 {
|
||||
jsonTag = jsonTag[:idx]
|
||||
}
|
||||
if !typeField.IsExported() {
|
||||
continue
|
||||
}
|
||||
data[jsonTag] = valueField.Interface()
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package bbxt
|
|||
import (
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
|
@ -29,12 +31,23 @@ type StatisOursProductLossSumResponse struct {
|
|||
}
|
||||
|
||||
const Base = "https://reportapi.1688sup.com/api"
|
||||
const AuthUrl = "http://test.analysis.com/api"
|
||||
|
||||
// StatisOursProductLossSumApi 负利润分析
|
||||
func StatisOursProductLossSumApi(param *StatisOursProductLossSumReq) (*StatisOursProductLossSumRes, error) {
|
||||
url := "/dataanalytics/statisOursProductLossSum"
|
||||
var res StatisOursProductLossSumRes
|
||||
if err := request(url, param, &res); err != nil {
|
||||
if err := request(url, param, &res, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// StatisOursProductLossSumApi 负利润分析
|
||||
func StatisOursProductLossSumApiWithAuth(param *StatisOursProductLossSumReq, token string) (*StatisOursProductLossSumRes, error) {
|
||||
url := "/dataanalytics/statisOursProductLossSum"
|
||||
var res StatisOursProductLossSumRes
|
||||
if err := request(url, param, &res, token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
|
|
@ -73,7 +86,7 @@ type ProfitRankingSumResponse struct {
|
|||
func GetProfitRankingSumApi(param *GetProfitRankingSumRequest) (*GetProfitRankingSumResponse, error) {
|
||||
url := "/dataanalytics/profitRankingSum"
|
||||
var res GetProfitRankingSumResponse
|
||||
if err := request(url, param, &res); err != nil {
|
||||
if err := request(url, param, &res, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
|
|
@ -106,7 +119,7 @@ type GetStatisOfficialProductSum struct {
|
|||
func GetStatisOfficialProductSumApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumResponse, error) {
|
||||
url := "/dataanalytics/statisOfficialProduct"
|
||||
var res GetStatisOfficialProductSumResponse
|
||||
if err := request(url, param, &res); err != nil {
|
||||
if err := request(url, param, &res, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
|
|
@ -133,7 +146,7 @@ type GetStatisOfficialProductSumDecline struct {
|
|||
func GetStatisOfficialProductSumDeclineApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumDeclineResponse, error) {
|
||||
url := "/dataanalytics/statisOfficialProductDecline"
|
||||
var res GetStatisOfficialProductSumDeclineResponse
|
||||
if err := request(url, param, &res); err != nil {
|
||||
if err := request(url, param, &res, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
|
|
@ -162,23 +175,119 @@ type StatisFilterOfficialProductResponse struct {
|
|||
func GetStatisFilterOfficialProductApi(param *GetStatisFilterOfficialProductRequest) (*GetStatisFilterOfficialProductResponse, error) {
|
||||
url := "/dataanalytics/statisFilterOfficialProduct"
|
||||
var res GetStatisFilterOfficialProductResponse
|
||||
if err := request(url, param, &res); err != nil {
|
||||
if err := request(url, param, &res, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func request(url string, reqData interface{}, resData interface{}) error {
|
||||
//type GetManagerAndDefaultLossReasonRequest struct {
|
||||
// ResellerId int32 ` json:"reseller_id"`
|
||||
// GoodsIds int32 ` json:"reseller_id"`
|
||||
//}
|
||||
|
||||
type GetManagerAndDefaultLossReasonRequest struct {
|
||||
Param map[int32]map[string][]int32 ` json:"param"`
|
||||
}
|
||||
|
||||
type GetManagerAndDefaultLossReasonResponse struct {
|
||||
Res []*GetManagerAndDefaultLossReasonResponseList `json:"res,omitempty"`
|
||||
}
|
||||
|
||||
type GetManagerAndDefaultLossReasonResponseList struct {
|
||||
ResellerInfo *GetManagerAndDefaultLossReasonResponse_ResellerInfo `json:"GetManagerAndDefaultLossReasonResponse_ResellerInfo,omitempty"`
|
||||
ProductList []*GetManagerAndDefaultLossReasonResponse_ProductList `json:"GetManagerAndDefaultLossReasonResponse_ProductList,omitempty"`
|
||||
}
|
||||
|
||||
type GetManagerAndDefaultLossReasonResponse_ResellerInfo struct {
|
||||
ResellerId int32 `json:"reseller_id,omitempty"`
|
||||
AfterSaleName string `json:"after_sale_name,omitempty"`
|
||||
}
|
||||
|
||||
type GetManagerAndDefaultLossReasonResponse_ProductList struct {
|
||||
ProductId int32 `json:"product_id,omitempty"`
|
||||
LossReason string `json:"loss_reason,omitempty"`
|
||||
}
|
||||
|
||||
// GetStatisFilterOfficialProductApi 官方商品列表
|
||||
func GetManagerAndDefaultLossReasonApi(param *GetManagerAndDefaultLossReasonRequest, token string, reqUrl string) ([]*GetManagerAndDefaultLossReasonResponseList, error) {
|
||||
return []*GetManagerAndDefaultLossReasonResponseList{
|
||||
{
|
||||
ResellerInfo: &GetManagerAndDefaultLossReasonResponse_ResellerInfo{
|
||||
ResellerId: 25009,
|
||||
AfterSaleName: "张三",
|
||||
},
|
||||
ProductList: []*GetManagerAndDefaultLossReasonResponse_ProductList{
|
||||
{
|
||||
ProductId: 129,
|
||||
LossReason: "小米钱包h5-QQ音乐绿钻季卡原因",
|
||||
},
|
||||
{
|
||||
ProductId: 2218,
|
||||
LossReason: "小米钱包h5-百度网盘新vip会员月卡原因",
|
||||
},
|
||||
{
|
||||
ProductId: 3226,
|
||||
LossReason: "小米钱包h5-腾讯视频月卡-小米钱包2024原因",
|
||||
},
|
||||
{
|
||||
ProductId: 3364,
|
||||
LossReason: "小米钱包h5-腾讯视频月卡-0元直充原因",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
reqParam, err := util.StructToMap(param)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &l_request.Request{
|
||||
Url: reqUrl + "/admin/reseller/resellerAuthProduct/getManagerAndDefaultLossReason",
|
||||
Method: http.MethodPost,
|
||||
Json: reqParam,
|
||||
Headers: map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", token),
|
||||
},
|
||||
}
|
||||
res, err := req.Send()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("request failed, status code: %d,resion: %s", res.StatusCode, res.Reason)
|
||||
}
|
||||
var code resCode
|
||||
if err = json.Unmarshal(res.Content, &code); err != nil {
|
||||
return nil, fmt.Errorf("返回结构异常:%s", string(res.Content))
|
||||
}
|
||||
var resData []*GetManagerAndDefaultLossReasonResponseList
|
||||
if err = json.Unmarshal(code.Data, &resData); err != nil {
|
||||
return nil, fmt.Errorf("返回数据异常:%s", string(res.Content))
|
||||
}
|
||||
return resData, nil
|
||||
}
|
||||
|
||||
func request(url string, reqData interface{}, resData interface{}, token string) error {
|
||||
requestSchema := Base
|
||||
if len(token) > 0 {
|
||||
requestSchema = AuthUrl
|
||||
}
|
||||
reqParam, err := pkg.StructToURLValues(reqData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &l_request.Request{
|
||||
Url: FormatPHPURL(Base+url, reqParam),
|
||||
Url: FormatPHPURL(requestSchema+url, reqParam),
|
||||
Method: http.MethodGet,
|
||||
}
|
||||
if len(token) > 0 {
|
||||
req.Headers = map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", token),
|
||||
}
|
||||
}
|
||||
res, err := req.Send()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package bbxt
|
|||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
pkginner "ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/lsxd"
|
||||
"ai_scheduler/internal/pkg/utils_oss"
|
||||
"ai_scheduler/pkg"
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"slices"
|
||||
|
|
@ -18,18 +20,17 @@ const (
|
|||
GreenStyle = "${color: 008000;horizontal:center;vertical:center;borderColor:#000000}"
|
||||
)
|
||||
|
||||
const (
|
||||
IndexLossSumDetail = "lossSumDetail"
|
||||
)
|
||||
|
||||
type LossSumInitFunc func(ctx context.Context, day time.Time, totalDetail []*ResellerLoss, selfObj *BbxtTools) error
|
||||
|
||||
var (
|
||||
DownWardValue int32 = 1000
|
||||
SumFilter int32 = -150
|
||||
)
|
||||
|
||||
var resellerBlackList = []string{
|
||||
"悦跑",
|
||||
"电商-独立",
|
||||
"蓝星严选连续包月",
|
||||
"通钱-2025年12月",
|
||||
}
|
||||
|
||||
var resellerBlackListProduct = []string{
|
||||
"悦跑",
|
||||
"电商-独立",
|
||||
|
|
@ -43,9 +44,10 @@ type BbxtTools struct {
|
|||
excelTempDir string
|
||||
ossClient *utils_oss.Client
|
||||
config *config.Config
|
||||
login *lsxd.Login
|
||||
}
|
||||
|
||||
func NewBbxtTools(config *config.Config) (*BbxtTools, error) {
|
||||
func NewBbxtTools(config *config.Config, login *lsxd.Login) (*BbxtTools, error) {
|
||||
cache, err := pkg.GetCacheDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -59,12 +61,21 @@ func NewBbxtTools(config *config.Config) (*BbxtTools, error) {
|
|||
cacheDir: cache,
|
||||
excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir),
|
||||
config: config,
|
||||
login: login,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BbxtTools) DailyReport(now time.Time, downWardValue int32, productName []string, sumFilter int32, ossClient *utils_oss.Client) (reports []*ReportRes, err error) {
|
||||
func (b *BbxtTools) DailyReport(
|
||||
ctx context.Context,
|
||||
now time.Time,
|
||||
downWardValue int32,
|
||||
productName []string,
|
||||
sumFilter int32,
|
||||
ossClient *utils_oss.Client,
|
||||
initFunc LossSumInitFunc,
|
||||
) (reports []*ReportRes, err error) {
|
||||
reports = make([]*ReportRes, 0, 4)
|
||||
productLossReport, err := b.StatisOursProductLossSum(now)
|
||||
productLossReport, err := b.StatisOursProductLossSum(ctx, now, initFunc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -94,7 +105,7 @@ func (b *BbxtTools) DailyReport(now time.Time, downWardValue int32, productName
|
|||
}
|
||||
|
||||
// StatisOursProductLossSum 负利润分析
|
||||
func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes, err error) {
|
||||
func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time, initFunc LossSumInitFunc) (report []*ReportRes, 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"),
|
||||
adjustedTime(now), //adjustedTime(time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())),
|
||||
|
|
@ -110,6 +121,7 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
|
|||
resellerMap = make(map[int32]*ResellerLoss)
|
||||
total [][]string
|
||||
gt []*ResellerLoss
|
||||
totalDetail []*ResellerLoss
|
||||
)
|
||||
|
||||
for _, info := range data.List {
|
||||
|
|
@ -120,7 +132,7 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
|
|||
ResellerId: info.ResellerId,
|
||||
ResellerName: info.ResellerName,
|
||||
Total: 0, // 初始化为0,后续累加
|
||||
ProductLoss: make(map[int32]ProductLoss), // 初始化map
|
||||
ProductLoss: make(map[int32]*ProductLoss), // 初始化map
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +145,7 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
|
|||
// 检查产品是否已存在
|
||||
if _, ok := reseller.ProductLoss[info.OursProductId]; !ok {
|
||||
// 创建新的产品亏损记录
|
||||
reseller.ProductLoss[info.OursProductId] = ProductLoss{
|
||||
reseller.ProductLoss[info.OursProductId] = &ProductLoss{
|
||||
ProductId: info.OursProductId,
|
||||
ProductName: info.OursProductName,
|
||||
Loss: info.Loss, // 初始化为当前产品的亏损
|
||||
|
|
@ -156,6 +168,7 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
|
|||
})
|
||||
var (
|
||||
totalSum float64
|
||||
|
||||
totalSum500 float64
|
||||
)
|
||||
// 构建分组
|
||||
|
|
@ -166,46 +179,116 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
|
|||
fmt.Sprintf("%.2f", v.Total),
|
||||
})
|
||||
totalSum += v.Total
|
||||
totalDetail = append(totalDetail, v)
|
||||
}
|
||||
if v.Total <= -500 && !slices.Contains(resellerBlackListProduct, v.ResellerName) {
|
||||
gt = append(gt, v)
|
||||
totalSum500 += v.Total
|
||||
}
|
||||
}
|
||||
report = make([]*ReportRes, 2)
|
||||
report = make([]*ReportRes, 3)
|
||||
timeCh := now.Format("1月2日15点")
|
||||
//总量生成excel
|
||||
if len(total) > 0 {
|
||||
filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
|
||||
err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total, "")
|
||||
report[0] = &ReportRes{
|
||||
ReportName: "负利润分析(合计表)",
|
||||
Title: "截至" + timeCh + "利润累计亏损" + fmt.Sprintf("%.2f", totalSum),
|
||||
Path: filePath,
|
||||
Data: total,
|
||||
}
|
||||
}
|
||||
//if len(total) > 0 {
|
||||
// filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
|
||||
// err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total, "")
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// report[0] = &ReportRes{
|
||||
// ReportName: "分销商负利润统计",
|
||||
// Title: "截至" + timeCh + "利润累计亏损" + fmt.Sprintf("%.2f", totalSum),
|
||||
// Path: filePath,
|
||||
// Data: total,
|
||||
// }
|
||||
//}
|
||||
|
||||
//if len(gt) > 0 {
|
||||
// filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
|
||||
// title := "截至" + timeCh + "亏损500以上的分销商和产品"
|
||||
// err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt, title)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// report[1] = &ReportRes{
|
||||
// ReportName: "负利润分析(亏损500以上)",
|
||||
// Title: "截至" + timeCh + "亏损500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500),
|
||||
// Path: filePath,
|
||||
// Data: total,
|
||||
// }
|
||||
//}
|
||||
|
||||
if len(totalDetail) > 0 {
|
||||
err = initFunc(ctx, now, totalDetail, b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(gt) > 0 {
|
||||
filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
|
||||
title := "截至" + timeCh + "亏损500以上的分销商和产品"
|
||||
err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt, title)
|
||||
report[1] = &ReportRes{
|
||||
ReportName: "负利润分析(亏损500以上)",
|
||||
Title: "截至" + timeCh + "亏损500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500),
|
||||
filePath := b.cacheDir + "/kshj_total_ana" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
|
||||
title := "截至" + timeCh + "亏损100以上的分销商&产品负利润原因"
|
||||
err = b.resellerDetailFillExcelAna(b.excelTempDir+"/"+"kshj_total_ana.xlsx", filePath, totalDetail, title)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
report[2] = &ReportRes{
|
||||
ReportName: "负利润分析(亏损100以上)",
|
||||
Title: "截至" + timeCh + "亏损100以上利润原因",
|
||||
Path: filePath,
|
||||
Data: total,
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (b *BbxtTools) GetResellerLossMannagerAndLossReasonFromApi(ctx context.Context, resellerLoss []*ResellerLoss) (map[int32]*ResellerLossSumProductRelation, error) {
|
||||
var (
|
||||
resellerMap = make(map[int32]map[string][]int32)
|
||||
resellerLossMap = make(map[int32]*ResellerLoss, len(resellerLoss))
|
||||
relationMap = make(map[int32]*ResellerLossSumProductRelation)
|
||||
)
|
||||
|
||||
for _, v := range resellerLoss {
|
||||
var productSlice = make([]int32, 0, len(v.ProductLoss))
|
||||
for _, vv := range v.ProductLoss {
|
||||
productSlice = append(productSlice, vv.ProductId)
|
||||
}
|
||||
if _, ex := resellerMap[v.ResellerId]; !ex {
|
||||
resellerMap[v.ResellerId] = make(map[string][]int32, 1)
|
||||
}
|
||||
resellerMap[v.ResellerId]["values"] = productSlice
|
||||
resellerLossMap[v.ResellerId] = v
|
||||
relationMap[v.ResellerId] = &ResellerLossSumProductRelation{
|
||||
ResellerName: v.ResellerName,
|
||||
Products: make(map[int32]*LossReason, len(v.ProductLoss)),
|
||||
}
|
||||
for _, product := range v.ProductLoss {
|
||||
relationMap[v.ResellerId].Products[product.ProductId] = &LossReason{
|
||||
ProductName: product.ProductName,
|
||||
LossReason: "未填写", // 初始化为未填写
|
||||
}
|
||||
}
|
||||
}
|
||||
res, err := GetManagerAndDefaultLossReasonApi(&GetManagerAndDefaultLossReasonRequest{
|
||||
Param: resellerMap,
|
||||
}, b.login.GetToken(ctx), b.config.ZLTX.ReqUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range res {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := resellerLossMap[v.ResellerInfo.ResellerId]; !ok {
|
||||
continue
|
||||
}
|
||||
relationMap[v.ResellerInfo.ResellerId].AfterSaleName = v.ResellerInfo.AfterSaleName
|
||||
|
||||
for _, vv := range v.ProductList {
|
||||
relationMap[v.ResellerInfo.ResellerId].Products[vv.ProductId].LossReason = vv.LossReason
|
||||
}
|
||||
}
|
||||
return relationMap, nil
|
||||
}
|
||||
|
||||
// GetProfitRankingSum 利润同比分销商排行榜
|
||||
func (b *BbxtTools) GetProfitRankingSum(now time.Time) (report *ReportRes, err error) {
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,19 @@ package bbxt
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/lsxd"
|
||||
"ai_scheduler/internal/pkg/utils_oss"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
|
||||
|
|
@ -23,29 +32,30 @@ func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
o, err := NewBbxtTools(nil)
|
||||
o, err := NewBbxtTools(nil, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
reports, err := o.DailyReport(time.Now(), DownWardValue, []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, SumFilter, ossClient)
|
||||
reports, err := o.DailyReport(context.Background(), time.Now(), DownWardValue, []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, SumFilter, ossClient, GetReportCache)
|
||||
|
||||
t.Log(reports, err)
|
||||
|
||||
}
|
||||
|
||||
func Test_StatisOursProductLossSum(t *testing.T) {
|
||||
o, err := NewBbxtTools(nil)
|
||||
run()
|
||||
o, err := NewBbxtTools(configConfig, lsxd.NewLogin(configConfig, utils.NewRdb(configConfig)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
report, err := o.StatisOursProductLossSum(time.Now())
|
||||
report, err := o.StatisOursProductLossSum(context.Background(), time.Now(), GetReportCache)
|
||||
|
||||
t.Log(report, err)
|
||||
|
||||
}
|
||||
|
||||
func Test_GetProfitRankingSum(t *testing.T) {
|
||||
o, err := NewBbxtTools(nil)
|
||||
o, err := NewBbxtTools(nil, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
@ -56,7 +66,7 @@ func Test_GetProfitRankingSum(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_GetStatisOfficialProductSumDecline(t *testing.T) {
|
||||
o, err := NewBbxtTools(nil)
|
||||
o, err := NewBbxtTools(nil, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
@ -69,7 +79,9 @@ func Test_GetStatisOfficialProductSumDecline(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_GetStatisOfficialProductSum(t *testing.T) {
|
||||
o, err := NewBbxtTools(nil)
|
||||
|
||||
configs := configConfig
|
||||
o, err := NewBbxtTools(nil, lsxd.NewLogin(configs, utils.NewRdb(configConfig)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
@ -79,3 +91,62 @@ func Test_GetStatisOfficialProductSum(t *testing.T) {
|
|||
t.Log(report, err)
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||
configConfig *config.Config
|
||||
)
|
||||
|
||||
func run() {
|
||||
configConfig, _ = config.LoadConfigWithTest()
|
||||
// 初始化数据库连接
|
||||
db, _ := utils.NewGormDb(configConfig)
|
||||
reportDailyCacheImpl = impl.NewReportDailyCacheImpl(db)
|
||||
}
|
||||
|
||||
func GetReportCache(ctx context.Context, day time.Time, totalDetail []*ResellerLoss, bbxtObj *BbxtTools) error {
|
||||
run()
|
||||
|
||||
var ResellerProductRelation map[int32]*ResellerLossSumProductRelation
|
||||
dayDate := day.Format(time.DateOnly)
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"`index`": IndexLossSumDetail})
|
||||
cond = cond.And(builder.Eq{"`key`": dayDate})
|
||||
var cache model.AiReportDailyCache
|
||||
|
||||
err := reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cache.Value == "" {
|
||||
ResellerProductRelation, err = bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cache = model.AiReportDailyCache{
|
||||
Key: dayDate,
|
||||
Index: IndexLossSumDetail,
|
||||
Value: pkg.JsonStringIgonErr(ResellerProductRelation),
|
||||
}
|
||||
_, err = reportDailyCacheImpl.Add(&cache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = json.Unmarshal([]byte(cache.Value), &ResellerProductRelation)
|
||||
|
||||
for _, v := range totalDetail {
|
||||
if _, ex := ResellerProductRelation[v.ResellerId]; !ex {
|
||||
continue
|
||||
}
|
||||
v.Manager = ResellerProductRelation[v.ResellerId].AfterSaleName
|
||||
for _, vv := range v.ProductLoss {
|
||||
if _, ex := ResellerProductRelation[v.ResellerId].Products[vv.ProductId]; !ex {
|
||||
continue
|
||||
}
|
||||
vv.LossReason = ResellerProductRelation[v.ResellerId].Products[vv.ProductId].LossReason
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ type ResellerLoss struct {
|
|||
ResellerId int32
|
||||
ResellerName string
|
||||
Total float64
|
||||
ProductLoss map[int32]ProductLoss
|
||||
ProductLoss map[int32]*ProductLoss
|
||||
Manager string
|
||||
}
|
||||
|
||||
type ProductLoss struct {
|
||||
ProductId int32
|
||||
ProductName string
|
||||
Loss float64
|
||||
LossReason string
|
||||
}
|
||||
|
||||
type ReportRes struct {
|
||||
|
|
@ -37,3 +39,14 @@ type ProductSumReseller struct {
|
|||
HistoryTwoNum int32 //上周成功数量
|
||||
HistoryTwoDiff int32 //同比上周当前增减量
|
||||
}
|
||||
|
||||
type ResellerLossSumProductRelation struct {
|
||||
AfterSaleName string `json:"after_sale_name"`
|
||||
ResellerName string `json:"reseller_name"`
|
||||
Products map[int32]*LossReason
|
||||
}
|
||||
|
||||
type LossReason struct {
|
||||
ProductName string
|
||||
LossReason string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d
|
|||
|
||||
for _, reseller := range dataSlice {
|
||||
// 排序 ProductLoss
|
||||
var products []ProductLoss
|
||||
var products []*ProductLoss
|
||||
for _, p := range reseller.ProductLoss {
|
||||
products = append(products, p)
|
||||
}
|
||||
|
|
@ -311,4 +311,164 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d
|
|||
return f.SaveAs(outputPath)
|
||||
}
|
||||
|
||||
// OfficialProductSumDeclineExcel
|
||||
func (b *BbxtTools) resellerDetailFillExcelAna(templatePath, outputPath string, dataSlice []*ResellerLoss, 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
|
||||
styleA3, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowData))
|
||||
if err != nil {
|
||||
styleA3 = 0
|
||||
}
|
||||
// B2和C2通常样式一致,这里取B2作为明细列样式
|
||||
styleB3, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowData))
|
||||
if err != nil {
|
||||
styleB3 = 0
|
||||
}
|
||||
styleC3, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowData))
|
||||
if err != nil {
|
||||
styleC3 = 0
|
||||
}
|
||||
|
||||
styleD3, err := f.GetCellStyle(sheet, fmt.Sprintf("D%d", tplRowData))
|
||||
if err != nil {
|
||||
styleC3 = 0
|
||||
}
|
||||
|
||||
styleE3, err := f.GetCellStyle(sheet, fmt.Sprintf("E%d", tplRowData))
|
||||
if err != nil {
|
||||
styleC3 = 0
|
||||
}
|
||||
|
||||
rowHeightData, err := f.GetRowHeight(sheet, tplRowData)
|
||||
if err != nil {
|
||||
rowHeightData = 20
|
||||
}
|
||||
|
||||
// 模板第4行:合计行样式
|
||||
tplRowTotal := 5
|
||||
styleTotalA, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowTotal))
|
||||
if err != nil {
|
||||
styleTotalA = 0
|
||||
}
|
||||
styleTotalB, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowTotal))
|
||||
if err != nil {
|
||||
styleTotalB = 0
|
||||
}
|
||||
styleTotalC, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowTotal))
|
||||
if err != nil {
|
||||
styleTotalC = 0
|
||||
}
|
||||
styleTotalD, err := f.GetCellStyle(sheet, fmt.Sprintf("D%d", tplRowTotal))
|
||||
if err != nil {
|
||||
styleTotalC = 0
|
||||
}
|
||||
styleTotalE, err := f.GetCellStyle(sheet, fmt.Sprintf("E%d", tplRowTotal))
|
||||
if err != nil {
|
||||
styleTotalC = 0
|
||||
}
|
||||
rowHeightTotal, err := f.GetRowHeight(sheet, tplRowTotal)
|
||||
if err != nil {
|
||||
rowHeightTotal = 30
|
||||
}
|
||||
// ----------------------------------------
|
||||
|
||||
currentRow := 3
|
||||
totalLoss := 0.0
|
||||
|
||||
for _, reseller := range dataSlice {
|
||||
// 排序 ProductLoss
|
||||
var products []*ProductLoss
|
||||
for _, p := range reseller.ProductLoss {
|
||||
products = append(products, p)
|
||||
}
|
||||
sort.Slice(products, func(i, j int) bool {
|
||||
return products[i].Loss < products[j].Loss
|
||||
})
|
||||
|
||||
startRow := currentRow
|
||||
|
||||
// 填充该经销商的所有产品
|
||||
for _, p := range products {
|
||||
// 设置行高
|
||||
f.SetRowHeight(sheet, currentRow, rowHeightData)
|
||||
|
||||
// 设置值
|
||||
f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), reseller.ResellerName)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.ProductName)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), p.Loss)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("D%d", currentRow), reseller.Manager)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("E%d", currentRow), p.LossReason)
|
||||
// 设置样式
|
||||
if styleA3 != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleA3)
|
||||
}
|
||||
if styleB3 != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleB3)
|
||||
}
|
||||
if styleC3 != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleC3)
|
||||
}
|
||||
if styleD3 != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("D%d", currentRow), fmt.Sprintf("D%d", currentRow), styleD3)
|
||||
}
|
||||
if styleE3 != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("E%d", currentRow), fmt.Sprintf("E%d", currentRow), styleE3)
|
||||
}
|
||||
|
||||
totalLoss += p.Loss
|
||||
currentRow++
|
||||
}
|
||||
|
||||
endRow := currentRow - 1
|
||||
// 合并单元格 (如果多于1行)
|
||||
if endRow > startRow {
|
||||
f.MergeCell(sheet, fmt.Sprintf("A%d", startRow), fmt.Sprintf("A%d", endRow))
|
||||
f.MergeCell(sheet, fmt.Sprintf("D%d", startRow), fmt.Sprintf("D%d", endRow))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 填充合计行 ----------------
|
||||
// 四舍五入保留四位小数
|
||||
totalLoss, _ = decimal.NewFromFloat(totalLoss).Round(4).Float64()
|
||||
// 设置行高
|
||||
f.SetRowHeight(sheet, currentRow, rowHeightTotal)
|
||||
|
||||
f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), "合计")
|
||||
// B列留空,C列填充总亏损
|
||||
f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), totalLoss)
|
||||
|
||||
// 设置合计行样式
|
||||
if styleTotalA != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleTotalA)
|
||||
}
|
||||
if styleTotalB != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleTotalB)
|
||||
}
|
||||
if styleTotalC != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleTotalC)
|
||||
}
|
||||
if styleTotalD != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("D%d", currentRow), fmt.Sprintf("D%d", currentRow), styleTotalD)
|
||||
}
|
||||
if styleTotalE != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("E%d", currentRow), fmt.Sprintf("E%d", currentRow), styleTotalE)
|
||||
}
|
||||
|
||||
// 取消合并合计行的A、B列
|
||||
// f.MergeCell(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow))
|
||||
|
||||
// 6. 保存
|
||||
return f.SaveAs(outputPath)
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in New Issue