This commit is contained in:
ziming 2025-07-01 16:45:11 +08:00
parent d091564610
commit 31fb6e77da
10 changed files with 206 additions and 57 deletions

View File

@ -13,6 +13,10 @@ type WarningBudget struct {
UsedBudget int64 // 已使用预算 UsedBudget int64 // 已使用预算
AvailableStock int64 // 可用库存 AvailableStock int64 // 可用库存
RemainingBudget int64 // 剩余预算 RemainingBudget int64 // 剩余预算
StockUsageRate float64 // 券使用率
StartTime *time.Time
EndTime *time.Time
} }
type WarningBudgetLog struct { type WarningBudgetLog struct {
@ -20,3 +24,13 @@ type WarningBudgetLog struct {
Num int Num int
LastTime time.Time LastTime time.Time
} }
type WarningPerson struct {
Mobile string `json:"mobile"`
Name string `json:"name"`
Tag string `json:"tag"`
}
type WarningSend struct {
title, text string
}

View File

@ -3,5 +3,5 @@ package mixrepos
import "context" import "context"
type SmsMixRepo interface { type SmsMixRepo interface {
SendCode(ctx context.Context, phone, code string) error Send(ctx context.Context, phoneNumbers []string, params map[string]string) error
} }

View File

@ -17,6 +17,21 @@ func (this *VoucherBiz) RegisterTag(ctx context.Context, batchNo string) error {
return err return err
} }
wxResp, err := this.WechatCpnRepo.QueryProduct(ctx, stock.MchId, stock.BatchNo)
if err != nil {
return err
}
req, err := this.GetWarningBudget(stock, wxResp)
if err != nil {
return err
}
err = this.ProductRepo.UpdateWarningBudget(ctx, stock.ID, req)
if err != nil {
return err
}
if err = this.registerNotifyTag(ctx, stock.MchId, stock.BatchNo); err != nil { if err = this.registerNotifyTag(ctx, stock.MchId, stock.BatchNo); err != nil {
return err return err
} }

View File

@ -3,10 +3,12 @@ package repo
import ( import (
"context" "context"
"voucher/internal/biz/bo" "voucher/internal/biz/bo"
"voucher/internal/biz/do"
) )
type ProductRepo interface { type ProductRepo interface {
FindWarningBudget(ctx context.Context, fun func(ctx context.Context, rows []*bo.ProductBo) error) error FindWarningBudget(ctx context.Context, fun func(ctx context.Context, rows []*bo.ProductBo) error) error
GetByBatchNo(ctx context.Context, batchNo string) (*bo.ProductBo, error) GetByBatchNo(ctx context.Context, batchNo string) (*bo.ProductBo, error)
GetByProductNo(ctx context.Context, productNo string) (*bo.ProductBo, error) GetByProductNo(ctx context.Context, productNo string) (*bo.ProductBo, error)
UpdateWarningBudget(ctx context.Context, id int32, req *do.WarningBudget) error
} }

View File

