Merge pull request 'feat(multi-notify): 增加合单支付回调' (#2) from feature/fzy/multi-notify into pro

Reviewed-on: #2
This commit is contained in:
fuzhongyun 2026-05-29 11:41:54 +08:00
commit 08355a0fa8
6 changed files with 172 additions and 11 deletions

2
.gitignore vendored
View File

@ -22,6 +22,8 @@ Thumbs.db
*.swp *.swp
.vscode/ .vscode/
.idea/ .idea/
.trae/
bin/ bin/
cert/ cert/
log log

View File

@ -16,6 +16,19 @@ type ConsumeInformation struct {
ConsumeAmount int `json:"consume_amount"` // 核销金额(单位:分) // 多笔立减金必须 validate:"required" ConsumeAmount int `json:"consume_amount"` // 核销金额(单位:分) // 多笔立减金必须 validate:"required"
} }
// CombineSubOrder 定义合单子单消费信息结构体
type CombineSubOrder struct {
TransactionID string `json:"transaction_id" validate:"required"` // 合单子单微信支付订单号
ConsumeAmount int `json:"comsume_amount" validate:"required"` // 子单核销金额,微信文档字段名如此定义
ConsumeTime time.Time `json:"consume_time" validate:"required"` // 子单核销时间
}
// CombineOrderInfo 定义合单订单信息结构体
type CombineOrderInfo struct {
CombineConsumeAmount int `json:"combine_consume_amount"` // 合单总核销金额
SubOrders []CombineSubOrder `json:"sub_orders,omitempty"` // 合单子单列表
}
// PlainText 定义明文数据结构体 // PlainText 定义明文数据结构体
type PlainText struct { type PlainText struct {
StockCreatorMchid string `json:"stock_creator_mchid" validate:"required"` StockCreatorMchid string `json:"stock_creator_mchid" validate:"required"`
@ -29,7 +42,9 @@ type PlainText struct {
NoCash bool `json:"no_cash"` NoCash bool `json:"no_cash"`
Singleitem bool `json:"singleitem"` Singleitem bool `json:"singleitem"`
BusinessType string `json:"business_type"` // 业务类型 BusinessType string `json:"business_type"` // 业务类型
IsCombineOrder bool `json:"is_combine_order,omitempty"`
ConsumeInformation *ConsumeInformation `json:"consume_information,omitempty"` ConsumeInformation *ConsumeInformation `json:"consume_information,omitempty"`
CombineOrderInfo *CombineOrderInfo `json:"combine_order_info,omitempty"`
} }
type WechatVoucherNotifyBo struct { type WechatVoucherNotifyBo struct {
@ -63,3 +78,39 @@ func (c *WechatVoucherNotifyBo) Validate() error {
return nil return nil
} }
func (c *WechatVoucherNotifyBo) ValidateMultiNotify() error {
if err := c.Validate(); err != nil {
return err
}
if c.PlainText.IsCombineOrder {
if c.PlainText.CombineOrderInfo == nil {
return fmt.Errorf("合单订单信息不能为空")
}
if len(c.PlainText.CombineOrderInfo.SubOrders) == 0 {
return fmt.Errorf("合单子单不能为空")
}
for _, subOrder := range c.PlainText.CombineOrderInfo.SubOrders {
if subOrder.TransactionID == "" {
return fmt.Errorf("合单子单微信支付订单号不能为空")
}
if subOrder.ConsumeAmount <= 0 {
return fmt.Errorf("合单子单核销金额必须大于0")
}
if subOrder.ConsumeTime.IsZero() {
return fmt.Errorf("合单子单核销时间不能为空")
}
}
return nil
}
if c.PlainText.ConsumeInformation == nil {
return fmt.Errorf("消费信息不能为空")
}
if c.PlainText.ConsumeInformation.ConsumeAmount <= 0 {
return fmt.Errorf("消费金额必须大于0")
}
return nil
}

View File

@ -71,8 +71,8 @@ func (biz *MultiBiz) Notify(ctx context.Context, ip, source string, req *bo.Wech
return lock.NewMutex(biz.rdb.Rdb, cl.TTL).Lock(ctx, cl.Key, func(ctx context.Context) error { return lock.NewMutex(biz.rdb.Rdb, cl.TTL).Lock(ctx, cl.Key, func(ctx context.Context) error {
if req.PlainText.ConsumeInformation.ConsumeAmount == 0 { if err = req.ValidateMultiNotify(); err != nil {
return fmt.Errorf("消费金额不能为0") return fmt.Errorf("multi validate req error: %v", err)
} }
order, err := biz.order(ctx, req) order, err := biz.order(ctx, req)
@ -115,7 +115,7 @@ func (biz *MultiBiz) Run(ctx context.Context, ip, source string, req *bo.WechatV
} }
if mnd != nil { if mnd != nil {
if mnd.NoticeNum > 0 { if !req.PlainText.IsCombineOrder && mnd.NoticeNum > 0 {
log.Warnf("[%s] multi notify log already exists,req:%+v", source, req) log.Warnf("[%s] multi notify log already exists,req:%+v", source, req)
return nil return nil
} }
@ -150,6 +150,14 @@ func (biz *MultiBiz) RetryRunByMultiNotifyDataId(ctx context.Context, multiNotif
} }
func (biz *MultiBiz) run(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) error { func (biz *MultiBiz) run(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) error {
if req.PlainText.IsCombineOrder {
return biz.runCombine(ctx, req, mnd, order)
}
return biz.runSingle(ctx, req, mnd, order)
}
func (biz *MultiBiz) runSingle(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) error {
// 如果核销金额为空,不再推送下游 // 如果核销金额为空,不再推送下游
if mnd.ConsumeAmount == 0 { if mnd.ConsumeAmount == 0 {
log.Warnf("[%s] multi notify log consume amount is 0,req:%+v", mnd.NotifyID, req) log.Warnf("[%s] multi notify log consume amount is 0,req:%+v", mnd.NotifyID, req)
@ -165,20 +173,50 @@ func (biz *MultiBiz) run(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd
return fmt.Errorf("请求错误 error: %v", err) return fmt.Errorf("请求错误 error: %v", err)
} }
return biz.updateOrderStatus(ctx, req, order)
}
func (biz *MultiBiz) runCombine(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) error {
for _, subOrder := range req.PlainText.CombineOrderInfo.SubOrders {
exists, err := biz.MultiNotifyLogRepo.ExistsSuccessByDataIDAndTransactionID(ctx, mnd.ID, subOrder.TransactionID)
if err != nil {
return fmt.Errorf("查询合单子单通知记录错误 error: %v", err)
}
if exists {
log.Warnf("[%s] combine sub order already notified,transaction_id:%s", mnd.NotifyID, subOrder.TransactionID)
continue
}
nl, request, err := biz.nlCreateBySubOrder(ctx, req, mnd, order, subOrder)
if err != nil {
return fmt.Errorf("创建合单子单通知日志错误 error: %v", err)
}
if err = biz.Request(ctx, mnd, nl, request); err != nil {
return fmt.Errorf("合单子单请求错误 error: %v", err)
}
}
return biz.updateOrderStatus(ctx, req, order)
}
func (biz *MultiBiz) updateOrderStatus(ctx context.Context, req *bo.WechatVoucherNotifyBo, order *bo.OrderBo) error {
consumeTime := req.PlainText.ConsumeInformation.ConsumeTime
if req.PlainText.Status.IsUsed() { if req.PlainText.Status.IsUsed() {
if order.Status.IsUse() { if order.Status.IsUse() {
if err = biz.OrderRepo.MultiOverUsed(ctx, order.ID, req.PlainText.ConsumeInformation.ConsumeTime, "再次核销完成"); err != nil { if err := biz.OrderRepo.MultiOverUsed(ctx, order.ID, consumeTime, "再次核销完成"); err != nil {
return fmt.Errorf("订单再次核销完成修改发生错误 error: %v", err) return fmt.Errorf("订单再次核销完成修改发生错误 error: %v", err)
} }
} else { } else {
if err = biz.OrderRepo.MultiOverUsed(ctx, order.ID, req.PlainText.ConsumeInformation.ConsumeTime, "核销完成"); err != nil { if err := biz.OrderRepo.MultiOverUsed(ctx, order.ID, consumeTime, "核销完成"); err != nil {
return fmt.Errorf("订单核销完成修改发生错误 error: %v", err) return fmt.Errorf("订单核销完成修改发生错误 error: %v", err)
} }
} }
} else { } else {
if err = biz.OrderRepo.MultiLastUsed(ctx, order.ID, req.PlainText.ConsumeInformation.ConsumeTime); err != nil { if err := biz.OrderRepo.MultiLastUsed(ctx, order.ID, consumeTime); err != nil {
return fmt.Errorf("订单核销修改发生错误 error: %v", err) return fmt.Errorf("订单核销修改发生错误 error: %v", err)
} }
} }
@ -193,6 +231,13 @@ func (biz *MultiBiz) mndCreate(ctx context.Context, ip, source string, req *bo.W
return nil, fmt.Errorf("通知数据 json str 错误 error: %v", err) return nil, fmt.Errorf("通知数据 json str 错误 error: %v", err)
} }
consumeAmount := int32(req.PlainText.ConsumeInformation.ConsumeAmount)
consumeTime := &req.PlainText.ConsumeInformation.ConsumeTime
transactionID := req.PlainText.ConsumeInformation.TransactionID
if req.PlainText.IsCombineOrder {
consumeAmount = int32(req.PlainText.CombineOrderInfo.CombineConsumeAmount)
}
return biz.MultiNotifyDataRepo.Create(ctx, &bo.MultiNotifyDataBo{ return biz.MultiNotifyDataRepo.Create(ctx, &bo.MultiNotifyDataBo{
Source: source, Source: source,
IP: ip, IP: ip,
@ -201,15 +246,57 @@ func (biz *MultiBiz) mndCreate(ctx context.Context, ip, source string, req *bo.W
OutBizNo: order.OutBizNo, OutBizNo: order.OutBizNo,
CouponID: req.PlainText.CouponID, CouponID: req.PlainText.CouponID,
StockID: req.PlainText.StockID, StockID: req.PlainText.StockID,
ConsumeAmount: int32(req.PlainText.ConsumeInformation.ConsumeAmount), ConsumeAmount: consumeAmount,
ConsumeTime: &req.PlainText.ConsumeInformation.ConsumeTime, ConsumeTime: consumeTime,
TransactionID: req.PlainText.ConsumeInformation.TransactionID, TransactionID: transactionID,
EventType: req.EventType, EventType: req.EventType,
Status: req.PlainText.Status, Status: req.PlainText.Status,
OriginalData: originalData, OriginalData: originalData,
}) })
} }
func (biz *MultiBiz) nlCreateBySubOrder(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo, subOrder bo.CombineSubOrder) (*bo.MultiNotifyLogBo, *v1.CmbRequest, error) {
if biz.bc.Cmb.MultiNotifyUrl == "" {
return nil, nil, fmt.Errorf("CMB多笔立减金通知地址为空")
}
consumeTime := subOrder.ConsumeTime
nl := &bo.MultiNotifyLogBo{
MultiNotifyDataID: mnd.ID,
OrderNo: mnd.OrderNo,
OutBizNo: mnd.OutBizNo,
CouponID: mnd.CouponID,
ActivityNo: order.ProductNo,
StockID: mnd.StockID,
EventType: mnd.EventType,
Status: req.PlainText.Status,
ConsumeAmount: int32(subOrder.ConsumeAmount),
ConsumeTime: &consumeTime,
TransactionID: subOrder.TransactionID,
RequestURL: biz.bc.Cmb.MultiNotifyUrl,
RequestStatus: vo.MultiNotifyLogStatusWait.GetValue(),
OrderCreateTime: order.CreateTime,
CouponCreateTime: &req.PlainText.CreateTime,
}
request, cmbRequestBo, err := biz.GetRequest(ctx, nl, order)
if err != nil {
return nil, nil, err
}
b, _ := json.Marshal(request)
nl.OriginReq = cmbRequestBo.BizContent
nl.Request = string(b)
res, err := biz.MultiNotifyLogRepo.Create(ctx, nl)
if err != nil {
return nil, nil, fmt.Errorf("创建通知日志错误 error: %v", err)
}
res.ConsumeTime = nl.ConsumeTime
return res, request, nil
}
func (biz *MultiBiz) nlCreate(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) (*bo.MultiNotifyLogBo, *v1.CmbRequest, error) { func (biz *MultiBiz) nlCreate(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) (*bo.MultiNotifyLogBo, *v1.CmbRequest, error) {
if biz.bc.Cmb.MultiNotifyUrl == "" { if biz.bc.Cmb.MultiNotifyUrl == "" {

View File

@ -8,6 +8,7 @@ import (
type MultiNotifyLogRepo interface { type MultiNotifyLogRepo interface {
Create(ctx context.Context, req *bo.MultiNotifyLogBo) (*bo.MultiNotifyLogBo, error) Create(ctx context.Context, req *bo.MultiNotifyLogBo) (*bo.MultiNotifyLogBo, error)
GetByID(ctx context.Context, id int64) (*bo.MultiNotifyLogBo, error) GetByID(ctx context.Context, id int64) (*bo.MultiNotifyLogBo, error)
ExistsSuccessByDataIDAndTransactionID(ctx context.Context, multiNotifyDataID int64, transactionID string) (bool, error)
Success(ctx context.Context, id int64, response string) error Success(ctx context.Context, id int64, response string) error
Fail(ctx context.Context, id int64, remark string) error Fail(ctx context.Context, id int64, remark string) error
} }

View File

@ -34,7 +34,7 @@ func (this *VoucherBiz) WechatNotifyConsumer(ctx context.Context, ip string, req
} }
if order.ActivityId != "" { if order.ActivityId != "" {
if err = req.Validate(); err != nil { if err = req.ValidateMultiNotify(); err != nil {
return fmt.Errorf("multi validate req error: %v", err) return fmt.Errorf("multi validate req error: %v", err)
} }
return this.MultiBiz.Run(ctx, ip, req.PlainText.StockCreatorMchid, req, order) return this.MultiBiz.Run(ctx, ip, req.PlainText.StockCreatorMchid, req, order)

View File

@ -2,8 +2,8 @@ package repoimpl
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gorm.io/gorm"
"time" "time"
"unicode/utf8" "unicode/utf8"
err2 "voucher/api/err" err2 "voucher/api/err"
@ -12,6 +12,8 @@ import (
"voucher/internal/biz/vo" "voucher/internal/biz/vo"
"voucher/internal/data" "voucher/internal/data"
"voucher/internal/data/model" "voucher/internal/data/model"
"gorm.io/gorm"
) )
// MultiNotifyLogRepoImpl . // MultiNotifyLogRepoImpl .
@ -77,6 +79,24 @@ func (p *MultiNotifyLogRepoImpl) GetByID(ctx context.Context, id int64) (*bo.Mul
return p.ToBo(&item), nil return p.ToBo(&item), nil
} }
func (p *MultiNotifyLogRepoImpl) ExistsSuccessByDataIDAndTransactionID(ctx context.Context, multiNotifyDataID int64, transactionID string) (bool, error) {
var item model.MultiNotifyLog
err := p.DB(ctx).
Select("id").
Where("multi_notify_data_id = ? AND transaction_id = ? AND request_status = ?", multiNotifyDataID, transactionID, vo.MultiNotifyLogStatusSuccess.GetValue()).
Limit(1).
Take(&item).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, err
}
return true, nil
}
func (p *MultiNotifyLogRepoImpl) Success(ctx context.Context, id int64, response string) error { func (p *MultiNotifyLogRepoImpl) Success(ctx context.Context, id int64, response string) error {
now := time.Now() now := time.Now()