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 // 已使用预算
AvailableStock int64 // 可用库存
RemainingBudget int64 // 剩余预算
StockUsageRate float64 // 券使用率
StartTime *time.Time
EndTime *time.Time
}
type WarningBudgetLog struct {
@ -20,3 +24,13 @@ type WarningBudgetLog struct {
Num int
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"
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
}
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 {
return err
}

View File

@ -3,10 +3,12 @@ package repo
import (
"context"
"voucher/internal/biz/bo"
"voucher/internal/biz/do"
)
type ProductRepo interface {
FindWarningBudget(ctx context.Context, fun func(ctx context.Context, rows []*bo.ProductBo) error) error
GetByBatchNo(ctx context.Context, batchNo 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
}
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
couponAmount := *wxResp.StockUseRule.FixedNormalCoupon.CouponAmount / 100
remainingBudget := availableStock * couponAmount
stockUsageRate := float64(*wxResp.DistributedCoupons) / float64(*wxResp.StockUseRule.MaxCoupons) * 100
req := &do.WarningBudget{
StockName: product.Name,
StockId: product.BatchNo,
@ -156,37 +158,55 @@ func (v *VoucherBiz) Calculate(ctx context.Context, product *bo.ProductBo, wxRes
UsedBudget: *wxResp.DistributedCoupons * couponAmount,
AvailableStock: availableStock,
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 {
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 {
return v.WarningSend(ctx, str)
return v.WarningSend(ctx, formatAsCard(req))
} else {
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
}
func (v *VoucherBiz) WarningSend(ctx context.Context, str string) error {
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("\n")
// 使用率
stockUsageRate := float64(req.UsedStock) / float64(req.AllStock) * 100
card.WriteString("#### 📈 使用率\n")
card.WriteString(fmt.Sprintf("- **使用率**: %.1f%%\n", stockUsageRate))
card.WriteString(fmt.Sprintf("- **使用率**: %.1f%%\n", req.StockUsageRate))
return card.String()
}

View File

@ -20,6 +20,20 @@ func NewDingMixRepoImpl(bc *conf.Bootstrap) mixrepos.DingMixRepo {
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 {
isAtAll := false

View File

@ -31,6 +31,6 @@ func NewSmsMixRepoImpl(bc *conf.Bootstrap) (mixrepos.SmsMixRepo, error) {
return &SmsMixRepoImpl{bc: bc, smsService: smsService}, nil
}
func (s *SmsMixRepoImpl) SendCode(ctx context.Context, phone, code string) error {
return s.smsService.SendSMS(ctx, []string{phone}, s.bc.AliYunSms.TemplateSendCode, map[string]string{"code": code})
func (s *SmsMixRepoImpl) Send(ctx context.Context, phoneNumbers []string, params map[string]string) error {
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:支付宝
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"`
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不做预警
WarningPerson string `gorm:"column:warning_person" json:"warning_person"`
StartTime *time.Time `gorm:"column:start_time;not null" json:"start_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"`

View File

@ -11,6 +11,7 @@ import (
"time"
err2 "voucher/api/err"
"voucher/internal/biz/bo"
"voucher/internal/biz/do"
"voucher/internal/biz/repo"
"voucher/internal/biz/vo"
"voucher/internal/data"
@ -37,9 +38,10 @@ func (r *ProductRepoImpl) FindWarningBudget(ctx context.Context, fun func(ctx co
nowTime := time.Now().Format(time.DateTime)
result := r.db.DB(ctx).
Where("warning_budget > 0").
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 {
return fun(ctx, r.ToBos(results))
})
@ -51,6 +53,36 @@ func (r *ProductRepoImpl) FindWarningBudget(ctx context.Context, fun func(ctx co
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) {
var item *model.Product

View File

@ -2,12 +2,14 @@ package sms
import (
"context"
"encoding/json"
"fmt"
"strconv"
"testing"
"voucher/internal/biz/do"
)
func TestSendSMS(t *testing.T) {
config := Config{
var config = Config{
AccessKeyID: "LTAI5tM1X4HuqUwT8D74qXAH",
AccessKeySecret: "gxsC1QK12NSKH1HkCqKR1EnMdAy3ad",
Endpoint: "dysmsapi.aliyuncs.com",
@ -16,6 +18,8 @@ func TestSendSMS(t *testing.T) {
Timeout: 15,
}
func TestSendSMS(t *testing.T) {
smsService, err := NewService(config)
if err != nil {
t.Errorf("创建短信服务失败: %v", err)
@ -37,3 +41,51 @@ func TestSendSMS(t *testing.T) {
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),
}
}