package do 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/tools/bbxt" "ai_scheduler/utils" "database/sql" "errors" "context" "encoding/json" "fmt" "strconv" "strings" "time" "unicode" "github.com/gofiber/fiber/v2/log" "xorm.io/builder" ) type Macro struct { botGroupImpl *impl.BotGroupImpl reportDailyCacheImpl *impl.ReportDailyCacheImpl config *config.Config rdb *utils.Rdb } func NewMacro( botGroupImpl *impl.BotGroupImpl, reportDailyCacheImpl *impl.ReportDailyCacheImpl, config *config.Config, rdb *utils.Rdb, ) *Macro { return &Macro{ botGroupImpl: botGroupImpl, reportDailyCacheImpl: reportDailyCacheImpl, config: config, rdb: rdb, } } type MacroFunc func(ctx context.Context, content string, groupConfig *model.AiBotGroupConfig, config *config.Config) (successMsg string, err error, isFinish bool) func (m *Macro) Router(ctx context.Context, reqContent string, groupConfig *model.AiBotGroupConfig) (successMsg string, err error, isFinish bool) { reqContent = strings.TrimSpace(reqContent) switch { case strings.HasPrefix(reqContent, "[利润同比报表]商品修改"): return m.ProductModify(ctx, reqContent, groupConfig) case strings.HasPrefix(reqContent, "[利润同比报表]商品列表"): return m.ProductList(ctx, groupConfig) case strings.HasPrefix(reqContent, "[负利润分析]导出"): return m.NegativeProfitGet(ctx) case strings.HasPrefix(reqContent, "[负利润分析]更新"): return m.NegativeProfitUpdate(ctx, reqContent) case strings.HasPrefix(reqContent, "[负利润分析]清理"): return m.NegativeProfitClear() default: } return } func (m *Macro) NegativeProfitClear() (successMsg string, err error, isFinish bool) { dayDate := time.Now().Format(time.DateOnly) cond := builder.NewCond() cond = cond.And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail}) cond = cond.And(builder.Eq{"cache_key": dayDate}) cond = cond.And(builder.Eq{"status": 1}) err = m.reportDailyCacheImpl.UpdateByCond(&cond, &model.AiReportDailyCache{ Status: 2, }) if err != nil { err = fmt.Errorf("解析失败:%v", err) return } isFinish = true successMsg = "清理成功" return } func (m *Macro) NegativeProfitUpdate(ctx context.Context, content string) (successMsg string, err error, isFinish bool) { //newContent := strings.ReplaceAll(strings.TrimSpace(content), "[负利润分析]更新:", "") jsonData, err := ParseLossData(content) b, err := bbxt.NewBbxtTools(m.config, lsxd.NewLogin(m.config, m.rdb)) if err != nil { return } now := time.Now() dayData := now.Format(time.DateOnly) value, err := b.GetMapResellerLossSumProductRelation(ctx, now, m.GetReportCache) if err != nil { return } var setData = make(map[int32]*bbxt.ResellerLossSumProductRelation) for k, v := range jsonData { if _, ok := value[k]; !ok { continue } for productId, product := range v.Products { if _, ok := value[k].Products[productId]; !ok { continue } if value[k].Products[productId].LossReason == product.LossReason { continue } if _, ex := setData[k]; !ex { setData[k] = &bbxt.ResellerLossSumProductRelation{ ResellerName: value[k].ResellerName, AfterSaleName: value[k].AfterSaleName, Products: make(map[int32]*bbxt.LossReason), } } setData[k].Products[productId] = &bbxt.LossReason{ ProductName: product.ProductName, LossReason: product.LossReason, } } } cond := builder.NewCond() cond = cond.And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail}) cond = cond.And(builder.Eq{"cache_key": dayData}) cond = cond.And(builder.Eq{"status": 1}) var cache model.AiReportDailyCache err = m.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache) if err != nil { return } if cache.ID == 0 { cache = model.AiReportDailyCache{ CacheKey: dayData, CacheIndex: bbxt.IndexLossSumDetail, Value: pkg.JsonStringIgonErr(setData), } _, err = m.reportDailyCacheImpl.Add(&cache) if err != nil { return } } else { err = m.reportDailyCacheImpl.UpdateByCond(&cond, &model.AiReportDailyCache{ Value: pkg.JsonStringIgonErr(setData), }) if err != nil { return } } isFinish = true successMsg = "更新成功" return } func (m *Macro) NegativeProfitGet(ctx context.Context) (successMsg string, err error, isFinish bool) { b, err := bbxt.NewBbxtTools(m.config, lsxd.NewLogin(m.config, m.rdb)) if err != nil { return } now := time.Now() value, err := b.GetMapResellerLossSumProductRelation(ctx, now, m.GetReportCache) if err != nil { return } //将value转为string //**[供应商id]供应商名称->商务名称**\n //└──商品名称:亏损原因\n var valueString strings.Builder valueString.WriteString("[负利润分析]更新:\n") for k, v := range value { if len(v.AfterSaleName) == 0 { v.AfterSaleName = "未查找到对应商务" } valueString.WriteString(fmt.Sprintf("**[%d]%s->%s**\n", k, v.ResellerName, v.AfterSaleName)) for kk, vv := range v.Products { valueString.WriteString(fmt.Sprintf("└── [%d]%s:%s\n", kk, vv.ProductName, vv.LossReason)) } } successMsg = valueString.String() isFinish = true return } func (m *Macro) ProductModify(ctx context.Context, content string, groupConfig *model.AiBotGroupConfig) (successMsg string, err error, isFinish bool) { content = processString(content) if parts := strings.SplitN(content, ":", 2); len(parts) == 2 { itemInfo := strings.TrimSpace(parts[1]) log.Infof("商品修改信息: %s", itemInfo) groupConfig.ProductName = itemInfo cond := builder.NewCond() cond = cond.And(builder.Eq{"config_id": groupConfig.ConfigID}) err = m.botGroupImpl.UpdateByCond(&cond, groupConfig) if err != nil { err = fmt.Errorf("修改失败:%v", err) return } successMsg = "修改成功" isFinish = true return } return } func (m *Macro) ProductList(ctx context.Context, groupConfig *model.AiBotGroupConfig) (successMsg string, err error, isFinish bool) { if len(groupConfig.ProductName) == 0 { successMsg = "暂未设置" } else { successMsg = groupConfig.ProductName isFinish = true } return } func processString(s string) string { // 替换中文逗号为英文逗号 s = strings.ReplaceAll(s, ",", ",") return string(clearSpacialFormat(s)) } func clearSpacialFormat(s string) (result []rune) { //过滤控制字符(如 \n, \t, \r 等) for _, char := range s { // 判断是否是控制字符(ASCII < 32 或 = 127) if !unicode.IsControl(char) { // 如果需要完全移除 \n 和 \t,可以改成: // if !unicode.IsControl(char) result = append(result, char) } } return } func ParseLossData(input string) (map[int32]*bbxt.ResellerLossSumProductRelation, error) { result := make(map[int32]*bbxt.ResellerLossSumProductRelation) // 按行分割 lines := strings.Split(input, "\n") // 跳过第一行的标题 startIdx := 0 for i, line := range lines { if strings.HasPrefix(line, "[") && strings.Contains(line, "]") && strings.Contains(line, "->") { startIdx = i break } } var currentResellerID int32 var currentReseller *bbxt.ResellerLossSumProductRelation for i := startIdx; i < len(lines); i++ { line := strings.TrimSpace(lines[i]) if line == "" { continue } // 检查是否是供应商行(以 [ 开头) if strings.HasPrefix(line, "[") && strings.Contains(line, "]") { // 解析供应商行:[25131]兴业银行-营销系统->未查找到对应商务 // 找到第一个 ] 的位置 bracketEnd := strings.Index(line, "]") if bracketEnd == -1 { continue } // 提取供应商ID idStr := line[1:bracketEnd] id, err := strconv.ParseInt(idStr, 10, 32) if err != nil { continue } currentResellerID = int32(id) // 提取供应商名称和商务名称 remaining := strings.TrimSpace(line[bracketEnd+1:]) // 分割供应商名称和商务名称 var resellerName, afterSaleName string arrowIdx := strings.Index(remaining, "->") if arrowIdx != -1 { resellerName = strings.TrimSpace(remaining[:arrowIdx]) afterSaleName = strings.TrimSpace(remaining[arrowIdx+2:]) } else { resellerName = remaining afterSaleName = "" } // 创建新的供应商对象 currentReseller = &bbxt.ResellerLossSumProductRelation{ AfterSaleName: afterSaleName, ResellerName: resellerName, Products: make(map[int32]*bbxt.LossReason), } result[currentResellerID] = currentReseller } else if strings.Contains(line, "└──") { // 解析商品行:└── [460]全国话费30元:未填写 if currentReseller == nil { continue } // 移除└──前缀 productLine := strings.TrimPrefix(line, "└──") productLine = strings.TrimSpace(productLine) if !strings.HasPrefix(productLine, "[") { continue } // 找到商品ID的结束位置 prodBracketEnd := strings.Index(productLine, "]") if prodBracketEnd == -1 { continue } // 提取商品ID prodIDStr := productLine[1:prodBracketEnd] prodID, err := strconv.ParseInt(prodIDStr, 10, 32) if err != nil { continue } // 提取商品名称和亏损原因 prodRemaining := strings.TrimSpace(productLine[prodBracketEnd+1:]) // 找到冒号分隔符 colonIdx := strings.Index(prodRemaining, ":") var productName, lossReason string if colonIdx != -1 { productName = strings.TrimSpace(prodRemaining[:colonIdx]) lossReason = strings.TrimSpace(prodRemaining[colonIdx+1:]) } else { productName = prodRemaining lossReason = "" } // 添加到当前供应商的商品列表中 currentReseller.Products[int32(prodID)] = &bbxt.LossReason{ ProductName: productName, LossReason: lossReason, } } } return result, nil } func (m *Macro) GetReportCache(ctx context.Context, day time.Time, totalDetail []*bbxt.ResellerLoss, bbxtObj *bbxt.BbxtTools) error { dayDate := day.Format(time.DateOnly) // 1. 从 API 获取数据并填充 apiRelations, err := bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail) if err != nil { return fmt.Errorf("get API data failed: %w", err) } // 使用 API 数据填充损失原因 fillLossReasonFromData(totalDetail, apiRelations, "未填写") // 2. 从缓存获取数据并覆盖 cachedRelations, err := m.getCachedRelations(dayDate) if err != nil { return fmt.Errorf("get cache data failed: %w", err) } // 使用缓存数据覆盖损失原因 if cachedRelations != nil { fillLossReasonFromData(totalDetail, cachedRelations, "") } return nil } // 从缓存获取关系数据 func (m *Macro) getCachedRelations(dayDate string) (map[int32]*bbxt.ResellerLossSumProductRelation, error) { cond := builder.NewCond(). And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail}). And(builder.Eq{"cache_key": dayDate}). And(builder.Eq{"status": 1}) var cache model.AiReportDailyCache err := m.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache) if err != nil { if errors.Is(err, sql.ErrNoRows) { // 缓存不存在是正常情况 return nil, nil } return nil, fmt.Errorf("query cache failed: %w", err) } if cache.ID == 0 { return nil, nil } var relations map[int32]*bbxt.ResellerLossSumProductRelation if err := json.Unmarshal([]byte(cache.Value), &relations); err != nil { return nil, fmt.Errorf("unmarshal cache failed: %w", err) } return relations, nil } // 使用指定数据源填充损失原因 func fillLossReasonFromData( totalDetail []*bbxt.ResellerLoss, relations map[int32]*bbxt.ResellerLossSumProductRelation, defaultReason string, // 当数据不存在时使用的默认值 ) { for _, detail := range totalDetail { resellerRelation, exists := relations[detail.ResellerId] if !exists { continue } // 设置售后经理(只有在有值时才设置) if resellerRelation.AfterSaleName != "" { detail.Manager = resellerRelation.AfterSaleName } // 为每个产品设置损失原因 for _, product := range detail.ProductLoss { setProductLossReason(product, resellerRelation, defaultReason) } } } // 设置单个产品的损失原因 func setProductLossReason( product *bbxt.ProductLoss, resellerRelation *bbxt.ResellerLossSumProductRelation, defaultReason string, ) { // 如果该经销商没有产品数据,跳过 if resellerRelation.Products == nil { return } productRelation, exists := resellerRelation.Products[product.ProductId] if !exists { if defaultReason != "" { product.LossReason = defaultReason } return } // 如果有损失原因则设置,否则使用默认值 if productRelation.LossReason != "" { product.LossReason = productRelation.LossReason } else if defaultReason != "" { product.LossReason = defaultReason } }