实现支付商户池与微信授权配置
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 3m55s

新增收款商户、商户池轮询、微信授权配置独立管理;三类新支付
(C端套餐购买、C端资产钱包充值、代理在线预存款充值)无条件
经商户池选择并冻结路由,无旧综合配置回退。merchant_id 为空
历史支付继续按 payment_config_id 双读。凭证版本化加载与
ID+版本缓存保证轮换一致性。删除商户池新支付创建开关及全部
引用。
This commit is contained in:
2026-09-09 18:13:04 +08:00
parent ff25586dc9
commit 98c145fe70
39 changed files with 2718 additions and 415 deletions

View File

@@ -0,0 +1,218 @@
package admin
import (
"strconv"
"github.com/gofiber/fiber/v2"
merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// PaymentMerchantHandler 处理特权商户与商户池配置请求。
type PaymentMerchantHandler struct {
service *merchantpayment.ManagementService
}
// NewPaymentMerchantHandler 创建商户池管理处理器。
func NewPaymentMerchantHandler(service *merchantpayment.ManagementService) *PaymentMerchantHandler {
return &PaymentMerchantHandler{service: service}
}
func pathID(c *fiber.Ctx) (uint, error) {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil || id == 0 {
return 0, errors.New(errors.CodeInvalidParam, "无效的路径ID")
}
return uint(id), nil
}
// CreateMerchant 创建支付商户。
// POST /api/admin/payment-merchants
func (h *PaymentMerchantHandler) CreateMerchant(c *fiber.Ctx) error {
var r dto.PaymentMerchantRequest
if err := c.BodyParser(&r); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
v, err := h.service.CreateMerchant(c.UserContext(), r)
if err != nil {
return err
}
return response.Success(c, v)
}
// ListMerchants 分页查询支付商户。
// GET /api/admin/payment-merchants
func (h *PaymentMerchantHandler) ListMerchants(c *fiber.Ctx) error {
var r dto.PaymentMerchantListRequest
if err := c.QueryParser(&r); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
v, n, err := h.service.ListMerchants(c.UserContext(), r)
if err != nil {
return err
}
return response.SuccessWithPagination(c, v, n, r.Page, r.PageSize)
}
// GetMerchant 查询支付商户详情。
// GET /api/admin/payment-merchants/:id
func (h *PaymentMerchantHandler) GetMerchant(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
v, err := h.service.GetMerchant(c.UserContext(), id)
if err != nil {
return err
}
return response.Success(c, v)
}
// UpdateMerchant 更新支付商户。
// PUT /api/admin/payment-merchants/:id
func (h *PaymentMerchantHandler) UpdateMerchant(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
var r dto.PaymentMerchantUpdateRequest
if err := c.BodyParser(&r); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
v, err := h.service.UpdateMerchant(c.UserContext(), id, r)
if err != nil {
return err
}
return response.Success(c, v)
}
// DeleteMerchant 删除支付商户。
// DELETE /api/admin/payment-merchants/:id
func (h *PaymentMerchantHandler) DeleteMerchant(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
var r dto.PaymentMerchantDeleteRequest
if err := c.BodyParser(&r); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.service.DeleteMerchant(c.UserContext(), id, r.Confirm); err != nil {
return err
}
return response.Success(c, nil)
}
// CreatePool 创建商户池。
// POST /api/admin/payment-merchant-pools
func (h *PaymentMerchantHandler) CreatePool(c *fiber.Ctx) error {
var r dto.PaymentMerchantPoolRequest
if err := c.BodyParser(&r); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
v, err := h.service.SavePool(c.UserContext(), 0, r)
if err != nil {
return err
}
return response.Success(c, v)
}
// UpdatePool 更新商户池。
// PUT /api/admin/payment-merchant-pools/:id
func (h *PaymentMerchantHandler) UpdatePool(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
var r dto.PaymentMerchantPoolRequest
if err := c.BodyParser(&r); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
v, err := h.service.SavePool(c.UserContext(), id, r)
if err != nil {
return err
}
return response.Success(c, v)
}
// EnablePool 启用商户池。
// POST /api/admin/payment-merchant-pools/:id/enable
func (h *PaymentMerchantHandler) EnablePool(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
result, err := h.service.SetPoolEnabled(c.UserContext(), id, true)
if err != nil {
return err
}
return response.Success(c, result)
}
// DisablePool 停用商户池。
// POST /api/admin/payment-merchant-pools/:id/disable
func (h *PaymentMerchantHandler) DisablePool(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
result, err := h.service.SetPoolEnabled(c.UserContext(), id, false)
if err != nil {
return err
}
return response.Success(c, result)
}
// GetAuthorization 获取微信授权配置。
// GET /api/admin/wechat-authorizations
func (h *PaymentMerchantHandler) GetAuthorization(c *fiber.Ctx) error {
v, err := h.service.GetAuthorization(c.UserContext())
if err != nil {
return err
}
return response.Success(c, v)
}
// ListPools 分页查询商户池。
// GET /api/admin/payment-merchant-pools
func (h *PaymentMerchantHandler) ListPools(c *fiber.Ctx) error {
var r dto.PaymentMerchantPoolListRequest
if err := c.QueryParser(&r); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
v, total, err := h.service.ListPools(c.UserContext(), r)
if err != nil {
return err
}
return response.SuccessWithPagination(c, v, total, r.Page, r.PageSize)
}
// GetPool 查询商户池详情。
// GET /api/admin/payment-merchant-pools/:id
func (h *PaymentMerchantHandler) GetPool(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
v, err := h.service.GetPool(c.UserContext(), id)
if err != nil {
return err
}
return response.Success(c, v)
}
// SaveAuthorization 保存微信授权配置。
// PUT /api/admin/wechat-authorizations/current
func (h *PaymentMerchantHandler) SaveAuthorization(c *fiber.Ctx) error {
var r dto.WechatAuthorizationRequest
if err := c.BodyParser(&r); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
v, err := h.service.SaveAuthorization(c.UserContext(), r)
if err != nil {
return err
}
return response.Success(c, v)
}

View File

@@ -8,6 +8,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/middleware"
@@ -16,7 +17,7 @@ import (
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
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"
@@ -41,7 +42,7 @@ type ClientWalletHandler struct {
paymentStore *postgres.PaymentStore
rechargeService *rechargeSvc.Service
openIDStore *postgres.PersonalCustomerOpenIDStore
wechatConfigService *wechatConfigSvc.Service
merchantRuntime *merchantpayment.RuntimeLoader
redis *redis.Client
logger *zap.Logger
db *gorm.DB
@@ -73,7 +74,6 @@ func NewClientWalletHandler(
paymentStore *postgres.PaymentStore,
rechargeService *rechargeSvc.Service,
openIDStore *postgres.PersonalCustomerOpenIDStore,
wechatConfigService *wechatConfigSvc.Service,
redisClient *redis.Client,
logger *zap.Logger,
db *gorm.DB,
@@ -81,20 +81,20 @@ func NewClientWalletHandler(
deviceStore *postgres.DeviceStore,
) *ClientWalletHandler {
return &ClientWalletHandler{
assetService: assetService,
customerBinding: binding,
walletStore: walletStore,
transactionStore: transactionStore,
rechargeOrderStore: rechargeOrderStore,
paymentStore: paymentStore,
rechargeService: rechargeService,
openIDStore: openIDStore,
wechatConfigService: wechatConfigService,
redis: redisClient,
logger: logger,
db: db,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
assetService: assetService,
customerBinding: binding,
walletStore: walletStore,
transactionStore: transactionStore,
rechargeOrderStore: rechargeOrderStore,
paymentStore: paymentStore,
rechargeService: rechargeService,
openIDStore: openIDStore,
merchantRuntime: merchantpayment.NewRuntimeLoader(db, redisClient),
redis: redisClient,
logger: logger,
db: db,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
}
}
@@ -321,19 +321,11 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error {
// }
// }
config, err := h.wechatConfigService.GetActiveConfig(resolved.SkipPermissionCtx)
if err != nil {
return err
}
if config == nil {
return errors.New(errors.CodeWechatConfigUnavailable)
}
switch req.PaymentMethod {
case constants.RechargeMethodAlipay:
return h.createAlipayRecharge(c, resolved, config, wallet, req)
return h.createAlipayRecharge(c, resolved, wallet, req)
case constants.RechargeMethodWechat:
return h.createWechatRecharge(c, resolved, config, wallet, req)
return h.createWechatRecharge(c, resolved, wallet, req)
default:
return errors.New(errors.CodePaymentMethodUnavailable)
}
@@ -343,15 +335,13 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error {
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)
authorization, appID, err := h.loadWechatAuthorization(resolved.SkipPermissionCtx, req.AppType)
if err != nil {
return err
}
openID, err := h.findOpenIDByCustomerAndAppID(resolved.SkipPermissionCtx, resolved.CustomerID, appID)
if err != nil {
return err
@@ -359,9 +349,47 @@ func (h *ClientWalletHandler) createWechatRecharge(
rechargeNo := generateClientRechargeNo()
paymentNo := generateClientPaymentNo()
rechargeOrder := &model.RechargeOrder{
RechargeOrderNo: rechargeNo,
UserID: resolved.CustomerID,
AssetWalletID: wallet.ID,
ResourceType: resolved.ResourceType,
ResourceID: resolved.Asset.AssetID,
Amount: req.Amount,
Status: model.RechargeOrderStatusPending,
ShopIDTag: wallet.ShopIDTag,
EnterpriseIDTag: wallet.EnterpriseIDTag,
OperatorType: constants.OperatorTypePersonalCustomer,
Generation: resolved.Generation,
}
payment := &model.Payment{
PaymentNo: paymentNo,
OrderID: rechargeOrder.ID,
OrderType: model.PaymentOrderTypeRecharge,
PaymentMethod: model.PaymentByWechat,
Amount: req.Amount,
Status: model.PaymentRecordStatusPending,
}
var config *model.WechatConfig
if err := h.db.WithContext(resolved.SkipPermissionCtx).Transaction(func(tx *gorm.DB) error {
route, selectedConfig, err := h.selectMerchantConfig(resolved.SkipPermissionCtx, tx, model.PaymentByWechat, authorization)
if err != nil {
return err
}
config = selectedConfig
merchantpayment.FreezeRoute(payment, route)
if err := h.rechargeOrderStore.CreateWithTx(resolved.SkipPermissionCtx, tx, rechargeOrder); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
}
payment.OrderID = rechargeOrder.ID
if err := h.paymentStore.CreateWithTx(resolved.SkipPermissionCtx, tx, payment); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
}
return h.appendRechargePaymentCreatedAudit(resolved.SkipPermissionCtx, tx, payment, rechargeOrder)
}); err != nil {
return err
}
// 先初始化生效支付通道并创建预支付订单,确认支付通道可用
// 避免先写入充值记录后支付初始化失败,导致产生孤儿记录
attempt, startedAt, err := h.startRechargePaymentAttempt(resolved.SkipPermissionCtx, config, paymentNo, rechargeNo, req.Amount)
if err != nil {
return err
@@ -380,49 +408,15 @@ func (h *ClientWalletHandler) createWechatRecharge(
if completeErr := h.completeRechargePaymentAttempt(resolved.SkipPermissionCtx, attempt, startedAt, constants.IntegrationResultUnknown, "request_unknown", "充值支付预下单结果未知"); completeErr != nil {
return completeErr
}
if updateErr := h.markRechargePaymentFailed(resolved.SkipPermissionCtx, payment); updateErr != nil {
return updateErr
}
return err
}
if err := h.completeRechargePaymentAttempt(resolved.SkipPermissionCtx, attempt, startedAt, constants.IntegrationResultSuccess, "SUCCESS", ""); err != nil {
return err
}
// 支付通道确认可用后,再创建充值订单和支付记录
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,
OrderID: rechargeOrder.ID,
OrderType: model.PaymentOrderTypeRecharge,
PaymentMethod: model.PaymentByWechat,
Amount: req.Amount,
Status: model.PaymentRecordStatusPending,
PaymentConfigID: &config.ID,
}
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
if err := h.paymentStore.CreateWithTx(resolved.SkipPermissionCtx, tx, payment); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
}
return h.appendRechargePaymentCreatedAudit(resolved.SkipPermissionCtx, tx, payment, rechargeOrder)
}); err != nil {
return err
}
return response.Success(c, &dto.ClientRechargeResponse{
Recharge: dto.ClientRechargeResult{
RechargeID: rechargeOrder.ID,
@@ -439,18 +433,11 @@ func (h *ClientWalletHandler) createWechatRecharge(
func (h *ClientWalletHandler) createAlipayRecharge(
c *fiber.Ctx,
resolved *resolvedWalletAssetContext,
config *model.WechatConfig,
wallet *model.AssetWallet,
req dto.ClientCreateRechargeRequest,
) error {
rechargeNo := generateClientRechargeNo()
paymentNo := generateClientPaymentNo()
expireMinutes := config.AliPayExpireMinutes
if expireMinutes <= 0 {
expireMinutes = 30
}
expireAt := time.Now().Add(time.Duration(expireMinutes) * time.Minute)
rechargeOrder := &model.RechargeOrder{
RechargeOrderNo: rechargeNo,
UserID: resolved.CustomerID,
@@ -459,24 +446,33 @@ func (h *ClientWalletHandler) createAlipayRecharge(
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,
PaymentNo: paymentNo,
OrderType: model.PaymentOrderTypeRecharge,
PaymentMethod: model.PaymentByAlipay,
Amount: req.Amount,
Status: model.PaymentRecordStatusPending,
}
// WAP URL 是本地签名,不需要先调第三方,在事务内创建充值单和支付单
var config *model.WechatConfig
var expireAt time.Time
if err := h.db.WithContext(resolved.SkipPermissionCtx).Transaction(func(tx *gorm.DB) error {
route, selectedConfig, err := h.selectMerchantConfig(resolved.SkipPermissionCtx, tx, model.PaymentByAlipay, nil)
if err != nil {
return err
}
expireMinutes := selectedConfig.AliPayExpireMinutes
if expireMinutes <= 0 {
expireMinutes = 30
}
expireAt = time.Now().Add(time.Duration(expireMinutes) * time.Minute)
config = selectedConfig
payment.ExpireAt = &expireAt
merchantpayment.FreezeRoute(payment, route)
if err := h.rechargeOrderStore.CreateWithTx(resolved.SkipPermissionCtx, tx, rechargeOrder); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
}
@@ -505,7 +501,6 @@ func (h *ClientWalletHandler) createAlipayRecharge(
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)
@@ -738,7 +733,37 @@ func parseOptionalTime(value string) (*time.Time, error) {
return nil, fmt.Errorf("invalid time format")
}
func pickAppIDByType(config *model.WechatConfig, appType string) (string, error) {
func (h *ClientWalletHandler) loadWechatAuthorization(ctx context.Context, appType string) (*model.WechatAuthorization, string, error) {
if h.merchantRuntime == nil {
return nil, "", errors.New(errors.CodeServiceUnavailable, "商户池路由能力未配置")
}
authorization, err := h.merchantRuntime.LoadAuthorization(ctx)
if err != nil {
return nil, "", err
}
appID, err := pickAppIDByType(authorization, appType)
if err != nil {
return nil, "", err
}
return authorization, appID, nil
}
func (h *ClientWalletHandler) selectMerchantConfig(ctx context.Context, tx *gorm.DB, paymentMethod string, authorization *model.WechatAuthorization) (*merchantpayment.RouteSelection, *model.WechatConfig, error) {
if h.merchantRuntime == nil {
return nil, nil, errors.New(errors.CodeServiceUnavailable, "商户池路由能力未配置")
}
route, err := h.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 pickAppIDByType(config *model.WechatAuthorization, appType string) (string, error) {
switch appType {
case "official_account":
if strings.TrimSpace(config.OaAppID) == "" {

View File

@@ -54,7 +54,7 @@ func (h *ClientWechatHandler) GetJSSDKConfig(c *fiber.Ctx) error {
return errors.New(errors.CodeInvalidParam)
}
wechatConfig, err := h.wechatConfigService.GetActiveConfig(c.UserContext())
wechatConfig, err := h.wechatConfigService.GetAuthorizationConfig(c.UserContext())
if err != nil {
return err
}
@@ -82,7 +82,7 @@ func (h *ClientWechatHandler) GetJSSDKConfig(c *fiber.Ctx) error {
// GetAppID 获取当前生效的公众号 AppID
// GET /api/c/v1/wechat/appid
func (h *ClientWechatHandler) GetAppID(c *fiber.Ctx) error {
wechatConfig, err := h.wechatConfigService.GetActiveConfig(c.UserContext())
wechatConfig, err := h.wechatConfigService.GetAuthorizationConfig(c.UserContext())
if err != nil {
return err
}

View File

@@ -38,6 +38,7 @@ type AgentRechargeServiceInterface interface {
type WechatConfigServiceInterface interface {
GetActiveConfig(ctx context.Context) (*model.WechatConfig, error)
GetConfigForCallback(ctx context.Context, orderNo string) (*model.WechatConfig, error)
GetPaymentServiceForCallback(config *model.WechatConfig) (wechat.PaymentServiceInterface, error)
}
type PaymentHandler struct {
@@ -98,7 +99,7 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
body := c.Body()
ctx := c.UserContext()
// 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置
// 预解析订单号(不验签),用于按支付单冻结商户或历史 payment_config_id 双读加载配置
orderNo, err := wechat.PeekOrderNo(body)
if err != nil {
h.logger.Error("微信回调:预解析订单号失败", zap.Error(err))
@@ -144,12 +145,13 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
}
case model.ProviderTypeWechat:
if h.wechatPayment == nil {
return errors.New(errors.CodeWechatCallbackInvalid, "微信 v3 支付未初始化")
paymentSvc, err := h.wechatConfigService.GetPaymentServiceForCallback(cfg)
if err != nil {
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "构建微信 v3 回调验签实例失败")
}
var httpReq http.Request
fasthttpadaptor.ConvertRequest(c.Context(), &httpReq, true)
_, err := h.wechatPayment.HandlePaymentNotify(&httpReq, func(result *wechat.PaymentNotifyResult) error {
_, err = paymentSvc.HandlePaymentNotify(&httpReq, func(result *wechat.PaymentNotifyResult) error {
if result.TradeState != "SUCCESS" {
return h.recordIgnoredPaymentCallback(ctx, verifiedPaymentCallback{
PaymentNo: result.OutTradeNo, TransactionID: result.TransactionID,
@@ -421,7 +423,7 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
return errors.New(errors.CodeInvalidParam, "订单号不能为空")
}
// 按 payment_config_id 加载创建支付单时所用的配置(支持已停用配置)
// 按支付单冻结商户或历史 payment_config_id 双读加载配置(支持已停用配置)
cfg, err := h.wechatConfigService.GetConfigForCallback(ctx, outTradeNo)
if err != nil || cfg == nil {
h.logger.Error("支付宝回调:加载支付配置失败",
@@ -494,13 +496,14 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
return errors.New(errors.CodeWechatCallbackInvalid, "支付通道校验失败")
}
// 校验 payment_config_id 与当前配置一致;旧数据为空时兼容并记录 warn
if payment.PaymentConfigID == nil {
// 商户支付单已由 GetConfigForCallback 按 merchant_id 绑定当前商户凭证;
// payment_config_id 只属于历史双读路径,不能用于否定新商户回调。
if payment.MerchantID == nil && payment.PaymentConfigID == nil {
h.logger.Warn("支付宝回调payment_config_id 为空,跳过配置 ID 校验(旧数据兼容)",
zap.String("out_trade_no", outTradeNo),
zap.Uint("config_id", cfg.ID),
)
} else if *payment.PaymentConfigID != cfg.ID {
} else if payment.MerchantID == nil && *payment.PaymentConfigID != cfg.ID {
h.logger.Error("支付宝回调payment_config_id 不匹配",
zap.String("out_trade_no", outTradeNo),
zap.Uint("payment_config_id", *payment.PaymentConfigID),
@@ -567,7 +570,7 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
ctx := c.UserContext()
c.Set("Content-Type", "text/plain; charset=utf-8")
// 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置
// 预解析订单号(不验签),用于按支付单冻结商户或历史 payment_config_id 双读加载配置
preNotify, err := fuiou.ParseNotify(body)
if err != nil {
h.logger.Error("富友回调:预解析失败",