diff --git a/config/config.yaml b/config/config.yaml index 37b65aa..3b4fab8 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -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" diff --git a/config/config_test.yaml b/config/config_test.yaml index c993b35..4aab8b8 100644 --- a/config/config_test.yaml +++ b/config/config_test.yaml @@ -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 #每个连接最大空闲时间,如果超过了这个时间会被关闭 diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index 1ad715c..fe8c6e0 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -29,21 +29,22 @@ import ( // AiRouterBiz 智能路由服务 type DingTalkBotBiz struct { - do *do.Do - handle *do.Handle - botConfigImpl *impl.BotConfigImpl - replier *chatbot.ChatbotReplier - log log.Logger - dingTalkUser *dingtalk.User - botGroupImpl *impl.BotGroupImpl - botGroupConfigImpl *impl.BotGroupConfigImpl - botGroupQywxImpl *impl.BotGroupQywxImpl - toolManager *tools.Manager - chatHis *impl.BotChatHisImpl - conf *config.Config - cardSend *dingtalk.SendCardClient - qywxGroupHandle *qywx.Group - groupConfigBiz *GroupConfigBiz + do *do.Do + handle *do.Handle + botConfigImpl *impl.BotConfigImpl + replier *chatbot.ChatbotReplier + log log.Logger + dingTalkUser *dingtalk.User + botGroupImpl *impl.BotGroupImpl + botGroupConfigImpl *impl.BotGroupConfigImpl + botGroupQywxImpl *impl.BotGroupQywxImpl + toolManager *tools.Manager + chatHis *impl.BotChatHisImpl + conf *config.Config + cardSend *dingtalk.SendCardClient + qywxGroupHandle *qywx.Group + groupConfigBiz *GroupConfigBiz + reportDailyCacheImpl *impl.ReportDailyCacheImpl } // NewDingTalkBotBiz @@ -54,23 +55,25 @@ func NewDingTalkBotBiz( botGroupImpl *impl.BotGroupImpl, dingTalkUser *dingtalk.User, chatHis *impl.BotChatHisImpl, + reportDailyCacheImpl *impl.ReportDailyCacheImpl, toolManager *tools.Manager, conf *config.Config, cardSend *dingtalk.SendCardClient, groupConfigBiz *GroupConfigBiz, ) *DingTalkBotBiz { return &DingTalkBotBiz{ - do: do, - handle: handle, - botConfigImpl: botConfigImpl, - replier: chatbot.NewChatbotReplier(), - dingTalkUser: dingTalkUser, - groupConfigBiz: groupConfigBiz, - botGroupImpl: botGroupImpl, - toolManager: toolManager, - chatHis: chatHis, - conf: conf, - cardSend: cardSend, + do: do, + handle: handle, + botConfigImpl: botConfigImpl, + replier: chatbot.NewChatbotReplier(), + dingTalkUser: dingTalkUser, + groupConfigBiz: groupConfigBiz, + botGroupImpl: botGroupImpl, + toolManager: toolManager, + 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 } diff --git a/internal/biz/group_config.go b/internal/biz/group_config.go index b8ccb92..21605fc 100644 --- a/internal/biz/group_config.go +++ b/internal/biz/group_config.go @@ -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" @@ -30,12 +34,14 @@ import ( // AiRouterBiz 智能路由服务 type GroupConfigBiz struct { - botGroupConfigImpl *impl.BotGroupConfigImpl - ossClient *utils_oss.Client - workflowManager *runtime.Registry - botTools []model.AiBotTool - toolManager *tools.Manager - conf *config.Config + 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,13 +51,17 @@ func NewGroupConfigBiz( botGroupConfigImpl *impl.BotGroupConfigImpl, workflowManager *runtime.Registry, conf *config.Config, + reportDailyCacheImpl *impl.ReportDailyCacheImpl, + rdb *utils.Rdb, ) *GroupConfigBiz { return &GroupConfigBiz{ - botTools: tools.BootTools, - ossClient: ossClient, - botGroupConfigImpl: botGroupConfigImpl, - workflowManager: workflowManager, - conf: conf, + botTools: tools.BootTools, + ossClient: ossClient, + 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 +} diff --git a/internal/config/config.go b/internal/config/config.go index 6756040..b4f05d9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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"` } diff --git a/internal/data/impl/provider_set.go b/internal/data/impl/provider_set.go index 916b017..c200234 100644 --- a/internal/data/impl/provider_set.go +++ b/internal/data/impl/provider_set.go @@ -17,4 +17,5 @@ var ProviderImpl = wire.NewSet( NewBotGroupImpl, NewBotGroupConfigImpl, NewBotGroupQywxImpl, + NewReportDailyCacheImpl, ) diff --git a/internal/data/impl/report_dayily_cache.go b/internal/data/impl/report_dayily_cache.go new file mode 100644 index 0000000..479d888 --- /dev/null +++ b/internal/data/impl/report_dayily_cache.go @@ -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)), + } +} diff --git a/internal/data/model/ai_report_daily_cache.gen.go b/internal/data/model/ai_report_daily_cache.gen.go new file mode 100644 index 0000000..0fa1f57 --- /dev/null +++ b/internal/data/model/ai_report_daily_cache.gen.go @@ -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 +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 +} diff --git a/internal/pkg/lsxd/login.go b/internal/pkg/lsxd/login.go index 5ea6801..13b7f1c 100644 --- a/internal/pkg/lsxd/login.go +++ b/internal/pkg/lsxd/login.go @@ -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) { diff --git a/internal/pkg/util/map.go b/internal/pkg/util/map.go index 5ca80c8..8bc5e1d 100644 --- a/internal/pkg/util/map.go +++ b/internal/pkg/util/map.go @@ -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 +} diff --git a/internal/tools/bbxt/api.go b/internal/tools/bbxt/api.go index a7b057b..4dd645b 100644 --- a/internal/tools/bbxt/api.go +++ b/internal/tools/bbxt/api.go @@ -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 diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index 304be24..926f973 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -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 { @@ -119,8 +131,8 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes resellerMap[info.ResellerId] = &ResellerLoss{ ResellerId: info.ResellerId, ResellerName: info.ResellerName, - Total: 0, // 初始化为0,后续累加 - ProductLoss: make(map[int32]ProductLoss), // 初始化map + Total: 0, // 初始化为0,后续累加 + 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, // 初始化为当前产品的亏损 @@ -155,7 +167,8 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes return resellers[i].Total < resellers[j].Total }) var ( - totalSum float64 + 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 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), + //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 + } + 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) { diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go index ee7f5da..1ea8f43 100644 --- a/internal/tools/bbxt/bbxt_test.go +++ b/internal/tools/bbxt/bbxt_test.go @@ -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 +} diff --git a/internal/tools/bbxt/entitys.go b/internal/tools/bbxt/entitys.go index 7729d68..904f541 100644 --- a/internal/tools/bbxt/entitys.go +++ b/internal/tools/bbxt/entitys.go @@ -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 +} diff --git a/internal/tools/bbxt/excel.go b/internal/tools/bbxt/excel.go index 1744f76..91915dc 100644 --- a/internal/tools/bbxt/excel.go +++ b/internal/tools/bbxt/excel.go @@ -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) +} diff --git a/tmpl/excel_temp/kshj_total_ana.xlsx b/tmpl/excel_temp/kshj_total_ana.xlsx new file mode 100644 index 0000000..d2774f3 Binary files /dev/null and b/tmpl/excel_temp/kshj_total_ana.xlsx differ