This commit is contained in:
@@ -2,16 +2,18 @@ package callback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp/fasthttpadaptor"
|
||||
"go.uber.org/zap"
|
||||
|
||||
agentrechargeApp "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
orderService "github.com/break/junhong_cmp_fiber/internal/service/order"
|
||||
rechargeService "github.com/break/junhong_cmp_fiber/internal/service/recharge"
|
||||
@@ -21,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/fuiou"
|
||||
pkgmiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -44,6 +47,8 @@ type PaymentHandler struct {
|
||||
wechatPayment wechat.PaymentServiceInterface
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
paymentStore *postgres.PaymentStore
|
||||
agentPaymentConfirm *agentrechargeApp.ConfirmOnlinePaymentService
|
||||
integration *integrationlog.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -55,6 +60,8 @@ func NewPaymentHandler(
|
||||
wechatPayment wechat.PaymentServiceInterface,
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
paymentStore *postgres.PaymentStore,
|
||||
agentPaymentConfirm *agentrechargeApp.ConfirmOnlinePaymentService,
|
||||
integration *integrationlog.Repository,
|
||||
logger *zap.Logger,
|
||||
) *PaymentHandler {
|
||||
return &PaymentHandler{
|
||||
@@ -65,15 +72,30 @@ func NewPaymentHandler(
|
||||
wechatPayment: wechatPayment,
|
||||
wechatConfigService: wechatConfigService,
|
||||
paymentStore: paymentStore,
|
||||
agentPaymentConfirm: agentPaymentConfirm,
|
||||
integration: integration,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type verifiedPaymentCallback struct {
|
||||
PaymentNo string
|
||||
PaymentMethod string
|
||||
TransactionID string
|
||||
Amount int64
|
||||
ConfigID uint
|
||||
MerchantIdentity string
|
||||
PaidAt time.Time
|
||||
Provider string
|
||||
RawPayload []byte
|
||||
ContentType string
|
||||
}
|
||||
|
||||
// WechatPayCallback 微信支付回调(带签名验证)
|
||||
// POST /api/callback/wechat-pay
|
||||
func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
body := c.Body()
|
||||
ctx := context.Background()
|
||||
ctx := c.UserContext()
|
||||
|
||||
// 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置
|
||||
orderNo, err := wechat.PeekOrderNo(body)
|
||||
@@ -103,7 +125,13 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
}
|
||||
// TotalFee 为字符串格式的分,解析失败则降级为 0(后续 handlePaymentCallback 会记录日志)
|
||||
totalFee, _ := strconv.ParseInt(result.TotalFee, 10, 64)
|
||||
if err := h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID, totalFee); err != nil {
|
||||
paidAt, _ := time.ParseInLocation("20060102150405", result.SuccessTime, time.Local)
|
||||
if err := h.dispatchWechatCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: result.OutTradeNo, PaymentMethod: model.PaymentMethodWechat,
|
||||
TransactionID: result.TransactionID, Amount: totalFee, ConfigID: cfg.ID,
|
||||
MerchantIdentity: cfg.WxMchID, PaidAt: paidAt, Provider: constants.IntegrationProviderWechatPay,
|
||||
RawPayload: body, ContentType: c.Get("Content-Type"),
|
||||
}); err != nil {
|
||||
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
|
||||
}
|
||||
|
||||
@@ -117,7 +145,13 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
if result.TradeState != "SUCCESS" {
|
||||
return nil
|
||||
}
|
||||
return h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID, result.TotalAmount)
|
||||
paidAt, _ := time.Parse(time.RFC3339, result.SuccessTime)
|
||||
return h.dispatchWechatCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: result.OutTradeNo, PaymentMethod: model.PaymentMethodWechat,
|
||||
TransactionID: result.TransactionID, Amount: result.TotalAmount, ConfigID: cfg.ID,
|
||||
MerchantIdentity: cfg.WxMchID, PaidAt: paidAt, Provider: constants.IntegrationProviderWechatPay,
|
||||
RawPayload: body, ContentType: c.Get("Content-Type"),
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
|
||||
@@ -130,20 +164,22 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
return h.wechatV2SuccessResponse(c)
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, paymentNo, paymentMethod, transactionID string, paidAmount int64) (bool, error) {
|
||||
func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, callback verifiedPaymentCallback) (bool, error) {
|
||||
if h.paymentStore != nil {
|
||||
payment, err := h.paymentStore.GetByPaymentNo(ctx, paymentNo)
|
||||
payment, err := h.paymentStore.GetByPaymentNo(ctx, callback.PaymentNo)
|
||||
if err == nil {
|
||||
switch payment.OrderType {
|
||||
case model.PaymentOrderTypePackage:
|
||||
return true, h.orderService.HandlePaymentRecordCallback(ctx, paymentNo, paymentMethod, transactionID, paidAmount)
|
||||
return true, h.orderService.HandlePaymentRecordCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID, callback.Amount)
|
||||
case model.PaymentOrderTypeRecharge:
|
||||
if h.rechargeOrderService != nil {
|
||||
return true, h.rechargeOrderService.HandlePaymentCallback(ctx, paymentNo, paymentMethod, transactionID)
|
||||
return true, h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID)
|
||||
}
|
||||
return true, fmt.Errorf("充值订单服务未配置,无法处理支付单: %s", paymentNo)
|
||||
return true, errors.New(errors.CodeInternalError, "充值订单服务未配置")
|
||||
case model.PaymentOrderTypeAgentRecharge:
|
||||
return true, h.confirmAgentRechargePayment(ctx, callback)
|
||||
default:
|
||||
return true, fmt.Errorf("未知支付记录类型: %s", payment.OrderType)
|
||||
return true, errors.New(errors.CodeInvalidStatus, "未知支付记录类型")
|
||||
}
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
@@ -154,27 +190,83 @@ func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, paym
|
||||
}
|
||||
|
||||
// dispatchWechatCallback 优先按支付单分发,旧单号继续按前缀兼容处理。
|
||||
func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, outTradeNo, transactionID string, paidAmount int64) error {
|
||||
handled, err := h.dispatchPaymentRecordCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID, paidAmount)
|
||||
func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, callback verifiedPaymentCallback) error {
|
||||
handled, err := h.dispatchPaymentRecordCallback(ctx, callback)
|
||||
if handled || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(outTradeNo, "ORD"):
|
||||
return h.orderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, paidAmount)
|
||||
case strings.HasPrefix(outTradeNo, constants.AssetRechargeOrderPrefix):
|
||||
case strings.HasPrefix(callback.PaymentNo, "ORD"):
|
||||
return h.orderService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentMethodWechat, callback.Amount)
|
||||
case strings.HasPrefix(callback.PaymentNo, constants.AssetRechargeOrderPrefix):
|
||||
if h.rechargeOrderService != nil {
|
||||
return h.rechargeOrderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentByWechat, transactionID)
|
||||
return h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentByWechat, callback.TransactionID)
|
||||
}
|
||||
return fmt.Errorf("充值订单服务未配置,无法处理订单: %s", outTradeNo)
|
||||
case strings.HasPrefix(outTradeNo, constants.AgentRechargeOrderPrefix):
|
||||
return errors.New(errors.CodeInternalError, "充值订单服务未配置")
|
||||
case strings.HasPrefix(callback.PaymentNo, constants.AgentRechargeOrderPrefix):
|
||||
if h.agentRechargeService != nil {
|
||||
return h.agentRechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID, paidAmount)
|
||||
return h.agentRechargeService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentMethodWechat, callback.TransactionID, callback.Amount)
|
||||
}
|
||||
return fmt.Errorf("代理充值服务未配置,无法处理订单: %s", outTradeNo)
|
||||
return errors.New(errors.CodeInternalError, "代理充值服务未配置")
|
||||
default:
|
||||
return fmt.Errorf("未知订单号前缀: %s", outTradeNo)
|
||||
return errors.New(errors.CodeInvalidStatus, "未知订单号前缀")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) confirmAgentRechargePayment(ctx context.Context, callback verifiedPaymentCallback) error {
|
||||
if h.agentPaymentConfirm == nil || h.integration == nil {
|
||||
return errors.New(errors.CodeInternalError, "代理充值支付回调能力未配置")
|
||||
}
|
||||
resourceID, correlationID := callback.PaymentNo, callback.PaymentNo
|
||||
idempotencyKey := callback.TransactionID
|
||||
if idempotencyKey == "" {
|
||||
idempotencyKey = callback.PaymentNo
|
||||
}
|
||||
log, _, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
|
||||
IdempotencyKey: idempotencyKey, Provider: callback.Provider,
|
||||
Operation: constants.IntegrationOperationPaymentCallback, ExternalID: callback.TransactionID,
|
||||
ResourceType: constants.IntegrationResourceTypeAgentRechargePayment, ResourceID: &resourceID,
|
||||
RawPayload: callback.RawPayload, ContentType: callback.ContentType,
|
||||
RequestID: pkgmiddleware.GetRequestIDFromContext(ctx), CorrelationID: &correlationID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, confirmErr := h.agentPaymentConfirm.Execute(ctx, agentrechargeApp.ConfirmOnlinePaymentCommand{
|
||||
PaymentNo: callback.PaymentNo, PaymentMethod: callback.PaymentMethod, ConfigID: callback.ConfigID,
|
||||
MerchantIdentity: callback.MerchantIdentity, ThirdPartyTradeNo: callback.TransactionID,
|
||||
Amount: callback.Amount, PaidAt: callback.PaidAt, CorrelationID: correlationID,
|
||||
})
|
||||
if confirmErr != nil {
|
||||
h.completePaymentCallbackLog(ctx, log, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultFailed, ProviderMessage: confirmErr.Error(),
|
||||
ResponseSummary: map[string]any{"confirmed": false},
|
||||
})
|
||||
return confirmErr
|
||||
}
|
||||
if log.Result == constants.IntegrationResultPending {
|
||||
_, err = h.integration.Complete(ctx, log.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess, ProviderCode: "SUCCESS",
|
||||
ResponseSummary: map[string]any{"confirmed": true, "already_confirmed": result.AlreadyConfirmed},
|
||||
StateChanged: !result.AlreadyConfirmed,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) completePaymentCallbackLog(ctx context.Context, log *model.IntegrationLog, completion integrationlog.Completion) {
|
||||
if log == nil || log.Result != constants.IntegrationResultPending {
|
||||
return
|
||||
}
|
||||
if _, err := h.integration.Complete(ctx, log.IntegrationID, completion); err != nil {
|
||||
h.logger.Error("支付回调 Integration Log 终结失败",
|
||||
zap.String("integration_id", log.IntegrationID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,14 +397,16 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "支付金额校验失败")
|
||||
}
|
||||
|
||||
// 写入支付宝交易流水号
|
||||
if err := h.paymentStore.UpdatePaymentInfo(ctx, payment.ID, notification.TradeNo, nil); err != nil {
|
||||
h.logger.Error("支付宝回调:写入 third_party_trade_no 失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", notification.TradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 不中断业务,继续分发(幂等业务层会处理状态)
|
||||
// 新代理充值由统一确认事务原子写入交易号,旧业务保持原处理方式。
|
||||
if payment.OrderType != model.PaymentOrderTypeAgentRecharge {
|
||||
if err := h.paymentStore.UpdatePaymentInfo(ctx, payment.ID, notification.TradeNo, nil); err != nil {
|
||||
h.logger.Error("支付宝回调:写入 third_party_trade_no 失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", notification.TradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 不中断存量业务,继续由原幂等业务层处理状态。
|
||||
}
|
||||
}
|
||||
|
||||
// 按支付单 order_type 分发业务
|
||||
@@ -353,6 +447,28 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
|
||||
zap.String("order_type", payment.OrderType),
|
||||
)
|
||||
|
||||
case model.PaymentOrderTypeAgentRecharge:
|
||||
paidAt, parseErr := time.ParseInLocation("2006-01-02 15:04:05", notification.GmtPayment, time.Local)
|
||||
if parseErr != nil {
|
||||
h.logger.Error("支付宝回调:付款时间格式无效",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(parseErr),
|
||||
)
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "支付宝付款时间格式错误")
|
||||
}
|
||||
if err := h.confirmAgentRechargePayment(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: outTradeNo, PaymentMethod: model.PaymentByAlipay,
|
||||
TransactionID: notification.TradeNo, Amount: notifyAmountFen, ConfigID: cfg.ID,
|
||||
MerchantIdentity: notification.AppId, PaidAt: paidAt, Provider: constants.IntegrationProviderAlipay,
|
||||
RawPayload: c.Body(), ContentType: c.Get("Content-Type"),
|
||||
}); err != nil {
|
||||
h.logger.Error("支付宝回调:确认代理充值支付失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "处理支付宝代理充值回调失败")
|
||||
}
|
||||
|
||||
default:
|
||||
h.logger.Error("支付宝回调:未知支付记录类型",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
@@ -452,7 +568,11 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
|
||||
orderNo := notify.MchntOrderNo
|
||||
// OrderAmt 为字符串格式的分,解析失败则降级为 0
|
||||
orderAmt, _ := strconv.ParseInt(notify.OrderAmt, 10, 64)
|
||||
if handled, err := h.dispatchPaymentRecordCallback(ctx, orderNo, "fuiou", notify.TransactionId, orderAmt); err != nil {
|
||||
if handled, err := h.dispatchPaymentRecordCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: orderNo, PaymentMethod: "fuiou", TransactionID: notify.TransactionId, Amount: orderAmt,
|
||||
ConfigID: cfg.ID, MerchantIdentity: cfg.FyMchntCd, Provider: model.ProviderTypeFuiou,
|
||||
RawPayload: body, ContentType: c.Get("Content-Type"),
|
||||
}); err != nil {
|
||||
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
|
||||
} else if handled {
|
||||
return c.Send(fuiou.BuildNotifySuccessResponse())
|
||||
|
||||
Reference in New Issue
Block a user