feat(multi-notify): 增加合单支付回调

This commit is contained in:
fuzhongyun 2026-05-28 16:27:05 +08:00
parent b5b07a4d79
commit 5e4b072062
6 changed files with 172 additions and 11 deletions

2
.gitignore vendored
View File

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

View File

@ -16,6 +16,19 @@ type ConsumeInformation struct {
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 定义明文数据结构体
type PlainText struct {
StockCreatorMchid string `json:"stock_creator_mchid" validate:"required"`
@ -29,7 +42,9 @@ type PlainText struct {
NoCash bool `json:"no_cash"`
Singleitem bool `json:"singleitem"`
BusinessType string `json:"business_type"` // 业务类型
IsCombineOrder bool `json:"is_combine_order,omitempty"`
ConsumeInformation *ConsumeInformation `json:"consume_information,omitempty"`
CombineOrderInfo *CombineOrderInfo `json:"combine_order_info,omitempty"`
}
type WechatVoucherNotifyBo struct {
@ -63,3 +78,39 @@ func (c *WechatVoucherNotifyBo) Validate() error {
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 {
if req.PlainText.ConsumeInformation.ConsumeAmount == 0 {
return fmt.Errorf("消费金额不能为0")
if err = req.ValidateMultiNotify(); err != nil {
return fmt.Errorf("multi validate req error: %v", err)
}
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.NoticeNum > 0 {
if !req.PlainText.IsCombineOrder && mnd.NoticeNum > 0 {
log.Warnf("[%s] multi notify log already exists,req:%+v", source, req)
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 {
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 {
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 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 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)
}
} 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)
}
}
} 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)
}
}
@ -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)
}
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{
Source: source,
IP: ip,
@ -201,15 +246,57 @@ func (biz *MultiBiz) mndCreate(ctx context.Context, ip, source string, req *bo.W
OutBizNo: order.OutBizNo,
CouponID: req.PlainText.CouponID,
StockID: req.PlainText.StockID,
ConsumeAmount: int32(req.PlainText.ConsumeInformation.ConsumeAmount),
ConsumeTime: &req.PlainText.ConsumeInformation.ConsumeTime,
TransactionID: req.PlainText.ConsumeInformation.TransactionID,
ConsumeAmount: consumeAmount,
ConsumeTime: consumeTime,
TransactionID: transactionID,
EventType: req.EventType,
Status: req.PlainText.Status,
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) {
if biz.bc.Cmb.MultiNotifyUrl == "" {

View File

@ -8,6 +8,7 @@ import (
type MultiNotifyLogRepo interface {
Create(ctx context.Context, req *bo.MultiNotifyLogBo) (*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
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 err = req.Validate(); err != nil {
if err = req.ValidateMultiNotify(); err != nil {
return fmt.Errorf("multi validate req error: %v", err)
}
return this.MultiBiz.Run(ctx, ip, req.PlainText.StockCreatorMchid, req, order)

View File

@ -2,8 +2,8 @@ package repoimpl
import (
"context"
"errors"
"fmt"
"gorm.io/gorm"
"time"
"unicode/utf8"
err2 "voucher/api/err"
@ -12,6 +12,8 @@ import (
"voucher/internal/biz/vo"
"voucher/internal/data"
"voucher/internal/data/model"
"gorm.io/gorm"
)
// MultiNotifyLogRepoImpl .
@ -77,6 +79,24 @@ func (p *MultiNotifyLogRepoImpl) GetByID(ctx context.Context, id int64) (*bo.Mul
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 {
now := time.Now()