diff --git a/internal/biz/do/warning_budget.go b/internal/biz/do/warning_budget.go index d142ddd..ff9d165 100644 --- a/internal/biz/do/warning_budget.go +++ b/internal/biz/do/warning_budget.go @@ -3,16 +3,20 @@ package do import "time" type WarningBudget struct { - StockName string // 券名称 - StockId string // 券ID - StockNo string // 券编号 - Amount int64 // 券面额 - AllBudget int64 // 总预算 - AllStock int64 // 总库存 - UsedStock int64 // 已使用库存 - UsedBudget int64 // 已使用预算 - AvailableStock int64 // 可用库存 - RemainingBudget int64 // 剩余预算 + StockName string // 券名称 + StockId string // 券ID + StockNo string // 券编号 + Amount int64 // 券面额 + AllBudget int64 // 总预算 + AllStock int64 // 总库存 + UsedStock int64 // 已使用库存 + 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 +} diff --git a/internal/biz/mixrepos/sms.go b/internal/biz/mixrepos/sms.go index 5624301..31689ae 100644 --- a/internal/biz/mixrepos/sms.go +++ b/internal/biz/mixrepos/sms.go @@ -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 } diff --git a/internal/biz/register_tag.go b/internal/biz/register_tag.go index 90eab42..3ec029e 100644 --- a/internal/biz/register_tag.go +++ b/internal/biz/register_tag.go @@ -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 } diff --git a/internal/biz/repo/product.go b/internal/biz/repo/product.go index 8d2e9fc..eb93abe 100644 --- a/internal/biz/repo/product.go +++ b/internal/biz/repo/product.go @@ -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 } diff --git a/internal/biz/warning_budget.go b/internal/biz/warning_budget.go index 37f514b..e151bd3 100644 --- a/internal/biz/warning_budget.go +++ b/internal/biz/warning_budget.go @@ -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) - if err != nil { - return err + 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() } diff --git a/internal/data/mixrepoimpl/ding.go b/internal/data/mixrepoimpl/ding.go index d831f0a..9a5b39c 100644 --- a/internal/data/mixrepoimpl/ding.go +++ b/internal/data/mixrepoimpl/ding.go @@ -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 diff --git a/internal/data/mixrepoimpl/sms.go b/internal/data/mixrepoimpl/sms.go index aff2bee..0d4c811 100644 --- a/internal/data/mixrepoimpl/sms.go +++ b/internal/data/mixrepoimpl/sms.go @@ -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) } diff --git a/internal/data/model/product.gen.go b/internal/data/model/product.gen.go index 5f014b3..8dcd391 100644 --- a/internal/data/model/product.gen.go +++ b/internal/data/model/product.gen.go @@ -12,20 +12,23 @@ const TableNameProduct = "product" // Product mapped from table type Product struct { - ID int32 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"` - Name string `gorm:"column:name;not null;comment:商品名称" json:"name"` // 商品名称 - ProductNo string `gorm:"column:product_no;not null;comment:商品编号" json:"product_no"` // 商品编号 - BatchName string `gorm:"column:batch_name;not null;comment:批次名称" json:"batch_name"` // 批次名称 - BatchNo string `gorm:"column:batch_no;not null;comment:立减金批次号" json:"batch_no"` // 立减金批次号 - MchId string `gorm:"column:mch_id;not null;comment:商户号,创建批次的商户号" json:"mch_id"` // 商户号,创建批次的商户号 - 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"` - WarningBudget int64 `gorm:"column:warning_budget;not null;default:0" json:"warning_budget"` // 预警预算=0不做预警 - 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"` - UpdateTime *time.Time `gorm:"column:update_time" json:"update_time"` + ID int32 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"` + Name string `gorm:"column:name;not null;comment:商品名称" json:"name"` // 商品名称 + ProductNo string `gorm:"column:product_no;not null;comment:商品编号" json:"product_no"` // 商品编号 + BatchName string `gorm:"column:batch_name;not null;comment:批次名称" json:"batch_name"` // 批次名称 + BatchNo string `gorm:"column:batch_no;not null;comment:立减金批次号" json:"batch_no"` // 立减金批次号 + MchId string `gorm:"column:mch_id;not null;comment:商户号,创建批次的商户号" json:"mch_id"` // 商户号,创建批次的商户号 + 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"` + UpdateTime *time.Time `gorm:"column:update_time" json:"update_time"` } // TableName Product's table name diff --git a/internal/data/repoimpl/product.go b/internal/data/repoimpl/product.go index 56e45e4..ad70199 100644 --- a/internal/data/repoimpl/product.go +++ b/internal/data/repoimpl/product.go @@ -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 diff --git a/internal/pkg/sms/sms_test.go b/internal/pkg/sms/sms_test.go index b865d44..17847fb 100644 --- a/internal/pkg/sms/sms_test.go +++ b/internal/pkg/sms/sms_test.go @@ -2,19 +2,23 @@ package sms import ( "context" + "encoding/json" + "fmt" + "strconv" "testing" + "voucher/internal/biz/do" ) -func TestSendSMS(t *testing.T) { +var config = Config{ + AccessKeyID: "LTAI5tM1X4HuqUwT8D74qXAH", + AccessKeySecret: "gxsC1QK12NSKH1HkCqKR1EnMdAy3ad", + Endpoint: "dysmsapi.aliyuncs.com", + SignName: "蓝色兄弟", + RetryTimes: 0, + Timeout: 15, +} - config := Config{ - AccessKeyID: "LTAI5tM1X4HuqUwT8D74qXAH", - AccessKeySecret: "gxsC1QK12NSKH1HkCqKR1EnMdAy3ad", - Endpoint: "dysmsapi.aliyuncs.com", - SignName: "蓝色兄弟", - RetryTimes: 0, - Timeout: 15, - } +func TestSendSMS(t *testing.T) { smsService, err := NewService(config) if err != nil { @@ -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), + } +}