新增收款商户、商户池轮询、微信授权配置独立管理;三类新支付 (C端套餐购买、C端资产钱包充值、代理在线预存款充值)无条件 经商户池选择并冻结路由,无旧综合配置回退。merchant_id 为空 历史支付继续按 payment_config_id 双读。凭证版本化加载与 ID+版本缓存保证轮换一致性。删除商户池新支付创建开关及全部 引用。
This commit is contained in:
@@ -137,10 +137,12 @@ func (s *Service) VerifyAsset(ctx context.Context, req *dto.VerifyAssetRequest,
|
||||
ExpiresIn: assetTokenExpireSeconds,
|
||||
}
|
||||
|
||||
if wechatConfig, err := s.wechatConfigService.GetActiveConfig(ctx); err == nil && wechatConfig != nil {
|
||||
resp.OaAppID = wechatConfig.OaAppID
|
||||
resp.MiniappAppID = wechatConfig.MiniappAppID
|
||||
wechatAuthorization, err := s.wechatConfigService.GetAuthorizationConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.OaAppID = wechatAuthorization.OaAppID
|
||||
resp.MiniappAppID = wechatAuthorization.MiniappAppID
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -156,13 +158,10 @@ func (s *Service) WechatLogin(ctx context.Context, req *dto.WechatLoginRequest,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wechatConfig, err := s.wechatConfigService.GetActiveConfig(ctx)
|
||||
wechatConfig, err := s.wechatConfigService.GetAuthorizationConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wechatConfig == nil {
|
||||
return nil, errors.New(errors.CodeWechatConfigUnavailable)
|
||||
}
|
||||
|
||||
oaApp, err := wechat.NewOfficialAccountAppFromConfig(wechatConfig, s.wechatCache, s.logger)
|
||||
if err != nil {
|
||||
@@ -219,13 +218,10 @@ func (s *Service) MiniappLogin(ctx context.Context, req *dto.MiniappLoginRequest
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wechatConfig, err := s.wechatConfigService.GetActiveConfig(ctx)
|
||||
wechatConfig, err := s.wechatConfigService.GetAuthorizationConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wechatConfig == nil {
|
||||
return nil, errors.New(errors.CodeWechatConfigUnavailable)
|
||||
}
|
||||
|
||||
miniService, err := wechat.NewMiniAppServiceFromConfig(wechatConfig, s.logger)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
@@ -33,9 +34,10 @@ const (
|
||||
clientPurchaseLockTTL = 10 * time.Second
|
||||
)
|
||||
|
||||
// WechatConfigServiceInterface 微信配置服务接口。
|
||||
// WechatConfigServiceInterface 只读取历史支付单的综合支付配置。
|
||||
type WechatConfigServiceInterface interface {
|
||||
GetActiveConfig(ctx context.Context) (*model.WechatConfig, error)
|
||||
GetAuthorizationConfig(ctx context.Context) (*model.WechatConfig, error)
|
||||
GetByIDUnscoped(ctx context.Context, id uint) (*model.WechatConfig, error)
|
||||
}
|
||||
|
||||
// OrderWalletPayServiceInterface 订单钱包支付服务接口。
|
||||
@@ -70,7 +72,6 @@ type Service struct {
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore
|
||||
personalCustomerStore *postgres.PersonalCustomerStore
|
||||
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
orderPaymentService OrderWalletPayServiceInterface
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
@@ -82,6 +83,8 @@ type Service struct {
|
||||
paymentMethodPolicy PaymentMethodPolicy
|
||||
auditWriter *audit.Writer
|
||||
paymentIntegration *integrationlog.Repository
|
||||
merchantRuntime *merchantpayment.RuntimeLoader
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
}
|
||||
|
||||
// SetPaymentMethodPolicy 注入 C 端支付方式策略。
|
||||
@@ -95,6 +98,11 @@ func (s *Service) SetPaymentAudit(writer *audit.Writer, integration *integration
|
||||
s.paymentIntegration = integration
|
||||
}
|
||||
|
||||
// SetLegacyPaymentConfigService 注入仅供 merchant_id 为空历史支付单使用的旧配置读取。
|
||||
func (s *Service) SetLegacyPaymentConfigService(wechatConfigService WechatConfigServiceInterface) {
|
||||
s.wechatConfigService = wechatConfigService
|
||||
}
|
||||
|
||||
// New 创建客户端订单服务。
|
||||
func New(
|
||||
assetService *asset.Service,
|
||||
@@ -107,7 +115,6 @@ func New(
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore,
|
||||
personalCustomerStore *postgres.PersonalCustomerStore,
|
||||
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore,
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
orderPaymentService OrderWalletPayServiceInterface,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
@@ -128,7 +135,6 @@ func New(
|
||||
openIDStore: openIDStore,
|
||||
personalCustomerStore: personalCustomerStore,
|
||||
personalCustomerPhoneStore: personalCustomerPhoneStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
orderPaymentService: orderPaymentService,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
@@ -137,6 +143,7 @@ func New(
|
||||
db: db,
|
||||
redis: redisClient,
|
||||
logger: logger,
|
||||
merchantRuntime: merchantpayment.NewRuntimeLoader(db, redisClient),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,18 +308,10 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
// }
|
||||
|
||||
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)
|
||||
return s.createAlipayForceRechargeOrder(skipCtx, customerID, assetInfo, validationResult, forceRecharge, redisKey, &created)
|
||||
}
|
||||
// 微信强充:app_type 必须传入
|
||||
activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType)
|
||||
authorization, appID, err := s.loadWechatAuthorization(skipCtx, req.AppType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -320,11 +319,7 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paymentProvider, err := s.newPaymentProvider(activeConfig, appID, req.AppType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.createForceRechargeOrder(skipCtx, customerID, openID, assetInfo, validationResult, activeConfig, forceRecharge, redisKey, paymentProvider, &created)
|
||||
return s.createForceRechargeOrder(skipCtx, customerID, openID, assetInfo, validationResult, authorization, appID, req.AppType, forceRecharge, redisKey, &created)
|
||||
}
|
||||
|
||||
return s.createPackageOrder(skipCtx, customerID, validationResult, req.PaymentMethod, redisKey, &created)
|
||||
@@ -362,31 +357,49 @@ func (s *Service) validatePurchase(ctx context.Context, assetInfo *dto.AssetReso
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) resolveWechatConfig(ctx context.Context, appType string) (*model.WechatConfig, string, error) {
|
||||
activeConfig, err := s.wechatConfigService.GetActiveConfig(ctx)
|
||||
func (s *Service) loadWechatAuthorization(ctx context.Context, appType string) (*model.WechatAuthorization, string, error) {
|
||||
if s.merchantRuntime == nil {
|
||||
return nil, "", errors.New(errors.CodeServiceUnavailable, "商户池路由能力未配置")
|
||||
}
|
||||
authorization, err := s.merchantRuntime.LoadAuthorization(ctx)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(errors.CodeDatabaseError, err, "查询微信配置失败")
|
||||
return nil, "", err
|
||||
}
|
||||
if activeConfig == nil {
|
||||
return nil, "", errors.New(errors.CodeWechatPayFailed, "未找到生效的微信支付配置")
|
||||
}
|
||||
|
||||
switch appType {
|
||||
case "official_account":
|
||||
if activeConfig.OaAppID == "" {
|
||||
return nil, "", errors.New(errors.CodeWechatPayFailed, "公众号支付配置不完整")
|
||||
if strings.TrimSpace(authorization.OaAppID) == "" {
|
||||
return nil, "", errors.New(errors.CodeWechatConfigUnavailable, "微信授权未配置")
|
||||
}
|
||||
return activeConfig, activeConfig.OaAppID, nil
|
||||
return authorization, authorization.OaAppID, nil
|
||||
case "miniapp":
|
||||
if activeConfig.MiniappAppID == "" {
|
||||
return nil, "", errors.New(errors.CodeWechatPayFailed, "小程序支付配置不完整")
|
||||
if strings.TrimSpace(authorization.MiniappAppID) == "" {
|
||||
return nil, "", errors.New(errors.CodeWechatConfigUnavailable, "微信授权未配置")
|
||||
}
|
||||
return activeConfig, activeConfig.MiniappAppID, nil
|
||||
return authorization, authorization.MiniappAppID, nil
|
||||
default:
|
||||
return nil, "", errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) selectMerchantConfig(ctx context.Context, tx *gorm.DB, paymentMethod string, authorization *model.WechatAuthorization) (*merchantpayment.RouteSelection, *model.WechatConfig, error) {
|
||||
if s.merchantRuntime == nil {
|
||||
return nil, nil, errors.New(errors.CodeServiceUnavailable, "商户池路由能力未配置")
|
||||
}
|
||||
route, err := s.merchantRuntime.SelectForNewPaymentWithTx(ctx, tx, paymentMethod, time.Now())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
config, err := merchantpayment.MerchantConfig(route.Merchant, authorization)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return route, config, nil
|
||||
}
|
||||
|
||||
func freezePaymentRoute(payment *model.Payment, route *merchantpayment.RouteSelection) {
|
||||
merchantpayment.FreezeRoute(payment, route)
|
||||
}
|
||||
|
||||
func (s *Service) resolveCustomerOpenID(ctx context.Context, customerID uint, appID string) (string, error) {
|
||||
records, err := s.openIDStore.ListByCustomerID(ctx, customerID)
|
||||
if err != nil {
|
||||
@@ -405,6 +418,41 @@ func (s *Service) resolveCustomerOpenID(ctx context.Context, customerID uint, ap
|
||||
return "", errors.New(errors.CodeNotFound, "未找到当前应用的微信授权信息")
|
||||
}
|
||||
|
||||
// merchantConfigForPayment 按冻结商户或历史 payment_config_id 加载支付凭证。
|
||||
// merchant_id 为空仅是历史支付保留期兼容:独立 Change 清理后删除此读取,
|
||||
// 绝不按当前商户池或综合配置推断、回填该支付的商户。
|
||||
func (s *Service) merchantConfigForPayment(ctx context.Context, payment *model.Payment) (*model.WechatConfig, error) {
|
||||
if payment != nil && payment.MerchantID != nil {
|
||||
if s.merchantRuntime == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "商户池路由能力未配置")
|
||||
}
|
||||
merchant, err := s.merchantRuntime.LoadMerchant(ctx, *payment.MerchantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 仅微信直连(v3/v2)商户需要全局授权配置中的 AppID;富友商户不依赖该配置。
|
||||
var authorization *model.WechatAuthorization
|
||||
if merchant.ProviderType == model.ProviderTypeWechat || merchant.ProviderType == model.ProviderTypeWechatV2 {
|
||||
authorization, err = s.merchantRuntime.LoadAuthorization(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return merchantpayment.MerchantConfig(merchant, authorization)
|
||||
}
|
||||
if payment == nil || payment.PaymentConfigID == nil {
|
||||
return nil, errors.New(errors.CodeNoPaymentConfig, "历史支付宝支付单缺少支付配置")
|
||||
}
|
||||
if s.wechatConfigService == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "旧支付配置加载能力未配置")
|
||||
}
|
||||
config, err := s.wechatConfigService.GetByIDUnscoped(ctx, *payment.PaymentConfigID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeNoPaymentConfig, err, "历史支付宝支付配置不可用")
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (s *Service) createPackageOrder(
|
||||
ctx context.Context,
|
||||
customerID uint,
|
||||
@@ -465,10 +513,10 @@ func (s *Service) createForceRechargeOrder(
|
||||
openID string,
|
||||
assetInfo *dto.AssetResolveResponse,
|
||||
validationResult *purchase_validation.PurchaseValidationResult,
|
||||
activeConfig *model.WechatConfig,
|
||||
authorization *model.WechatAuthorization,
|
||||
appID, appType string,
|
||||
forceRecharge *ForceRechargeRequirement,
|
||||
redisKey string,
|
||||
paymentProvider PaymentProvider,
|
||||
created *bool,
|
||||
) (*dto.ClientCreateOrderResponse, error) {
|
||||
resourceType, resourceID, err := resolveWalletResource(validationResult)
|
||||
@@ -519,16 +567,27 @@ func (s *Service) createForceRechargeOrder(
|
||||
|
||||
paymentNo := generateClientPaymentNo()
|
||||
payment := &model.Payment{
|
||||
PaymentNo: paymentNo,
|
||||
OrderID: rechargeOrder.ID,
|
||||
OrderType: model.PaymentOrderTypeRecharge,
|
||||
PaymentMethod: model.PaymentByWechat,
|
||||
Amount: forceRecharge.ForceRechargeAmount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
PaymentConfigID: &activeConfig.ID,
|
||||
PaymentNo: paymentNo,
|
||||
OrderID: rechargeOrder.ID,
|
||||
OrderType: model.PaymentOrderTypeRecharge,
|
||||
PaymentMethod: model.PaymentByWechat,
|
||||
Amount: forceRecharge.ForceRechargeAmount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
}
|
||||
var activeConfig *model.WechatConfig
|
||||
var paymentProvider PaymentProvider
|
||||
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
route, config, err := s.selectMerchantConfig(ctx, tx, model.PaymentByWechat, authorization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paymentProvider, err = s.newPaymentProvider(config, appID, appType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activeConfig = config
|
||||
freezePaymentRoute(payment, route)
|
||||
if err := s.rechargeOrderStore.CreateWithTx(ctx, tx, rechargeOrder); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
@@ -967,26 +1026,22 @@ func (s *Service) getOrBuildAlipayPaymentLink(
|
||||
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
|
||||
var activeConfig *model.WechatConfig
|
||||
created := false
|
||||
now := time.Now()
|
||||
if existing != nil && existing.ExpireAt != nil && existing.ExpireAt.After(now) {
|
||||
// 复用未过期的支付单
|
||||
payment = existing
|
||||
var err error
|
||||
activeConfig, err = s.merchantConfigForPayment(ctx, payment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// 过期或不存在,标记旧单 failed 并新建
|
||||
if existing != nil {
|
||||
if updateErr := s.markPaymentFailed(ctx, existing, "支付宝支付记录过期关闭"); updateErr != nil {
|
||||
s.logger.Warn("标记过期支付宝支付单 failed 失败",
|
||||
@@ -995,44 +1050,51 @@ func (s *Service) getOrBuildAlipayPaymentLink(
|
||||
)
|
||||
}
|
||||
}
|
||||
expireMinutes := activeConfig.AliPayExpireMinutes
|
||||
if expireMinutes <= 0 {
|
||||
expireMinutes = 30
|
||||
}
|
||||
expireAt := time.Now().Add(time.Duration(expireMinutes) * time.Minute)
|
||||
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.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
route, config, err := s.selectMerchantConfig(ctx, tx, model.PaymentByAlipay, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expireMinutes := config.AliPayExpireMinutes
|
||||
if expireMinutes <= 0 {
|
||||
expireMinutes = 30
|
||||
}
|
||||
expireAt := time.Now().Add(time.Duration(expireMinutes) * time.Minute)
|
||||
newPayment := &model.Payment{
|
||||
PaymentNo: generateClientPaymentNo(),
|
||||
OrderID: orderID,
|
||||
OrderType: orderType,
|
||||
PaymentMethod: model.PaymentByAlipay,
|
||||
Amount: amount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
ExpireAt: &expireAt,
|
||||
}
|
||||
freezePaymentRoute(newPayment, route)
|
||||
if err := s.paymentStore.CreateWithTx(ctx, tx, newPayment); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付宝支付单失败")
|
||||
}
|
||||
return s.appendPaymentCreatedAudit(ctx, tx, newPayment, nil, nil)
|
||||
if err := s.appendPaymentCreatedAudit(ctx, tx, newPayment, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
payment = newPayment
|
||||
activeConfig = config
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payment = newPayment
|
||||
created = true
|
||||
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),
|
||||
zap.Time("expire_at", *payment.ExpireAt),
|
||||
)
|
||||
}
|
||||
|
||||
wapURL, err := alipay.BuildWapPayURL(ctx, activeConfig, payment, subject)
|
||||
if err != nil {
|
||||
// 新建的 payment 生成链接失败,标记 failed
|
||||
if existing == nil || payment.ID != existing.ID {
|
||||
if created {
|
||||
_ = s.markPaymentFailed(ctx, payment, "支付宝支付链接生成失败,关闭支付记录")
|
||||
}
|
||||
return nil, err
|
||||
@@ -1056,7 +1118,6 @@ func (s *Service) createAlipayForceRechargeOrder(
|
||||
customerID uint,
|
||||
assetInfo *dto.AssetResolveResponse,
|
||||
validationResult *purchase_validation.PurchaseValidationResult,
|
||||
activeConfig *model.WechatConfig,
|
||||
forceRecharge *ForceRechargeRequirement,
|
||||
redisKey string,
|
||||
created *bool,
|
||||
@@ -1105,27 +1166,31 @@ func (s *Service) createAlipayForceRechargeOrder(
|
||||
LinkedCarrierType: assetInfo.AssetType,
|
||||
LinkedCarrierID: &resourceID,
|
||||
AutoPurchaseStatus: model.AutoPurchaseStatusPending,
|
||||
PaymentConfigID: &activeConfig.ID,
|
||||
}
|
||||
|
||||
expireMinutesForce := activeConfig.AliPayExpireMinutes
|
||||
if expireMinutesForce <= 0 {
|
||||
expireMinutesForce = 30
|
||||
}
|
||||
expireAt := time.Now().Add(time.Duration(expireMinutesForce) * time.Minute)
|
||||
paymentNo := generateClientPaymentNo()
|
||||
payment := &model.Payment{
|
||||
PaymentNo: paymentNo,
|
||||
OrderType: model.PaymentOrderTypeRecharge,
|
||||
PaymentMethod: model.PaymentByAlipay,
|
||||
Amount: forceRecharge.ForceRechargeAmount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
PaymentConfigID: &activeConfig.ID,
|
||||
ExpireAt: &expireAt,
|
||||
PaymentNo: paymentNo,
|
||||
OrderType: model.PaymentOrderTypeRecharge,
|
||||
PaymentMethod: model.PaymentByAlipay,
|
||||
Amount: forceRecharge.ForceRechargeAmount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
}
|
||||
|
||||
// 事务创建充值单 + 支付单(WAP URL 本地签名,不需要先调第三方)
|
||||
var activeConfig *model.WechatConfig
|
||||
var expireAt time.Time
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
route, config, err := s.selectMerchantConfig(ctx, tx, model.PaymentByAlipay, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expireMinutes := config.AliPayExpireMinutes
|
||||
if expireMinutes <= 0 {
|
||||
expireMinutes = 30
|
||||
}
|
||||
expireAt = time.Now().Add(time.Duration(expireMinutes) * time.Minute)
|
||||
activeConfig = config
|
||||
payment.ExpireAt = &expireAt
|
||||
freezePaymentRoute(payment, route)
|
||||
if err := s.rechargeOrderStore.CreateWithTx(ctx, tx, rechargeOrder); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
@@ -1154,7 +1219,6 @@ func (s *Service) createAlipayForceRechargeOrder(
|
||||
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)
|
||||
@@ -1432,7 +1496,7 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
|
||||
return &dto.ClientPayOrderResponse{PaymentMethod: paymentMethod}, nil
|
||||
|
||||
case model.PaymentMethodWechat:
|
||||
activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType)
|
||||
authorization, appID, err := s.loadWechatAuthorization(skipCtx, req.AppType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1440,29 +1504,29 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paymentProvider, err := s.newPaymentProvider(activeConfig, appID, req.AppType)
|
||||
if err != nil {
|
||||
return nil, 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,
|
||||
PaymentNo: paymentNo,
|
||||
OrderID: order.ID,
|
||||
OrderType: model.PaymentOrderTypePackage,
|
||||
PaymentMethod: model.PaymentByWechat,
|
||||
Amount: order.TotalAmount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
}
|
||||
|
||||
var activeConfig *model.WechatConfig
|
||||
var paymentProvider PaymentProvider
|
||||
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, "更新订单支付配置失败")
|
||||
route, config, err := s.selectMerchantConfig(skipCtx, tx, model.PaymentByWechat, authorization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paymentProvider, err = s.newPaymentProvider(config, appID, req.AppType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activeConfig = config
|
||||
freezePaymentRoute(payment, route)
|
||||
if err := s.paymentStore.CreateWithTx(skipCtx, tx, payment); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
|
||||
}
|
||||
@@ -1498,30 +1562,12 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
|
||||
}, 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: paymentMethod,
|
||||
PaymentLink: paymentLink,
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
||||
merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
|
||||
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
|
||||
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
@@ -2018,8 +2019,10 @@ func (s *Service) HandlePaymentRecordCallback(ctx context.Context, paymentNo str
|
||||
if thirdPartyTradeNo != "" {
|
||||
paymentUpdates["third_party_trade_no"] = thirdPartyTradeNo
|
||||
}
|
||||
// 迟到首次成功必须允许 pending→paid 与 failed→paid;支付创建时冻结的
|
||||
// merchant_id/routing_epoch 仍决定统计归属,失败状态不表示已计入或已冲销。
|
||||
paymentResult := tx.Model(&model.Payment{}).
|
||||
Where("id = ? AND status = ?", payment.ID, model.PaymentRecordStatusPending).
|
||||
Where("id = ? AND status IN ?", payment.ID, []int{model.PaymentRecordStatusPending, model.PaymentRecordStatusFailed}).
|
||||
Updates(paymentUpdates)
|
||||
if paymentResult.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, paymentResult.Error, "更新支付记录失败")
|
||||
@@ -2035,6 +2038,10 @@ func (s *Service) HandlePaymentRecordCallback(ctx context.Context, paymentNo str
|
||||
return errors.New(errors.CodeInvalidStatus, "支付记录状态不允许处理")
|
||||
}
|
||||
|
||||
if err := merchantpayment.RecordFirstSuccess(ctx, tx, payment, now); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := tx.Model(&model.Order{}).
|
||||
Where("id = ? AND payment_status = ?", order.ID, model.PaymentStatusPending).
|
||||
Updates(map[string]any{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
@@ -86,7 +87,8 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, paymentNo string, p
|
||||
return nil
|
||||
}
|
||||
|
||||
if payment.Status != model.PaymentRecordStatusPending {
|
||||
// 迟到首次成功必须允许 pending 与 failed 状态进入确认;冻结路由决定统计归属。
|
||||
if payment.Status != model.PaymentRecordStatusPending && payment.Status != model.PaymentRecordStatusFailed {
|
||||
return errors.New(errors.CodeInvalidStatus, "支付状态不允许处理")
|
||||
}
|
||||
|
||||
@@ -102,7 +104,11 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, paymentNo string, p
|
||||
|
||||
now := time.Now()
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
oldPaymentStatus := model.PaymentRecordStatusPending
|
||||
// 乐观锁允许 pending 或 failed 首次进入 paid;重复成功由 RowsAffected/唯一事实保护。
|
||||
oldPaymentStatus := payment.Status
|
||||
if oldPaymentStatus != model.PaymentRecordStatusPending && oldPaymentStatus != model.PaymentRecordStatusFailed {
|
||||
return errors.New(errors.CodeInvalidStatus, "支付状态不允许处理")
|
||||
}
|
||||
if err := s.paymentStore.UpdateStatusWithOptimisticLockDB(ctx, tx, payment.ID, &oldPaymentStatus, model.PaymentRecordStatusPaid, &now); err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil
|
||||
@@ -117,6 +123,10 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, paymentNo string, p
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新支付信息失败")
|
||||
}
|
||||
|
||||
if err := merchantpayment.RecordFirstSuccess(ctx, tx, payment, now); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldRechargeStatus := model.RechargeOrderStatusPending
|
||||
if err := s.rechargeOrderStore.UpdateStatusWithOptimisticLockDB(ctx, tx, rechargeOrder.ID, &oldRechargeStatus, model.RechargeOrderStatusPaid, &now); err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
|
||||
@@ -62,6 +62,9 @@ func (s *Service) applyApprovedDecision(ctx context.Context, event approvalapp.T
|
||||
if err := validateApprovedRefundAmount(approvedAmount, refund.RequestedRefundAmount, &order); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.preparePaymentRefundCredentials(ctx, tx, order.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if refund.Status == model.RefundStatusPending {
|
||||
changed = true
|
||||
result := tx.WithContext(ctx).Model(&model.RefundRequest{}).
|
||||
|
||||
99
internal/service/refund/payment_merchant.go
Normal file
99
internal/service/refund/payment_merchant.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package refund
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// SetPaymentMerchantRuntime 注入冻结商户的当前凭证版本加载器。
|
||||
func (s *Service) SetPaymentMerchantRuntime(runtime *merchantpayment.RuntimeLoader) {
|
||||
s.paymentMerchantRuntime = runtime
|
||||
}
|
||||
|
||||
// preparePaymentRefundCredentials 只为既有套餐订单退款流程装载和校验支付凭证;本 Change
|
||||
// 不发起任何渠道退款请求。钱包支付和线下支付没有收款商户凭证,直接放行。
|
||||
// merchant_id 为空仅是留存期内的历史线上支付,独立 Change 清理后才可删除按
|
||||
// payment_config_id 读取的兼容路径,绝不能按当前商户池推断商户。
|
||||
func (s *Service) preparePaymentRefundCredentials(ctx context.Context, tx *gorm.DB, orderID uint) error {
|
||||
var payment model.Payment
|
||||
err := tx.WithContext(ctx).
|
||||
Where("order_id = ? AND order_type = ? AND status = ?", orderID, model.PaymentOrderTypePackage, model.PaymentRecordStatusPaid).
|
||||
Order("id DESC").
|
||||
First(&payment).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询退款关联支付单失败")
|
||||
}
|
||||
// 钱包支付和线下支付没有收款商户凭证,不被本检查阻断。
|
||||
if payment.PaymentMethod == model.PaymentByWallet || payment.PaymentMethod == model.PaymentByOffline {
|
||||
return nil
|
||||
}
|
||||
if payment.MerchantID != nil {
|
||||
if s.paymentMerchantRuntime == nil {
|
||||
return errors.New(errors.CodeServiceUnavailable, "支付商户加载能力未配置")
|
||||
}
|
||||
merchant, loadErr := s.paymentMerchantRuntime.LoadMerchant(ctx, *payment.MerchantID)
|
||||
if loadErr != nil {
|
||||
return loadErr
|
||||
}
|
||||
return validateFrozenMerchantRefundCredentials(merchant)
|
||||
}
|
||||
if payment.PaymentConfigID == nil {
|
||||
return errors.New(errors.CodeNoPaymentConfig, "历史支付单缺少支付配置")
|
||||
}
|
||||
var legacy model.WechatConfig
|
||||
if err := tx.WithContext(ctx).Unscoped().First(&legacy, *payment.PaymentConfigID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNoPaymentConfig, "历史支付配置不可用")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "读取历史支付配置失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateFrozenMerchantRefundCredentials 只判断既有渠道流程所需凭证是否完整。
|
||||
// 当前系统没有任何渠道原路退款 Adapter,因此返回前不调用微信、支付宝或富友。
|
||||
func validateFrozenMerchantRefundCredentials(merchant *model.PaymentMerchant) error {
|
||||
config, err := merchantpayment.MerchantConfig(merchant, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch config.ProviderType {
|
||||
case model.ProviderTypeWechat:
|
||||
if blank(config.WxMchID, config.WxAPIV3Key, config.WxCertContent, config.WxKeyContent, config.WxSerialNo, config.WxNotifyURL) {
|
||||
return errors.New(errors.CodeNoPaymentConfig, "冻结微信商户退款凭证不完整")
|
||||
}
|
||||
case model.ProviderTypeWechatV2:
|
||||
if blank(config.WxMchID, config.WxAPIV2Key, config.WxNotifyURL) {
|
||||
return errors.New(errors.CodeNoPaymentConfig, "冻结微信商户退款凭证不完整")
|
||||
}
|
||||
case model.ProviderTypeFuiou:
|
||||
if blank(config.FyInsCd, config.FyMchntCd, config.FyTermID, config.FyPrivateKey, config.FyPublicKey, config.FyAPIURL, config.FyNotifyURL) {
|
||||
return errors.New(errors.CodeNoPaymentConfig, "冻结富友商户退款凭证不完整")
|
||||
}
|
||||
case "alipay":
|
||||
if blank(config.AliAppID, config.AliPrivateKey, config.AliPublicKey, config.AliNotifyURL, config.AliReturnURL) {
|
||||
return errors.New(errors.CodeNoPaymentConfig, "冻结支付宝商户退款凭证不完整")
|
||||
}
|
||||
default:
|
||||
return errors.New(errors.CodeNoPaymentConfig, "冻结商户不支持现有退款流程")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func blank(values ...string) bool {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
|
||||
notificationapp "github.com/break/junhong_cmp_fiber/internal/application/notification"
|
||||
refundapprovalapp "github.com/break/junhong_cmp_fiber/internal/application/refundapproval"
|
||||
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
|
||||
@@ -56,6 +57,7 @@ type Service struct {
|
||||
notificationOutbox *outbox.Repository
|
||||
auditWriter *audit.Writer
|
||||
logger *zap.Logger
|
||||
paymentMerchantRuntime *merchantpayment.RuntimeLoader
|
||||
}
|
||||
|
||||
// New 创建退款业务服务实例
|
||||
@@ -355,6 +357,9 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRe
|
||||
if orderResult.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "订单状态已变更,无法执行退款审批")
|
||||
}
|
||||
if err := s.preparePaymentRefundCredentials(ctx, tx, order.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.refundWalletPayment(ctx, tx, refund, order, approvedAmount, userID); err != nil {
|
||||
return err
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
|
||||
systemconfigapp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
)
|
||||
|
||||
// Redis 缓存键
|
||||
@@ -544,6 +546,28 @@ func (s *Service) GetActiveConfig(ctx context.Context) (*model.WechatConfig, err
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// GetAuthorizationConfig 读取唯一启用且按凭证版本缓存的 C 端微信授权配置。
|
||||
// 仅用于兼容现有微信 SDK 的配置结构,绝不回退到 tb_wechat_config。
|
||||
func (s *Service) GetAuthorizationConfig(ctx context.Context) (*model.WechatConfig, error) {
|
||||
if s == nil || s.store == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "微信授权加载能力未配置")
|
||||
}
|
||||
authorization, err := merchantpayment.NewRuntimeLoader(s.store.DB(), s.redis).LoadAuthorization(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.WechatConfig{
|
||||
IsActive: true,
|
||||
OaAppID: authorization.OaAppID,
|
||||
OaAppSecret: authorization.OaAppSecret,
|
||||
OaToken: authorization.OaToken,
|
||||
OaAesKey: authorization.OaAesKey,
|
||||
OaOAuthRedirectURL: authorization.OaOAuthRedirectURL,
|
||||
MiniappAppID: authorization.MiniappAppID,
|
||||
MiniappAppSecret: authorization.MiniappAppSecret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetActiveConfigForAPI 获取当前生效的支付配置(API 响应,已脱敏)
|
||||
func (s *Service) GetActiveConfigForAPI(ctx context.Context) (*dto.WechatConfigResponse, error) {
|
||||
config, err := s.GetActiveConfig(ctx)
|
||||
@@ -594,30 +618,74 @@ func (s *Service) mergeSensitiveField(target *string, newVal *string) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigForCallback 按订单号查找创建该订单时所用的支付配置(含已软删除/停用的记录)
|
||||
// 回调验签必须使用创建订单时的密钥,否则签名不匹配。
|
||||
// 若找不到 payment_config_id(旧订单或数据缺失),回退到当前激活配置。
|
||||
// GetByIDUnscoped loads a historical comprehensive payment configuration, including soft-deleted rows.
|
||||
func (s *Service) GetByIDUnscoped(ctx context.Context, id uint) (*model.WechatConfig, error) {
|
||||
if s == nil || s.store == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "旧支付配置加载能力未配置")
|
||||
}
|
||||
return s.store.GetByIDUnscoped(ctx, id)
|
||||
}
|
||||
|
||||
// GetConfigForCallback 按支付单冻结的商户或旧支付配置加载回调凭证。
|
||||
// merchant_id 为空仅表示数据留存期内的历史支付:本 Change 只允许按其
|
||||
// payment_config_id 读取,独立 Change 完成留存清理后才可删除这条旧路径。
|
||||
// 禁止按当前启用池、当前综合配置推断或回填历史支付的实际商户。
|
||||
func (s *Service) GetConfigForCallback(ctx context.Context, orderNo string) (*model.WechatConfig, error) {
|
||||
if s.paymentStore != nil {
|
||||
payment, err := s.paymentStore.GetByPaymentNo(ctx, orderNo)
|
||||
if err == nil && payment.MerchantID != nil {
|
||||
loader := merchantpayment.NewRuntimeLoader(s.store.DB(), s.redis)
|
||||
merchant, loadErr := loader.LoadMerchant(ctx, *payment.MerchantID)
|
||||
if loadErr != nil {
|
||||
return nil, loadErr
|
||||
}
|
||||
// 仅微信直连(v3/v2)商户需要全局授权配置中的 AppID;富友商户不依赖该配置。
|
||||
// 授权字段只注入内存中的渠道配置,绝不写入支付快照、日志或审计。
|
||||
if merchant.ProviderType == model.ProviderTypeWechat || merchant.ProviderType == model.ProviderTypeWechatV2 {
|
||||
authorization, authErr := loader.LoadAuthorization(ctx)
|
||||
if authErr != nil {
|
||||
return nil, authErr
|
||||
}
|
||||
return merchantpayment.MerchantConfig(merchant, authorization)
|
||||
}
|
||||
return merchantpayment.MerchantConfig(merchant, nil)
|
||||
}
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询支付单路由失败")
|
||||
}
|
||||
if err == nil && payment.PaymentConfigID == nil {
|
||||
return nil, errors.New(errors.CodeNoPaymentConfig, "历史支付单缺少支付配置")
|
||||
}
|
||||
}
|
||||
|
||||
configID, err := s.resolvePaymentConfigID(ctx, orderNo)
|
||||
if err != nil {
|
||||
s.logger.Warn("回调:查询订单 payment_config_id 失败,回退激活配置",
|
||||
zap.String("order_no", orderNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询历史支付配置失败")
|
||||
}
|
||||
|
||||
if configID != nil {
|
||||
cfg, err := s.store.GetByIDUnscoped(ctx, *configID)
|
||||
if err == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
s.logger.Warn("回调:按 config_id 加载配置失败,回退激活配置",
|
||||
zap.Uint("config_id", *configID),
|
||||
zap.Error(err),
|
||||
)
|
||||
if configID == nil {
|
||||
return nil, errors.New(errors.CodeNoPaymentConfig, "历史支付单缺少支付配置")
|
||||
}
|
||||
cfg, loadErr := s.store.GetByIDUnscoped(ctx, *configID)
|
||||
if loadErr != nil {
|
||||
return nil, errors.Wrap(errors.CodeNoPaymentConfig, loadErr, "历史支付配置不可用")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
return s.GetActiveConfig(ctx)
|
||||
// GetPaymentServiceForCallback 使用已选定的支付配置构造回调验签实例。
|
||||
// 回调验签只能使用支付单冻结商户的当前凭证,不能复用进程启动时的旧支付实例。
|
||||
func (s *Service) GetPaymentServiceForCallback(config *model.WechatConfig) (wechat.PaymentServiceInterface, error) {
|
||||
if s == nil || s.redis == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "微信支付回调加载能力未配置")
|
||||
}
|
||||
if config == nil || config.ProviderType != model.ProviderTypeWechat {
|
||||
return nil, errors.New(errors.CodeWechatConfigUnavailable, "微信支付回调配置不可用")
|
||||
}
|
||||
app, err := wechat.NewPaymentAppFromConfig(config, config.OaAppID, wechat.NewRedisCache(s.redis), s.logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "构建微信支付回调验签实例失败")
|
||||
}
|
||||
return wechat.NewPaymentService(app, s.logger), nil
|
||||
}
|
||||
|
||||
// resolvePaymentConfigID 按订单号前缀从对应表中取 payment_config_id
|
||||
|
||||
Reference in New Issue
Block a user