diff --git a/.gitignore b/.gitignore index b3a0095..2c999f1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ Thumbs.db *.swp .vscode/ .idea/ +.trae/ + bin/ cert/ log diff --git a/internal/biz/bo/wechat_notify_bo.go b/internal/biz/bo/wechat_notify_bo.go index 26c4007..d8c35ae 100644 --- a/internal/biz/bo/wechat_notify_bo.go +++ b/internal/biz/bo/wechat_notify_bo.go @@ -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 +} diff --git a/internal/biz/multi.go b/internal/biz/multi.go index a6f4983..598a92c 100644 --- a/internal/biz/multi.go +++ b/internal/biz/multi.go @@ -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 == "" { diff --git a/internal/biz/repo/multi_notify_log.go b/internal/biz/repo/multi_notify_log.go index a43f049..470d38e 100644 --- a/internal/biz/repo/multi_notify_log.go +++ b/internal/biz/repo/multi_notify_log.go @@ -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 } diff --git a/internal/biz/wechat_notify.go b/internal/biz/wechat_notify.go index 0c3206c..9de75dc 100644 --- a/internal/biz/wechat_notify.go +++ b/internal/biz/wechat_notify.go @@ -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) diff --git a/internal/data/repoimpl/multi_notify_log.go b/internal/data/repoimpl/multi_notify_log.go index d1f4cec..ba12dca 100644 --- a/internal/data/repoimpl/multi_notify_log.go +++ b/internal/data/repoimpl/multi_notify_log.go @@ -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()