@ -138,13 +138,15 @@ func (v *VoucherBiz) warningBudget(ctx context.Context) error {
return nil return nil
} }
func (v *VoucherBiz) Calculate(ctx context.Context, product *bo.ProductBo, wxResp *cashcoupons.Stock) error { func (v *VoucherBiz) GetWarningBudget(product *bo.ProductBo, wxResp *cashcoupons.Stock) (*do.WarningBudget, error) {
availableStock := *wxResp.StockUseRule.MaxCoupons - *wxResp.DistributedCoupons availableStock := *wxResp.StockUseRule.MaxCoupons - *wxResp.DistributedCoupons
couponAmount := *wxResp.StockUseRule.FixedNormalCoupon.CouponAmount / 100 couponAmount := *wxResp.StockUseRule.FixedNormalCoupon.CouponAmount / 100
remainingBudget := availableStock * couponAmount remainingBudget := availableStock * couponAmount
stockUsageRate := float64(*wxResp.DistributedCoupons) / float64(*wxResp.StockUseRule.MaxCoupons) * 100
req := &do.WarningBudget{ req := &do.WarningBudget{
StockName: product.Name, StockName: product.Name,
StockId: product.BatchNo, StockId: product.BatchNo,
@ -156,37 +158,55 @@ func (v *VoucherBiz) Calculate(ctx context.Context, product *bo.ProductBo, wxRes
UsedBudget: *wxResp.DistributedCoupons * couponAmount, UsedBudget: *wxResp.DistributedCoupons * couponAmount,
AvailableStock: availableStock, AvailableStock: availableStock,
RemainingBudget: remainingBudget, RemainingBudget: remainingBudget,
StockUsageRate: stockUsageRate,
} }
str := formatAsCard(req) inputFormat := time.RFC3339
log.Warnf("预警查询,券预算明细,%s", str) if wxResp.AvailableBeginTime != nil {
availableBeginTime, _ := time.Parse(inputFormat, *wxResp.AvailableBeginTime)
req.StartTime = &availableBeginTime
}
if product.WarningBudget >= remainingBudget { if wxResp.AvailableEndTime != nil {
availableEndTime, _ := time.Parse(inputFormat, *wxResp.AvailableEndTime)
req.EndTime = &availableEndTime
}
count, err := v.WarningBudgetIncr(ctx, req.StockId) return req, nil
}
func (v *VoucherBiz) Calculate(ctx context.Context, product *bo.ProductBo, wxResp *cashcoupons.Stock) error {
req, err := v.GetWarningBudget(product, wxResp)
if err != nil { if err != nil {
return err return err
} }
err = v.ProductRepo.UpdateWarningBudget(ctx, product.ID, req)
if err != nil {
return err
}
if product.WarningBudget >= req.RemainingBudget {
count, err2 := v.WarningBudgetIncr(ctx, req.StockId)
if err2 != nil {
return err2
}
if count == 1 { if count == 1 {
return v.WarningSend(ctx, str) return v.WarningSend(ctx, formatAsCard(req))
} else { } else {
log.Warnf("预警查询,当前达到预警第[%d]次,暂不做通知", count) log.Warnf("预警查询,当前达到预警第[%d]次,暂不做通知", count)
} }
//w := v.WarningBudgetAdd(req)
//if w.Num == 1 {
// return v.WarningSend(ctx, str)
//} else if w.Num > 5 {
// v.WarningBudgetRemove(req.StockId)
//}
} }
return nil return nil
} }
func (v *VoucherBiz) WarningSend(ctx context.Context, str string) error { func (v *VoucherBiz) WarningSend(ctx context.Context, str string) error {
return v.DingMixRepo.SendMarkdownMessage(ctx, "券预算不足", str) return v.DingMixRepo.SendMarkdownMessage(ctx, "券预算不足", str)
} }
@ -212,11 +232,8 @@ func formatAsCard(req *do.WarningBudget) string {
card.WriteString(fmt.Sprintf("- **剩余预算**: %d元\n", req.RemainingBudget)) card.WriteString(fmt.Sprintf("- **剩余预算**: %d元\n", req.RemainingBudget))
card.WriteString("\n") card.WriteString("\n")
// 使用率
stockUsageRate := float64(req.UsedStock) / float64(req.AllStock) * 100
card.WriteString("#### 📈 使用率\n") card.WriteString("#### 📈 使用率\n")
card.WriteString(fmt.Sprintf("- **使用率**: %.1f%%\n", stockUsageRate)) card.WriteString(fmt.Sprintf("- **使用率**: %.1f%%\n", req.StockUsageRate))
return card.String() return card.String()
} }

View File

