diff --git a/internal/handler/app/client_wallet.go b/internal/handler/app/client_wallet.go index a1a145e..37b418a 100644 --- a/internal/handler/app/client_wallet.go +++ b/internal/handler/app/client_wallet.go @@ -15,6 +15,7 @@ import ( rechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge" wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config" "github.com/break/junhong_cmp_fiber/internal/store/postgres" + "github.com/break/junhong_cmp_fiber/pkg/alipay" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/fuiou" @@ -262,10 +263,6 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error { return errors.New(errors.CodeInvalidParam) } - if req.PaymentMethod != constants.RechargeMethodWechat { - return errors.New(errors.CodeInvalidParam) - } - resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier) if err != nil { return err @@ -289,6 +286,23 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error { return errors.New(errors.CodeWechatConfigUnavailable) } + switch req.PaymentMethod { + case constants.RechargeMethodAlipay: + return h.createAlipayRecharge(c, resolved, config, wallet, req) + default: + // 微信充值(默认):app_type 必填 + return h.createWechatRecharge(c, resolved, config, wallet, req) + } +} + +// createWechatRecharge 微信充值分支 +func (h *ClientWalletHandler) createWechatRecharge( + c *fiber.Ctx, + resolved *resolvedWalletAssetContext, + config *model.WechatConfig, + wallet *model.AssetWallet, + req dto.ClientCreateRechargeRequest, +) error { appID, err := pickAppIDByType(config, req.AppType) if err != nil { return err @@ -350,17 +364,99 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error { return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败") } - resp := &dto.ClientRechargeResponse{ + return response.Success(c, &dto.ClientRechargeResponse{ Recharge: dto.ClientRechargeResult{ RechargeID: rechargeOrder.ID, RechargeNo: rechargeOrder.RechargeOrderNo, Amount: rechargeOrder.Amount, Status: rechargeOrder.Status, }, - PayConfig: payConfig, + PayConfig: &payConfig, + }) +} + +// createAlipayRecharge 支付宝充值分支。 +// WAP URL 为本地签名生成,事务内先写 DB,再生成链接;链接失败则标记 payment failed。 +func (h *ClientWalletHandler) createAlipayRecharge( + c *fiber.Ctx, + resolved *resolvedWalletAssetContext, + config *model.WechatConfig, + wallet *model.AssetWallet, + req dto.ClientCreateRechargeRequest, +) error { + rechargeNo := generateClientRechargeNo() + paymentNo := generateClientPaymentNo() + expireAt := alipay.CalcExpireAt(config) + + rechargeOrder := &model.RechargeOrder{ + RechargeOrderNo: rechargeNo, + UserID: resolved.CustomerID, + AssetWalletID: wallet.ID, + ResourceType: resolved.ResourceType, + ResourceID: resolved.Asset.AssetID, + Amount: req.Amount, + Status: model.RechargeOrderStatusPending, + PaymentConfigID: &config.ID, + ShopIDTag: wallet.ShopIDTag, + EnterpriseIDTag: wallet.EnterpriseIDTag, + OperatorType: constants.OperatorTypePersonalCustomer, + Generation: resolved.Generation, + } + payment := &model.Payment{ + PaymentNo: paymentNo, + OrderType: model.PaymentOrderTypeRecharge, + PaymentMethod: model.PaymentByAlipay, + Amount: req.Amount, + Status: model.PaymentRecordStatusPending, + PaymentConfigID: &config.ID, + ExpireAt: &expireAt, } - return response.Success(c, resp) + // WAP URL 是本地签名,不需要先调第三方,在事务内创建充值单和支付单 + if err := h.db.WithContext(resolved.SkipPermissionCtx).Transaction(func(tx *gorm.DB) error { + if err := h.rechargeOrderStore.CreateWithTx(resolved.SkipPermissionCtx, tx, rechargeOrder); err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败") + } + payment.OrderID = rechargeOrder.ID + return h.paymentStore.CreateWithTx(resolved.SkipPermissionCtx, tx, payment) + }); err != nil { + return err + } + + wapURL, err := alipay.BuildWapPayURL(resolved.SkipPermissionCtx, config, payment, "资产钱包充值", config.AliReturnURL) + if err != nil { + if updateErr := h.paymentStore.UpdateStatus(resolved.SkipPermissionCtx, payment.ID, model.PaymentRecordStatusFailed); updateErr != nil { + h.logger.Warn("标记支付宝支付单 failed 失败", + zap.String("payment_no", paymentNo), + zap.Error(updateErr), + ) + } + return err + } + + h.logger.Info("支付宝充值支付单已创建", + zap.String("payment_no", paymentNo), + zap.String("recharge_no", rechargeNo), + zap.Int64("amount", req.Amount), + zap.Time("expire_at", expireAt), + zap.Uint("config_id", config.ID), + ) + + expireStr := expireAt.Format(time.RFC3339) + return response.Success(c, &dto.ClientRechargeResponse{ + Recharge: dto.ClientRechargeResult{ + RechargeID: rechargeOrder.ID, + RechargeNo: rechargeOrder.RechargeOrderNo, + Amount: rechargeOrder.Amount, + Status: rechargeOrder.Status, + }, + PaymentLink: &dto.ClientPaymentLink{ + PaymentNo: paymentNo, + QRLink: wapURL, + CopyLink: wapURL, + PayExpireAt: expireStr, + }, + }) } // GetRechargeList C5 充值记录列表 diff --git a/internal/model/dto/client_order_dto.go b/internal/model/dto/client_order_dto.go index 3b8bab3..ea29722 100644 --- a/internal/model/dto/client_order_dto.go +++ b/internal/model/dto/client_order_dto.go @@ -8,8 +8,10 @@ package dto type ClientCreateOrderRequest struct { Identifier string `json:"identifier" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"资产标识符(SN/IMEI/虚拟号/ICCID/MSISDN)"` PackageIDs []uint `json:"package_ids" validate:"required,min=1,dive,gt=0" required:"true" description:"套餐ID列表"` - // app_type 仅强充场景需要(服务端检测到强充时会直接发起微信支付);普通下单时可不传 - AppType string `json:"app_type" validate:"omitempty,oneof=official_account miniapp" description:"应用类型(强充必传)(official_account:公众号, miniapp:小程序)"` + // PaymentMethod 指定支付方式;强充场景必传。wechat 时 app_type 也必传,alipay 时不需要 app_type + PaymentMethod string `json:"payment_method" validate:"omitempty,oneof=wechat alipay" description:"支付方式(强充必传)(wechat:微信, alipay:支付宝)"` + // AppType 仅强充 + 微信支付场景必传;支付宝强充无需传 + AppType string `json:"app_type" validate:"omitempty,oneof=official_account miniapp" description:"应用类型(微信强充必传)(official_account:公众号, miniapp:小程序)"` } // ClientCreateOrderResponse D1 客户端创建订单响应 @@ -17,7 +19,8 @@ type ClientCreateOrderResponse struct { OrderType string `json:"order_type" description:"订单类型 (package:套餐订单, recharge:充值订单)"` Order *ClientOrderInfo `json:"order,omitempty" description:"套餐订单信息(普通下单时返回,无支付参数,需单独调支付接口)"` Recharge *ClientRechargeInfo `json:"recharge,omitempty" description:"充值订单信息"` - PayConfig *ClientPayConfig `json:"pay_config,omitempty" description:"微信支付配置(仅强充场景返回)"` + PayConfig *ClientPayConfig `json:"pay_config,omitempty" description:"微信支付配置(强充微信支付时返回)"` + PaymentLink *ClientPaymentLink `json:"payment_link,omitempty" description:"支付宝支付链接(强充支付宝支付时返回)"` LinkedPackageInfo *LinkedPackageInfo `json:"linked_package_info,omitempty" description:"关联套餐信息"` Idempotent bool `json:"idempotent,omitempty" description:"幂等标志(true 表示返回的是已存在的待支付充值订单)"` } @@ -122,17 +125,26 @@ type ClientOrderPackageItem struct { // D4 订单支付 // ======================================== +// ClientPaymentLink C 端支付宝支付链接 +type ClientPaymentLink struct { + PaymentNo string `json:"payment_no" description:"支付单号"` + QRLink string `json:"qr_link" description:"二维码内容链接,前端用该链接生成二维码"` + CopyLink string `json:"copy_link" description:"复制到普通浏览器打开的支付链接(与 qr_link 相同)"` + PayExpireAt string `json:"pay_expire_at" description:"支付链接过期时间(RFC3339 格式)"` +} + // ClientPayOrderRequest D4 订单支付请求 type ClientPayOrderRequest struct { - PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet wechat" required:"true" description:"支付方式 (wallet:钱包, wechat:微信)"` - // AppType 仅 payment_method=wechat 时必填 + PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet wechat alipay" required:"true" description:"支付方式 (wallet:钱包, wechat:微信, alipay:支付宝)"` + // AppType 仅 payment_method=wechat 时必填,alipay 无需传 AppType string `json:"app_type" validate:"omitempty,oneof=official_account miniapp" description:"应用类型(微信支付必传)(official_account:公众号, miniapp:小程序)"` } // ClientPayOrderResponse D4 订单支付响应 type ClientPayOrderResponse struct { - PaymentMethod string `json:"payment_method" description:"支付方式"` - PayConfig *ClientPayConfig `json:"pay_config,omitempty" description:"微信支付配置(payment_method=wechat 时返回)"` + PaymentMethod string `json:"payment_method" description:"支付方式"` + PayConfig *ClientPayConfig `json:"pay_config,omitempty" description:"微信支付配置(payment_method=wechat 时返回)"` + PaymentLink *ClientPaymentLink `json:"payment_link,omitempty" description:"支付宝支付链接(payment_method=alipay 时返回)"` } // ClientPayOrderParams D4 订单支付文档参数(路径参数 + 请求体,仅供文档生成器使用) diff --git a/internal/model/dto/client_wallet_dto.go b/internal/model/dto/client_wallet_dto.go index f666ed3..974a22c 100644 --- a/internal/model/dto/client_wallet_dto.go +++ b/internal/model/dto/client_wallet_dto.go @@ -77,15 +77,17 @@ type ClientRechargeCheckResponse struct { // ClientCreateRechargeRequest C4 创建充值订单请求 type ClientCreateRechargeRequest struct { Identifier string `json:"identifier" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"资产标识符(SN/IMEI/虚拟号/ICCID/MSISDN)"` - Amount int64 `json:"amount" validate:"required,min=100,max=10000000" required:"true" minimum:"100" maximum:"10000000" description:"充值金额(分)"` - PaymentMethod string `json:"payment_method" validate:"required,oneof=wechat" required:"true" description:"支付方式 (wechat:微信支付)"` - AppType string `json:"app_type" validate:"required,oneof=official_account miniapp" required:"true" description:"应用类型 (official_account:公众号, miniapp:小程序)"` + Amount int64 `json:"amount" validate:"required,min=1,max=10000000" required:"true" minimum:"1" maximum:"10000000" description:"充值金额(分)"` + PaymentMethod string `json:"payment_method" validate:"required,oneof=wechat alipay" required:"true" description:"支付方式 (wechat:微信支付, alipay:支付宝)"` + // AppType 仅 payment_method=wechat 时必填,alipay 无需传 + AppType string `json:"app_type" validate:"omitempty,oneof=official_account miniapp" description:"应用类型(微信支付必传)(official_account:公众号, miniapp:小程序)"` } // ClientRechargeResponse C4 创建充值订单响应 type ClientRechargeResponse struct { - Recharge ClientRechargeResult `json:"recharge" description:"充值信息"` - PayConfig ClientRechargePayConfig `json:"pay_config" description:"支付配置"` + Recharge ClientRechargeResult `json:"recharge" description:"充值信息"` + PayConfig *ClientRechargePayConfig `json:"pay_config,omitempty" description:"微信支付配置(payment_method=wechat 时返回)"` + PaymentLink *ClientPaymentLink `json:"payment_link,omitempty" description:"支付宝支付链接(payment_method=alipay 时返回)"` } // ClientRechargeResult C4 充值信息 diff --git a/internal/service/client_order/service.go b/internal/service/client_order/service.go index 748d697..4d35d3c 100644 --- a/internal/service/client_order/service.go +++ b/internal/service/client_order/service.go @@ -15,6 +15,7 @@ import ( asset "github.com/break/junhong_cmp_fiber/internal/service/asset" "github.com/break/junhong_cmp_fiber/internal/service/purchase_validation" "github.com/break/junhong_cmp_fiber/internal/store/postgres" + "github.com/break/junhong_cmp_fiber/pkg/alipay" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/bytedance/sonic" @@ -178,7 +179,7 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli if err == nil && strings.HasPrefix(existingValue, "CRCH") { existingOrder, queryErr := s.rechargeOrderStore.GetByRechargeOrderNo(skipCtx, existingValue) if queryErr == nil && existingOrder.Status == model.RechargeOrderStatusPending { - return &dto.ClientCreateOrderResponse{ + resp := &dto.ClientCreateOrderResponse{ OrderType: "recharge", Recharge: &dto.ClientRechargeInfo{ RechargeID: existingOrder.ID, @@ -189,7 +190,23 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli AutoPurchaseStatus: existingOrder.AutoPurchaseStatus, }, Idempotent: true, - }, nil + } + // 支付宝强充幂等:复用已有待支付 payment 或生成新链接 + if req.PaymentMethod == model.PaymentMethodAlipay { + paymentLink, linkErr := s.getOrBuildAlipayPaymentLink( + skipCtx, existingOrder.ID, model.PaymentOrderTypeRecharge, + existingOrder.Amount, "余额充值", + ) + if linkErr != nil { + s.logger.Warn("强充幂等复用支付宝链接失败", + zap.String("recharge_no", existingOrder.RechargeOrderNo), + zap.Error(linkErr), + ) + } else { + resp.PaymentLink = paymentLink + } + } + return resp, nil } if queryErr == nil { _ = s.redis.Del(skipCtx, redisKey).Err() @@ -209,7 +226,18 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli }() if forceRecharge.NeedForceRecharge { - // 强充场景需要微信支付,此时 app_type 必须传入 + if req.PaymentMethod == model.PaymentMethodAlipay { + // 支付宝强充:不需要 app_type / OpenID + activeConfig, err := s.wechatConfigService.GetActiveConfig(skipCtx) + if err != nil { + return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询支付配置失败") + } + if activeConfig == nil { + return nil, errors.New(errors.CodeInvalidParam, "未找到生效的支付配置") + } + return s.createAlipayForceRechargeOrder(skipCtx, customerID, assetInfo, validationResult, activeConfig, forceRecharge, redisKey, &created) + } + // 微信强充:app_type 必须传入 activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType) if err != nil { return nil, err @@ -813,6 +841,214 @@ func generateClientPaymentNo() string { return fmt.Sprintf("PAY%d%06d", time.Now().UnixNano()/1e6, rand.Intn(1000000)) } +// getOrBuildAlipayPaymentLink 查找未过期的支付宝待支付单并返回链接; +// 过期则标记 failed 并创建新单;新建失败则整体返回错误。 +func (s *Service) getOrBuildAlipayPaymentLink( + ctx context.Context, + orderID uint, + orderType string, + amount int64, + subject string, +) (*dto.ClientPaymentLink, error) { + activeConfig, err := s.wechatConfigService.GetActiveConfig(ctx) + if err != nil { + return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询支付配置失败") + } + if activeConfig == nil { + return nil, errors.New(errors.CodeInvalidParam, "未找到生效的支付配置") + } + + // 查找最新的 alipay 待支付单 + existing, _ := s.paymentStore.FindLatestPendingByOrderAndMethod( + ctx, orderID, orderType, model.PaymentByAlipay, + ) + + var payment *model.Payment + now := time.Now() + if existing != nil && existing.ExpireAt != nil && existing.ExpireAt.After(now) { + // 复用未过期的支付单 + payment = existing + } else { + // 过期或不存在,标记旧单 failed 并新建 + if existing != nil { + if updateErr := s.paymentStore.UpdateStatus(ctx, existing.ID, model.PaymentRecordStatusFailed); updateErr != nil { + s.logger.Warn("标记过期支付宝支付单 failed 失败", + zap.Uint("payment_id", existing.ID), + zap.Error(updateErr), + ) + } + } + expireAt := alipay.CalcExpireAt(activeConfig) + newPayment := &model.Payment{ + PaymentNo: generateClientPaymentNo(), + OrderID: orderID, + OrderType: orderType, + PaymentMethod: model.PaymentByAlipay, + Amount: amount, + Status: model.PaymentRecordStatusPending, + PaymentConfigID: &activeConfig.ID, + ExpireAt: &expireAt, + } + if err := s.paymentStore.Create(ctx, newPayment); err != nil { + return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建支付宝支付单失败") + } + payment = newPayment + s.logger.Info("创建支付宝支付单", + zap.String("payment_no", payment.PaymentNo), + zap.String("order_type", orderType), + zap.Uint("order_id", orderID), + zap.Int64("amount", amount), + zap.Time("expire_at", expireAt), + zap.Uint("config_id", activeConfig.ID), + ) + } + + wapURL, err := alipay.BuildWapPayURL(ctx, activeConfig, payment, subject, activeConfig.AliReturnURL) + if err != nil { + // 新建的 payment 生成链接失败,标记 failed + if existing == nil || payment.ID != existing.ID { + _ = s.paymentStore.UpdateStatus(ctx, payment.ID, model.PaymentRecordStatusFailed) + } + return nil, err + } + + expireStr := "" + if payment.ExpireAt != nil { + expireStr = payment.ExpireAt.Format(time.RFC3339) + } + return &dto.ClientPaymentLink{ + PaymentNo: payment.PaymentNo, + QRLink: wapURL, + CopyLink: wapURL, + PayExpireAt: expireStr, + }, nil +} + +// createAlipayForceRechargeOrder 强充场景下创建支付宝充值单并返回支付链接。 +func (s *Service) createAlipayForceRechargeOrder( + ctx context.Context, + customerID uint, + assetInfo *dto.AssetResolveResponse, + validationResult *purchase_validation.PurchaseValidationResult, + activeConfig *model.WechatConfig, + forceRecharge *ForceRechargeRequirement, + redisKey string, + created *bool, +) (*dto.ClientCreateOrderResponse, error) { + resourceType, resourceID, err := resolveWalletResource(validationResult) + if err != nil { + return nil, err + } + + wallet, err := s.getOrCreateWallet(ctx, resourceType, resourceID, resolveSellerShopID(validationResult)) + if err != nil { + return nil, err + } + + linkedPackageIDs, err := sonic.Marshal(extractPackageIDs(validationResult.Packages)) + if err != nil { + return nil, errors.Wrap(errors.CodeInternalError, err, "序列化关联套餐失败") + } + + rechargeOrderNo := generateClientRechargeNo() + var iotCardID *uint + var deviceID *uint + if validationResult.Card != nil { + iotCardID = &validationResult.Card.ID + } + if validationResult.Device != nil { + deviceID = &validationResult.Device.ID + } + + rechargeOrder := &model.RechargeOrder{ + RechargeOrderNo: rechargeOrderNo, + UserID: customerID, + AssetWalletID: wallet.ID, + ResourceType: resourceType, + ResourceID: resourceID, + IotCardID: iotCardID, + DeviceID: deviceID, + Amount: forceRecharge.ForceRechargeAmount, + Status: model.RechargeOrderStatusPending, + ShopIDTag: wallet.ShopIDTag, + EnterpriseIDTag: wallet.EnterpriseIDTag, + OperatorType: "personal_customer", + Generation: resolveGeneration(validationResult), + LinkedPackageIDs: datatypes.JSON(linkedPackageIDs), + LinkedOrderType: resolveOrderType(validationResult), + LinkedCarrierType: assetInfo.AssetType, + LinkedCarrierID: &resourceID, + AutoPurchaseStatus: model.AutoPurchaseStatusPending, + PaymentConfigID: &activeConfig.ID, + } + + expireAt := alipay.CalcExpireAt(activeConfig) + paymentNo := generateClientPaymentNo() + payment := &model.Payment{ + PaymentNo: paymentNo, + OrderType: model.PaymentOrderTypeRecharge, + PaymentMethod: model.PaymentByAlipay, + Amount: forceRecharge.ForceRechargeAmount, + Status: model.PaymentRecordStatusPending, + PaymentConfigID: &activeConfig.ID, + ExpireAt: &expireAt, + } + + // 事务创建充值单 + 支付单(WAP URL 本地签名,不需要先调第三方) + if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := s.rechargeOrderStore.CreateWithTx(ctx, tx, rechargeOrder); err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败") + } + payment.OrderID = rechargeOrder.ID + return s.paymentStore.CreateWithTx(ctx, tx, payment) + }); err != nil { + return nil, err + } + + wapURL, err := alipay.BuildWapPayURL(ctx, activeConfig, payment, "余额充值", activeConfig.AliReturnURL) + if err != nil { + if updateErr := s.paymentStore.UpdateStatus(ctx, payment.ID, model.PaymentRecordStatusFailed); updateErr != nil { + s.logger.Warn("标记支付宝支付单 failed 失败", + zap.String("payment_no", paymentNo), + zap.Error(updateErr), + ) + } + return nil, err + } + + s.logger.Info("支付宝强充支付单已创建", + zap.String("payment_no", paymentNo), + zap.String("recharge_no", rechargeOrderNo), + zap.Int64("amount", forceRecharge.ForceRechargeAmount), + zap.Time("expire_at", expireAt), + zap.Uint("config_id", activeConfig.ID), + ) + + s.markClientPurchaseCreated(ctx, redisKey, rechargeOrderNo) + *created = true + + clientStatus := rechargeStatusToClientStatus(int(rechargeOrder.Status)) + expireStr := expireAt.Format(time.RFC3339) + return &dto.ClientCreateOrderResponse{ + OrderType: "recharge", + Recharge: &dto.ClientRechargeInfo{ + RechargeID: rechargeOrder.ID, + RechargeNo: rechargeOrderNo, + Amount: rechargeOrder.Amount, + Status: clientStatus, + StatusName: clientRechargeStatusName(clientStatus), + AutoPurchaseStatus: rechargeOrder.AutoPurchaseStatus, + }, + PaymentLink: &dto.ClientPaymentLink{ + PaymentNo: paymentNo, + QRLink: wapURL, + CopyLink: wapURL, + PayExpireAt: expireStr, + }, + LinkedPackageInfo: buildLinkedPackageInfo(validationResult, forceRecharge), + }, nil +} + // 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{}) @@ -1058,6 +1294,36 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r PayConfig: buildClientPayConfigFromResult(paymentResult), }, nil + case model.PaymentMethodAlipay: + // 支付宝支付:不需要 app_type / OpenID + activeConfig, err := s.wechatConfigService.GetActiveConfig(skipCtx) + if err != nil { + return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询支付配置失败") + } + if activeConfig == nil { + return nil, errors.New(errors.CodeInvalidParam, "未找到生效的支付配置") + } + + paymentLink, err := s.getOrBuildAlipayPaymentLink( + skipCtx, orderID, model.PaymentOrderTypePackage, order.TotalAmount, "套餐购买", + ) + if err != nil { + return nil, err + } + // 更新订单支付配置 ID,兼容回调配置回溯 + if dbErr := s.db.WithContext(skipCtx).Model(&model.Order{}). + Where("id = ?", orderID). + Update("payment_config_id", activeConfig.ID).Error; dbErr != nil { + s.logger.Warn("更新订单支付配置 ID 失败", + zap.Uint("order_id", orderID), + zap.Error(dbErr), + ) + } + return &dto.ClientPayOrderResponse{ + PaymentMethod: model.PaymentMethodAlipay, + PaymentLink: paymentLink, + }, nil + default: return nil, errors.New(errors.CodeInvalidParam, "不支持的支付方式") }