diff --git a/internal/bootstrap/services.go b/internal/bootstrap/services.go index 28acb97..e0c947a 100644 --- a/internal/bootstrap/services.go +++ b/internal/bootstrap/services.go @@ -142,7 +142,7 @@ func initServices(s *stores, deps *Dependencies) *services { iotCard.SetDataDeductor(usageService) // 创建支付配置服务(Order 和 Recharge 依赖) - wechatConfig := wechatConfigSvc.New(s.WechatConfig, s.Order, s.RechargeOrder, s.AgentRecharge, accountAudit, deps.Redis, deps.Logger) + wechatConfig := wechatConfigSvc.New(s.WechatConfig, s.Order, s.RechargeOrder, s.AgentRecharge, s.Payment, accountAudit, deps.Redis, deps.Logger) // 创建支付配置动态加载器(Order 和 Recharge 依赖) paymentLoader := payment.NewPaymentConfigLoader(s.WechatConfig, deps.Redis, deps.Logger) diff --git a/internal/handler/app/client_wallet.go b/internal/handler/app/client_wallet.go index ae79daf..441c71b 100644 --- a/internal/handler/app/client_wallet.go +++ b/internal/handler/app/client_wallet.go @@ -298,6 +298,7 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error { } rechargeNo := generateClientRechargeNo() + paymentNo := generateClientPaymentNo() // 先初始化微信支付实例并创建预支付订单,确认支付通道可用 // 避免先写入充值记录后支付初始化失败,导致产生孤儿记录 @@ -305,7 +306,7 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error { resolved.SkipPermissionCtx, config, appID, - rechargeNo, + paymentNo, "资产钱包充值", openID, int(req.Amount), @@ -334,7 +335,7 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error { } payment := &model.Payment{ - PaymentNo: rechargeNo, + PaymentNo: paymentNo, OrderID: rechargeOrder.ID, OrderType: model.PaymentOrderTypeRecharge, PaymentMethod: model.PaymentByWechat, @@ -639,6 +640,10 @@ func generateClientRechargeNo() string { return fmt.Sprintf("%s%s%06d", constants.AssetRechargeOrderPrefix, timestamp, randomNum) } +func generateClientPaymentNo() string { + return fmt.Sprintf("PAY%d%06d", time.Now().UnixNano()/1e6, rand.Intn(1000000)) +} + func buildClientRechargePayConfig(appID string, result *wechat.JSAPIPayResult) dto.ClientRechargePayConfig { resp := dto.ClientRechargePayConfig{AppID: appID} if result == nil || result.PayConfig == nil { diff --git a/internal/handler/callback/payment.go b/internal/handler/callback/payment.go index e3508b0..b19d105 100644 --- a/internal/handler/callback/payment.go +++ b/internal/handler/callback/payment.go @@ -21,6 +21,7 @@ import ( "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/fuiou" "github.com/break/junhong_cmp_fiber/pkg/wechat" + "gorm.io/gorm" ) // AgentRechargeServiceInterface 代理充值服务接口 @@ -128,8 +129,36 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error { return h.wechatV2SuccessResponse(c) } -// dispatchWechatCallback 按订单号前缀将支付结果分发到对应业务 +func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, paymentNo, paymentMethod, transactionID string, paidAmount int64) (bool, error) { + if h.paymentStore != nil { + payment, err := h.paymentStore.GetByPaymentNo(ctx, paymentNo) + if err == nil { + switch payment.OrderType { + case model.PaymentOrderTypePackage: + return true, h.orderService.HandlePaymentRecordCallback(ctx, paymentNo, paymentMethod, transactionID, paidAmount) + case model.PaymentOrderTypeRecharge: + if h.rechargeOrderService != nil { + return true, h.rechargeOrderService.HandlePaymentCallback(ctx, paymentNo, paymentMethod, transactionID) + } + return true, fmt.Errorf("充值订单服务未配置,无法处理支付单: %s", paymentNo) + default: + return true, fmt.Errorf("未知支付记录类型: %s", payment.OrderType) + } + } + if err != gorm.ErrRecordNotFound { + return true, errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败") + } + } + return false, nil +} + +// dispatchWechatCallback 优先按支付单分发,旧单号继续按前缀兼容处理。 func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, outTradeNo, transactionID string, paidAmount int64) error { + handled, err := h.dispatchPaymentRecordCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID, paidAmount) + if handled || err != nil { + return err + } + switch { case strings.HasPrefix(outTradeNo, "ORD"): return h.orderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, paidAmount) @@ -196,6 +225,14 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error { } paidAmount := parseYuanToFen(rawAmount) + handled, err := h.dispatchPaymentRecordCallback(ctx, req.OrderNo, model.PaymentMethodAlipay, "", paidAmount) + if err != nil { + return err + } + if handled { + return c.SendString("success") + } + // 按订单号前缀分发 switch { case strings.HasPrefix(req.OrderNo, "ORD"): @@ -277,6 +314,12 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error { orderNo := notify.MchntOrderNo // OrderAmt 为字符串格式的分,解析失败则降级为 0 orderAmt, _ := strconv.ParseInt(notify.OrderAmt, 10, 64) + if handled, err := h.dispatchPaymentRecordCallback(ctx, orderNo, "fuiou", notify.TransactionId, orderAmt); err != nil { + return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) + } else if handled { + return c.Send(fuiou.BuildNotifySuccessResponse()) + } + switch { case strings.HasPrefix(orderNo, "ORD"): if err := h.orderService.HandlePaymentCallback(ctx, orderNo, "fuiou", orderAmt); err != nil { diff --git a/internal/service/client_order/service.go b/internal/service/client_order/service.go index f1f19c9..748d697 100644 --- a/internal/service/client_order/service.go +++ b/internal/service/client_order/service.go @@ -418,7 +418,7 @@ func (s *Service) createForceRechargeOrder( return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败") } - paymentNo := generateClientRechargeNo() + paymentNo := generateClientPaymentNo() payment := &model.Payment{ PaymentNo: paymentNo, OrderID: rechargeOrder.ID, @@ -433,7 +433,7 @@ func (s *Service) createForceRechargeOrder( return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败") } - paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, rechargeOrderNo, "余额充值", openID, int(rechargeOrder.Amount)) + paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, paymentNo, "余额充值", openID, int(rechargeOrder.Amount)) if err != nil { return nil, err } @@ -809,6 +809,10 @@ func generateClientRechargeNo() string { return fmt.Sprintf("CRCH%d%06d", time.Now().UnixNano()/1e6, rand.Intn(1000000)) } +func generateClientPaymentNo() string { + return fmt.Sprintf("PAY%d%06d", time.Now().UnixNano()/1e6, rand.Intn(1000000)) +} + // ListOrders 查询 C 端订单列表。 func (s *Service) ListOrders(ctx context.Context, customerID uint, req *dto.ClientOrderListRequest) ([]dto.ClientOrderListItem, int64, error) { skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{}) @@ -1012,15 +1016,41 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r return nil, err } - // 记录本次支付使用的微信配置,供支付回调验签使用 - if err := s.db.WithContext(skipCtx).Model(&model.Order{}). - Where("id = ?", orderID). - Update("payment_config_id", activeConfig.ID).Error; err != nil { - return nil, errors.Wrap(errors.CodeDatabaseError, err, "更新订单支付配置失败") + paymentNo := generateClientPaymentNo() + payment := &model.Payment{ + PaymentNo: paymentNo, + OrderID: order.ID, + OrderType: model.PaymentOrderTypePackage, + PaymentMethod: model.PaymentByWechat, + Amount: order.TotalAmount, + Status: model.PaymentRecordStatusPending, + PaymentConfigID: &activeConfig.ID, } - paymentResult, err := paymentProvider.CreateJSAPIPayment(skipCtx, order.OrderNo, "套餐购买", openID, int(order.TotalAmount)) + if err := s.db.WithContext(skipCtx).Transaction(func(tx *gorm.DB) error { + // 订单仍保留最近一次支付配置,兼容旧 ORD 回调。 + if err := tx.Model(&model.Order{}). + Where("id = ?", orderID). + Update("payment_config_id", activeConfig.ID).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "更新订单支付配置失败") + } + if err := s.paymentStore.CreateWithTx(skipCtx, tx, payment); err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败") + } + return nil + }); err != nil { + return nil, err + } + + paymentResult, err := paymentProvider.CreateJSAPIPayment(skipCtx, paymentNo, "套餐购买", openID, int(order.TotalAmount)) if err != nil { + if updateErr := s.paymentStore.UpdateStatus(skipCtx, payment.ID, model.PaymentRecordStatusFailed); updateErr != nil { + s.logger.Warn("标记支付记录失败状态失败", + zap.Uint("payment_id", payment.ID), + zap.String("payment_no", paymentNo), + zap.Error(updateErr), + ) + } return nil, err } return &dto.ClientPayOrderResponse{ diff --git a/internal/service/order/service.go b/internal/service/order/service.go index 78f97cf..26f5c96 100644 --- a/internal/service/order/service.go +++ b/internal/service/order/service.go @@ -1778,6 +1778,106 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, pay return nil } +// HandlePaymentRecordCallback 通过支付单处理套餐订单第三方支付回调。 +func (s *Service) HandlePaymentRecordCallback(ctx context.Context, paymentNo string, paymentMethod string, thirdPartyTradeNo string, actualPaidAmount int64) error { + payment, err := s.paymentStore.GetByPaymentNo(ctx, paymentNo) + if err != nil { + if err == gorm.ErrRecordNotFound { + return errors.New(errors.CodeNotFound, "支付记录不存在") + } + return errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败") + } + if payment.OrderType != model.PaymentOrderTypePackage { + return errors.New(errors.CodeInvalidParam, "支付记录类型不匹配") + } + + order, err := s.orderStore.GetByID(ctx, payment.OrderID) + if err != nil { + if err == gorm.ErrRecordNotFound { + return errors.New(errors.CodeNotFound, "订单不存在") + } + return errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败") + } + + now := time.Now() + shouldResumeAfterPayment := false + shouldEnqueueCommission := false + err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + paymentUpdates := map[string]any{ + "status": model.PaymentRecordStatusPaid, + "paid_at": now, + } + if thirdPartyTradeNo != "" { + paymentUpdates["third_party_trade_no"] = thirdPartyTradeNo + } + paymentResult := tx.Model(&model.Payment{}). + Where("id = ? AND status = ?", payment.ID, model.PaymentRecordStatusPending). + Updates(paymentUpdates) + if paymentResult.Error != nil { + return errors.Wrap(errors.CodeDatabaseError, paymentResult.Error, "更新支付记录失败") + } + if paymentResult.RowsAffected == 0 { + var currentPayment model.Payment + if err := tx.First(¤tPayment, payment.ID).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败") + } + if currentPayment.Status == model.PaymentRecordStatusPaid { + return nil + } + return errors.New(errors.CodeInvalidStatus, "支付记录状态不允许处理") + } + + result := tx.Model(&model.Order{}). + Where("id = ? AND payment_status = ?", order.ID, model.PaymentStatusPending). + Updates(map[string]any{ + "payment_status": model.PaymentStatusPaid, + "payment_method": paymentMethod, + "paid_at": now, + "expires_at": nil, + "actual_paid_amount": actualPaidAmount, + }) + if result.Error != nil { + return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败") + } + if result.RowsAffected == 0 { + var currentOrder model.Order + if err := tx.First(¤tOrder, order.ID).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败") + } + + switch currentOrder.PaymentStatus { + case model.PaymentStatusPaid: + return nil + case model.PaymentStatusCancelled: + return errors.New(errors.CodeInvalidStatus, "订单已取消,无法支付") + case model.PaymentStatusRefunded: + return errors.New(errors.CodeInvalidStatus, "订单已退款,无法支付") + default: + return errors.New(errors.CodeInvalidStatus, "订单状态异常") + } + } + + actualPaidAmountSnapshot := actualPaidAmount + order.ActualPaidAmount = &actualPaidAmountSnapshot + shouldResumeAfterPayment = true + shouldEnqueueCommission = true + + return s.activatePackage(ctx, tx, order) + }) + + if err != nil { + return err + } + + if shouldResumeAfterPayment { + s.tryResumeAfterPayment(ctx, order) + } + if shouldEnqueueCommission && order.CommissionStatus == model.CommissionStatusPending { + s.enqueueCommissionCalculation(ctx, order.ID) + } + return nil +} + // tryResumeAfterPayment 支付成功后检查是否应触发自动复机 // 仅当本次支付直接激活了主套餐(status=1,非排队)时才调用复机,避免虚流量已超但套餐排队时错误复机 func (s *Service) tryResumeAfterPayment(ctx context.Context, order *model.Order) { diff --git a/internal/service/wechat_config/service.go b/internal/service/wechat_config/service.go index 68b3851..42f1d59 100644 --- a/internal/service/wechat_config/service.go +++ b/internal/service/wechat_config/service.go @@ -35,6 +35,7 @@ type Service struct { orderStore *postgres.OrderStore rechargeOrderStore *postgres.RechargeOrderStore agentRechargeStore *postgres.AgentRechargeStore + paymentStore *postgres.PaymentStore auditService AuditServiceInterface redis *redis.Client logger *zap.Logger @@ -46,6 +47,7 @@ func New( orderStore *postgres.OrderStore, rechargeOrderStore *postgres.RechargeOrderStore, agentRechargeStore *postgres.AgentRechargeStore, + paymentStore *postgres.PaymentStore, auditService AuditServiceInterface, rdb *redis.Client, logger *zap.Logger, @@ -55,6 +57,7 @@ func New( orderStore: orderStore, rechargeOrderStore: rechargeOrderStore, agentRechargeStore: agentRechargeStore, + paymentStore: paymentStore, auditService: auditService, redis: rdb, logger: logger, @@ -512,6 +515,17 @@ func (s *Service) GetConfigForCallback(ctx context.Context, orderNo string) (*mo // resolvePaymentConfigID 按订单号前缀从对应表中取 payment_config_id func (s *Service) resolvePaymentConfigID(ctx context.Context, orderNo string) (*uint, error) { + if s.paymentStore != nil { + payment, err := s.paymentStore.GetByPaymentNo(ctx, orderNo) + if err == nil { + if payment.PaymentConfigID != nil { + return payment.PaymentConfigID, nil + } + } else if err != gorm.ErrRecordNotFound { + return nil, err + } + } + switch { case len(orderNo) >= 3 && orderNo[:3] == "ORD": order, err := s.orderStore.GetByOrderNo(ctx, orderNo)