@ -20,6 +20,20 @@ func NewDingMixRepoImpl(bc *conf.Bootstrap) mixrepos.DingMixRepo {
return &DingMixRepoImpl{bc: bc, client: client} return &DingMixRepoImpl{bc: bc, client: client}
} }
func (s *DingMixRepoImpl) SendMessage(_ context.Context, title, text string) error {
isAtAll := false
if len(s.bc.Alarm.AtMobiles) == 0 {
isAtAll = true
}
if err := s.client.SendMarkdownMessage(title, text, s.bc.Alarm.AtMobiles, isAtAll); err != nil {
return fmt.Errorf("markdown 消息发送失败: %v", err)
}
return nil
}
func (s *DingMixRepoImpl) SendMarkdownMessage(_ context.Context, title, text string) error { func (s *DingMixRepoImpl) SendMarkdownMessage(_ context.Context, title, text string) error {
isAtAll := false isAtAll := false

View File

@ -31,6 +31,6 @@ func NewSmsMixRepoImpl(bc *conf.Bootstrap) (mixrepos.SmsMixRepo, error) {
return &SmsMixRepoImpl{bc: bc, smsService: smsService}, nil return &SmsMixRepoImpl{bc: bc, smsService: smsService}, nil
} }
func (s *SmsMixRepoImpl) SendCode(ctx context.Context, phone, code string) error { func (s *SmsMixRepoImpl) Send(ctx context.Context, phoneNumbers []string, params map[string]string) error {
return s.smsService.SendSMS(ctx, []string{phone}, s.bc.AliYunSms.TemplateSendCode, map[string]string{"code": code}) return s.smsService.SendSMS(ctx, phoneNumbers, s.bc.AliYunSms.TemplateSendCode, params)
} }

View File

@ -21,7 +21,10 @@ type Product struct {
Channel uint8 `gorm:"column:channel;not null;comment:1:微信 2:支付宝" json:"channel"` // 1:微信 2:支付宝 Channel uint8 `gorm:"column:channel;not null;comment:1:微信 2:支付宝" json:"channel"` // 1:微信 2:支付宝
AvailableType uint8 `gorm:"column:available_type;not null;comment:1:固定有效期 2:动态有效期" json:"available_type"` AvailableType uint8 `gorm:"column:available_type;not null;comment:1:固定有效期 2:动态有效期" json:"available_type"`
AvailableDays uint32 `gorm:"column:available_days;not null;comment:领取后多少天内" json:"available_days"` AvailableDays uint32 `gorm:"column:available_days;not null;comment:领取后多少天内" json:"available_days"`
AllBudget int64 `gorm:"column:all_budget;not null;default:0" json:"all_budget"`
RemainingBudget int64 `gorm:"column:remaining_budget;not null;default:0" json:"remaining_budget"`
WarningBudget int64 `gorm:"column:warning_budget;not null;default:0" json:"warning_budget"` // 预警预算=0不做预警 WarningBudget int64 `gorm:"column:warning_budget;not null;default:0" json:"warning_budget"` // 预警预算=0不做预警
WarningPerson string `gorm:"column:warning_person" json:"warning_person"`
StartTime *time.Time `gorm:"column:start_time;not null" json:"start_time"` StartTime *time.Time `gorm:"column:start_time;not null" json:"start_time"`
EndTime *time.Time `gorm:"column:end_time;not null" json:"end_time"` EndTime *time.Time `gorm:"column:end_time;not null" json:"end_time"`
CreateTime *time.Time `gorm:"column:create_time;not null" json:"create_time"` CreateTime *time.Time `gorm:"column:create_time;not null" json:"create_time"`

View File

@ -11,6 +11,7 @@ import (
"time" "time"
err2 "voucher/api/err" err2 "voucher/api/err"
"voucher/internal/biz/bo" "voucher/internal/biz/bo"
"voucher/internal/biz/do"
"voucher/internal/biz/repo" "voucher/internal/biz/repo"
"voucher/internal/biz/vo" "voucher/internal/biz/vo"
"voucher/internal/data" "voucher/internal/data"
@ -37,9 +38,10 @@ func (r *ProductRepoImpl) FindWarningBudget(ctx context.Context, fun func(ctx co
nowTime := time.Now().Format(time.DateTime) nowTime := time.Now().Format(time.DateTime)
result := r.db.DB(ctx). result := r.db.DB(ctx).
Where("warning_budget > 0").
Where("start_time <= ?", nowTime). Where("start_time <= ?", nowTime).
Where("end_time >= ?", nowTime). Where("start_time <= ?", nowTime).
Where("warning_budget > 0").
Where("warning_person IS NOT NULL").
FindInBatches(&results, 5, func(tx *gorm.DB, batch int) error { FindInBatches(&results, 5, func(tx *gorm.DB, batch int) error {
return fun(ctx, r.ToBos(results)) return fun(ctx, r.ToBos(results))
}) })
@ -51,6 +53,36 @@ func (r *ProductRepoImpl) FindWarningBudget(ctx context.Context, fun func(ctx co
return nil return nil
} }
func (r *ProductRepoImpl) UpdateWarningBudget(ctx context.Context, id int32, req *do.WarningBudget) error {
now := time.Now()
u := model.Product{
AllBudget: req.AllBudget,
RemainingBudget: req.RemainingBudget,
UpdateTime: &now,
}
if req.StartTime != nil {
u.StartTime = req.StartTime
}
if req.EndTime != nil {
u.EndTime = req.EndTime
}
tx := r.db.DB(ctx).
Where(model.Product{
ID: id,
}).
Updates(u)
if tx.Error != nil {
return fmt.Errorf("db fail %w", tx.Error)
}
return nil
}
func (r *ProductRepoImpl) GetByBatchNo(ctx context.Context, batchNo string) (*bo.ProductBo, error) { func (r *ProductRepoImpl) GetByBatchNo(ctx context.Context, batchNo string) (*bo.ProductBo, error) {
var item *model.Product var item *model.Product

View File

@ -2,19 +2,23 @@ package sms
import ( import (
"context" "context"
"encoding/json"
"fmt"
"strconv"
"testing" "testing"
"voucher/internal/biz/do"
) )
func TestSendSMS(t *testing.T) { var config = Config{
config := Config{
AccessKeyID: "LTAI5tM1X4HuqUwT8D74qXAH", AccessKeyID: "LTAI5tM1X4HuqUwT8D74qXAH",
AccessKeySecret: "gxsC1QK12NSKH1HkCqKR1EnMdAy3ad", AccessKeySecret: "gxsC1QK12NSKH1HkCqKR1EnMdAy3ad",
Endpoint: "dysmsapi.aliyuncs.com", Endpoint: "dysmsapi.aliyuncs.com",
SignName: "蓝色兄弟", SignName: "蓝色兄弟",
RetryTimes: 0, RetryTimes: 0,
Timeout: 15, Timeout: 15,
} }
func TestSendSMS(t *testing.T) {
smsService, err := NewService(config) smsService, err := NewService(config)
if err != nil { if err != nil {
@ -37,3 +41,51 @@ func TestSendSMS(t *testing.T) {
t.Logf("已发送至 %s\n", phoneNumber) t.Logf("已发送至 %s\n", phoneNumber)
} }
func TestWarningSend(t *testing.T) {
req := do.WarningBudget{
StockName: "招行2元立减金",
StockId: "20627186",
StockNo: "CMB20627186",
Amount: 2,
AllBudget: 70010,
AllStock: 35005,
UsedStock: 9390,
UsedBudget: 18780,
AvailableStock: 25615,
RemainingBudget: 51230,
StockUsageRate: 20,
}
params := buildTemplateParams(&req)
js, _ := json.Marshal(params)
t.Log(string(js))
smsService, err := NewService(config)
if err != nil {
t.Errorf("创建短信服务失败: %v", err)
return
}
err = smsService.SendSMS(context.Background(), []string{"18666173766"}, "", params)
if err != nil {
t.Errorf("发送短信失败: %v", err)
return
}
}
func buildTemplateParams(req *do.WarningBudget) map[string]string {
return map[string]string{
"stock_name": req.StockName,
"stock_no": req.StockNo,
"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)),
"remaining_budget": strconv.Itoa(int(req.RemainingBudget)),
"remaining_stock": strconv.Itoa(int(req.AvailableStock)),
"budget_usage_rate": fmt.Sprintf("%.1f", req.StockUsageRate),
}
}