package biz import ( "context" "encoding/json" "fmt" "github.com/go-kratos/kratos/v2/log" "github.com/wechatpay-apiv3/wechatpay-go/services/cashcoupons" "strconv" "strings" "time" "voucher/internal/biz/bo" "voucher/internal/biz/do" "voucher/internal/biz/vo" ) func (this *VoucherBiz) Warning(ctx context.Context, batchNo string) error { product, err := this.ProductRepo.GetByBatchNo(ctx, batchNo) if err != nil { return err } return this.WarningBudget(ctx, product) } func (this *VoucherBiz) WarningBudgetIncr(ctx context.Context, key string, ttl time.Duration) (int64, error) { // 增加发送计数 count, err := this.rdb.Rdb.IncrBy(ctx, key, 1).Result() if err != nil { return 0, err } // 如果是第一次发送,设置 过期时间 if count == 1 { if err = this.rdb.Rdb.Expire(ctx, key, ttl).Err(); err != nil { return 0, fmt.Errorf("设置过期时间失败: %v", err) } } // 如果发送次数超过 “指定” 条,清除再来 if count > 24 { // 大约2小时 return 0, this.WarningBudgetIncrDel(ctx, key) } return count, nil } func (this *VoucherBiz) WarningBudgetIncrDel(ctx context.Context, key string) error { // 检查键是否存在 exists, err := this.rdb.Rdb.Exists(ctx, key).Result() if err != nil { return fmt.Errorf("检查键存在性失败: %w", err) } // 如果键不存在,直接返回成功 if exists == 0 { return nil } if _, err = this.rdb.Rdb.Del(ctx, key).Result(); err != nil { return err } return nil } func (this *VoucherBiz) CronWarningBudget(ctx context.Context) { uid := "warningBudget" if b := this.Get(uid); b { log.Warn("预警查询,上波还未执行完毕,此次暂不执行") return } this.Add(uid) defer this.Remove(uid) start := time.Now() log.Warnf("预警查询,执行开始: %s", start.Format(time.DateTime)) if err := this.cronWarningBudget(ctx); err != nil { log.Errorf("预警查询,执行失败: %s", err) } end := time.Now() elapsed := end.Sub(start) log.Warnf("预警查询,开始执行时间%s,执行结束时间%s,代码块执行耗时: %s", start.Format(time.DateTime), end.Format(time.DateTime), elapsed) return } func (this *VoucherBiz) cronWarningBudget(ctx context.Context) error { return this.ProductRepo.FindWarningBudget(ctx, func(ctx context.Context, rows []*bo.ProductBo) error { for _, row := range rows { if err := this.WarningBudget(ctx, row); err != nil { log.Context(ctx).Errorf("预警查询,处理失败: %s", err) } time.Sleep(time.Second * 2) } return nil }) } func (this *VoucherBiz) WarningBudget(ctx context.Context, product *bo.ProductBo) error { if product.WarningBudget == 0 { return fmt.Errorf("no warning budget") } if product.WarningBudget == 0 { return fmt.Errorf("no warning budget") } now := time.Now() if now.Before(*product.StartTime) { return fmt.Errorf("not start") } if now.After(*product.EndTime) { return fmt.Errorf("expired") } wxResp, err := this.WechatCpnRepo.QueryProduct(ctx, product.MchId, product.BatchNo) if err != nil { return err } return this.Calculate(ctx, product, wxResp) } func (this *VoucherBiz) Calculate(ctx context.Context, product *bo.ProductBo, wxResp *cashcoupons.Stock) error { w := this.WxResp(wxResp) b := vo.WarningBudgetSendIncr.BuildCache([]string{product.BatchNo}) key := b.Key ttl := b.TTL if w.AllBudget > product.AllBudget { if err := this.WarningBudgetIncrDel(ctx, key); err != nil { return err } } if err := this.ProductRepo.UpdateByWxResp(ctx, product.ID, w); err != nil { return err } if product.WarningBudget >= w.AvailableBudget { count, err2 := this.WarningBudgetIncr(ctx, key, ttl) if err2 != nil { return err2 } if count == 1 { return this.WarningSend(ctx, product, w) } else { log.Warnf("预警查询,当前达到预警第[%d]次,暂不做通知", count) } } return nil } func (this *VoucherBiz) WarningSend(ctx context.Context, product *bo.ProductBo, w *do.WxResp) error { if err := this.DingMixRepo.SendMarkdownMessage(ctx, "券预算不足", formatAsCard(product, w)); err != nil { return err } var warningPerson []*do.WarningPerson if err := json.Unmarshal([]byte(product.WarningPerson), &warningPerson); err != nil { return err } var mobileList []string for _, person := range warningPerson { mobileList = append(mobileList, person.Mobile) } if len(mobileList) > 0 { return this.SmsMixRepo.Send(ctx, mobileList, buildTemplateParams(product, w)) } log.Warnf("no mobile to send, batchNo:%s", product.BatchNo) return nil } func buildTemplateParams(product *bo.ProductBo, req *do.WxResp) map[string]string { return map[string]string{ "stock_name": product.BatchName, "stock_no": product.ProductNo, "amount": strconv.Itoa(int(req.Amount)), "all_budget": strconv.Itoa(int(req.AllBudget)), "all_stock": strconv.Itoa(int(req.AllStock)), "used_stock": strconv.Itoa(int(req.UsedStock)), "used_budget": strconv.Itoa(int(req.UsedBudget)), "available_budget": strconv.Itoa(int(req.AvailableBudget)), "available_stock": strconv.Itoa(int(req.AvailableStock)), "budget_usage_rate": fmt.Sprintf("%.1f", req.StockUsageRate), } } func formatAsCard(product *bo.ProductBo, req *do.WxResp) string { var card strings.Builder card.WriteString("### " + product.BatchName + "\n\n") // 基本信息 card.WriteString("#### 🎫 基本信息\n") card.WriteString(fmt.Sprintf("- **批次号**: %s\n", product.BatchNo)) card.WriteString(fmt.Sprintf("- **活动号**: %s\n", product.ProductNo)) card.WriteString(fmt.Sprintf("- **预警值**: %d元\n", product.WarningBudget)) card.WriteString(fmt.Sprintf("- **面额**: %d元\n", req.Amount)) card.WriteString(fmt.Sprintf("- **总预算**: %d元\n", req.AllBudget)) card.WriteString(fmt.Sprintf("- **总库存**: %d张\n", req.AllStock)) card.WriteString("\n") // 使用情况 card.WriteString("#### 📊 使用情况\n") card.WriteString(fmt.Sprintf("- **已发券数**: %d张\n", req.UsedStock)) card.WriteString(fmt.Sprintf("- **已发券金额**: %d元\n", req.UsedBudget)) card.WriteString(fmt.Sprintf("- **剩余库存**: %d张\n", req.AvailableStock)) card.WriteString(fmt.Sprintf("- **剩余预算**: %d元\n", req.AvailableBudget)) card.WriteString("\n") card.WriteString("#### 📈 使用率\n") card.WriteString(fmt.Sprintf("- **使用率**: %.1f%%\n", req.StockUsageRate)) return card.String() }