fix: 修复微信/富友支付回调配置加载错误
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m5s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m5s
- WechatPayCallback 移除对永远为 nil 的单例 wechatPayment 的依赖, 改为动态按 payment_config_id 加载创建订单时所用的精确配置 - PaymentV2Service 新增 VerifyCallback()(v2 XML 验签)和 PeekOrderNo()(不验签预解析) - FuiouPayCallback 由 GetActiveConfig 改为 GetConfigForCallback, 通过 ParseNotify 预解析订单号后精确加载配置,防止切换配置后旧订单验签失败 - wechat_config.Service 注入 assetRechargeStore/agentRechargeStore, 新增 GetConfigForCallback():按订单号前缀查 payment_config_id, GetByIDUnscoped 加载配置(含已停用/软删除记录),找不到则回退激活配置 - 修复微信 v2 回调响应格式:从 JSON 改为 XML,避免微信持续重试
This commit is contained in:
@@ -108,7 +108,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
iotCard.SetPollingCallback(polling.NewAPICallback(deps.Redis, deps.Logger))
|
||||
|
||||
// 创建支付配置服务(Order 和 Recharge 依赖)
|
||||
wechatConfig := wechatConfigSvc.New(s.WechatConfig, s.Order, accountAudit, deps.Redis, deps.Logger)
|
||||
wechatConfig := wechatConfigSvc.New(s.WechatConfig, s.Order, s.AssetRecharge, s.AgentRecharge, accountAudit, deps.Redis, deps.Logger)
|
||||
|
||||
packageActivation := packageSvc.NewActivationService(
|
||||
deps.DB,
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/fuiou"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
)
|
||||
|
||||
@@ -28,6 +27,7 @@ type AgentRechargeServiceInterface interface {
|
||||
// WechatConfigServiceInterface 支付配置服务接口
|
||||
type WechatConfigServiceInterface interface {
|
||||
GetActiveConfig(ctx context.Context) (*model.WechatConfig, error)
|
||||
GetConfigForCallback(ctx context.Context, orderNo string) (*model.WechatConfig, error)
|
||||
}
|
||||
|
||||
type PaymentHandler struct {
|
||||
@@ -60,43 +60,84 @@ func NewPaymentHandler(
|
||||
// WechatPayCallback 微信支付回调(带签名验证)
|
||||
// POST /api/callback/wechat-pay
|
||||
func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
if h.wechatPayment == nil {
|
||||
body := c.Body()
|
||||
ctx := context.Background()
|
||||
|
||||
// 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置
|
||||
orderNo, err := wechat.PeekOrderNo(body)
|
||||
if err != nil {
|
||||
h.logger.Error("微信回调:预解析订单号失败", zap.Error(err))
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "回调数据格式错误")
|
||||
}
|
||||
|
||||
cfg, err := h.wechatConfigService.GetConfigForCallback(ctx, orderNo)
|
||||
if err != nil || cfg == nil {
|
||||
h.logger.Error("微信回调:加载支付配置失败",
|
||||
zap.String("order_no", orderNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "微信支付服务未配置")
|
||||
}
|
||||
|
||||
var httpReq http.Request
|
||||
fasthttpadaptor.ConvertRequest(c.Context(), &httpReq, true)
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := h.wechatPayment.HandlePaymentNotify(&httpReq, func(result *wechat.PaymentNotifyResult) error {
|
||||
switch cfg.ProviderType {
|
||||
case model.ProviderTypeWechatV2:
|
||||
paymentSvc := wechat.NewPaymentV2Service(cfg.OaAppID, cfg.WxMchID, cfg.WxAPIV2Key, cfg.WxNotifyURL, h.logger)
|
||||
result, err := paymentSvc.VerifyCallback(body)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "微信 v2 回调验签失败")
|
||||
}
|
||||
if result.TradeState != "SUCCESS" {
|
||||
return nil
|
||||
return h.wechatV2SuccessResponse(c)
|
||||
}
|
||||
if err := h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID); err != nil {
|
||||
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
|
||||
}
|
||||
|
||||
// TODO: 按 payment_config_id 加载配置验签(当前留桩,仍用 wechatPayment 单例验签)
|
||||
|
||||
// 按订单号前缀分发
|
||||
outTradeNo := result.OutTradeNo
|
||||
switch {
|
||||
case strings.HasPrefix(outTradeNo, "ORD"):
|
||||
return h.orderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat)
|
||||
case strings.HasPrefix(outTradeNo, constants.AssetRechargeOrderPrefix):
|
||||
return h.rechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, result.TransactionID)
|
||||
case strings.HasPrefix(outTradeNo, constants.AgentRechargeOrderPrefix):
|
||||
if h.agentRechargeService != nil {
|
||||
return h.agentRechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, result.TransactionID)
|
||||
case model.ProviderTypeWechat:
|
||||
if h.wechatPayment == nil {
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "微信 v3 支付未初始化")
|
||||
}
|
||||
var httpReq http.Request
|
||||
fasthttpadaptor.ConvertRequest(c.Context(), &httpReq, true)
|
||||
_, err := h.wechatPayment.HandlePaymentNotify(&httpReq, func(result *wechat.PaymentNotifyResult) error {
|
||||
if result.TradeState != "SUCCESS" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("代理充值服务未配置,无法处理订单: %s", outTradeNo)
|
||||
default:
|
||||
return fmt.Errorf("未知订单号前缀: %s", outTradeNo)
|
||||
return h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID)
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
|
||||
default:
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "不支持的支付渠道类型")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]string{"return_code": "SUCCESS"})
|
||||
return h.wechatV2SuccessResponse(c)
|
||||
}
|
||||
|
||||
// dispatchWechatCallback 按订单号前缀将支付结果分发到对应业务
|
||||
func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, outTradeNo, transactionID string) error {
|
||||
switch {
|
||||
case strings.HasPrefix(outTradeNo, "ORD"):
|
||||
return h.orderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat)
|
||||
case strings.HasPrefix(outTradeNo, constants.AssetRechargeOrderPrefix):
|
||||
return h.rechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID)
|
||||
case strings.HasPrefix(outTradeNo, constants.AgentRechargeOrderPrefix):
|
||||
if h.agentRechargeService != nil {
|
||||
return h.agentRechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID)
|
||||
}
|
||||
return fmt.Errorf("代理充值服务未配置,无法处理订单: %s", outTradeNo)
|
||||
default:
|
||||
return fmt.Errorf("未知订单号前缀: %s", outTradeNo)
|
||||
}
|
||||
}
|
||||
|
||||
// wechatV2SuccessResponse 返回微信 v2 要求的 XML 格式成功响应
|
||||
// v2 API 要求 XML 格式的 return_code=SUCCESS,否则微信会持续重试回调
|
||||
func (h *PaymentHandler) wechatV2SuccessResponse(c *fiber.Ctx) error {
|
||||
c.Set("Content-Type", "text/xml; charset=utf-8")
|
||||
return c.SendString(`<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>`)
|
||||
}
|
||||
|
||||
type AlipayCallbackRequest struct {
|
||||
@@ -151,20 +192,30 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
|
||||
ctx := c.UserContext()
|
||||
c.Set("Content-Type", "text/xml; charset=gbk")
|
||||
|
||||
activeConfig, err := h.wechatConfigService.GetActiveConfig(ctx)
|
||||
if err != nil || activeConfig == nil {
|
||||
h.logger.Error("富友回调:加载支付配置失败", zap.Error(err))
|
||||
// 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置
|
||||
preNotify, err := fuiou.ParseNotify(body)
|
||||
if err != nil {
|
||||
h.logger.Error("富友回调:预解析失败", zap.Error(err))
|
||||
return c.Send(fuiou.BuildNotifyFailResponse("parse failed"))
|
||||
}
|
||||
|
||||
cfg, err := h.wechatConfigService.GetConfigForCallback(ctx, preNotify.MchntOrderNo)
|
||||
if err != nil || cfg == nil {
|
||||
h.logger.Error("富友回调:加载支付配置失败",
|
||||
zap.String("order_no", preNotify.MchntOrderNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return c.Send(fuiou.BuildNotifyFailResponse("payment config unavailable"))
|
||||
}
|
||||
|
||||
client, err := fuiou.NewClient(
|
||||
activeConfig.FyInsCd,
|
||||
activeConfig.FyMchntCd,
|
||||
activeConfig.FyTermID,
|
||||
activeConfig.FyAPIURL,
|
||||
activeConfig.FyNotifyURL,
|
||||
activeConfig.FyPrivateKey,
|
||||
activeConfig.FyPublicKey,
|
||||
cfg.FyInsCd,
|
||||
cfg.FyMchntCd,
|
||||
cfg.FyTermID,
|
||||
cfg.FyAPIURL,
|
||||
cfg.FyNotifyURL,
|
||||
cfg.FyPrivateKey,
|
||||
cfg.FyPublicKey,
|
||||
h.logger,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -31,21 +31,33 @@ type AuditServiceInterface interface {
|
||||
|
||||
// Service 微信参数配置业务服务
|
||||
type Service struct {
|
||||
store *postgres.WechatConfigStore
|
||||
orderStore *postgres.OrderStore
|
||||
auditService AuditServiceInterface
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
store *postgres.WechatConfigStore
|
||||
orderStore *postgres.OrderStore
|
||||
assetRechargeStore *postgres.AssetRechargeStore
|
||||
agentRechargeStore *postgres.AgentRechargeStore
|
||||
auditService AuditServiceInterface
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// New 创建微信参数配置服务实例
|
||||
func New(store *postgres.WechatConfigStore, orderStore *postgres.OrderStore, auditService AuditServiceInterface, rdb *redis.Client, logger *zap.Logger) *Service {
|
||||
func New(
|
||||
store *postgres.WechatConfigStore,
|
||||
orderStore *postgres.OrderStore,
|
||||
assetRechargeStore *postgres.AssetRechargeStore,
|
||||
agentRechargeStore *postgres.AgentRechargeStore,
|
||||
auditService AuditServiceInterface,
|
||||
rdb *redis.Client,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
orderStore: orderStore,
|
||||
auditService: auditService,
|
||||
redis: rdb,
|
||||
logger: logger,
|
||||
store: store,
|
||||
orderStore: orderStore,
|
||||
assetRechargeStore: assetRechargeStore,
|
||||
agentRechargeStore: agentRechargeStore,
|
||||
auditService: auditService,
|
||||
redis: rdb,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,3 +483,58 @@ func (s *Service) mergeSensitiveField(target *string, newVal *string) {
|
||||
*target = *newVal
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigForCallback 按订单号查找创建该订单时所用的支付配置(含已软删除/停用的记录)
|
||||
// 回调验签必须使用创建订单时的密钥,否则签名不匹配。
|
||||
// 若找不到 payment_config_id(旧订单或数据缺失),回退到当前激活配置。
|
||||
func (s *Service) GetConfigForCallback(ctx context.Context, orderNo string) (*model.WechatConfig, error) {
|
||||
configID, err := s.resolvePaymentConfigID(ctx, orderNo)
|
||||
if err != nil {
|
||||
s.logger.Warn("回调:查询订单 payment_config_id 失败,回退激活配置",
|
||||
zap.String("order_no", orderNo),
|
||||
zap.Error(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),
|
||||
)
|
||||
}
|
||||
|
||||
return s.GetActiveConfig(ctx)
|
||||
}
|
||||
|
||||
// resolvePaymentConfigID 按订单号前缀从对应表中取 payment_config_id
|
||||
func (s *Service) resolvePaymentConfigID(ctx context.Context, orderNo string) (*uint, error) {
|
||||
switch {
|
||||
case len(orderNo) >= 3 && orderNo[:3] == "ORD":
|
||||
order, err := s.orderStore.GetByOrderNo(ctx, orderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return order.PaymentConfigID, nil
|
||||
|
||||
case len(orderNo) >= 4 && orderNo[:4] == constants.AssetRechargeOrderPrefix:
|
||||
record, err := s.assetRechargeStore.GetByRechargeNo(ctx, orderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record.PaymentConfigID, nil
|
||||
|
||||
case len(orderNo) >= 4 && orderNo[:4] == constants.AgentRechargeOrderPrefix:
|
||||
record, err := s.agentRechargeStore.GetByRechargeNo(ctx, orderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record.PaymentConfigID, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("未知订单号前缀: %s", orderNo)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user