Files
junhong_cmp_fiber/internal/handler/callback/payment.go

497 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package callback
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp/fasthttpadaptor"
"go.uber.org/zap"
"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"
rechargeOrderSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge_order"
"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"
"github.com/break/junhong_cmp_fiber/pkg/wechat"
"gorm.io/gorm"
)
// AgentRechargeServiceInterface 代理充值服务接口
type AgentRechargeServiceInterface interface {
HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error
}
// WechatConfigServiceInterface 支付配置服务接口
type WechatConfigServiceInterface interface {
GetActiveConfig(ctx context.Context) (*model.WechatConfig, error)
GetConfigForCallback(ctx context.Context, orderNo string) (*model.WechatConfig, error)
}
type PaymentHandler struct {
orderService *orderService.Service
rechargeService *rechargeService.Service
rechargeOrderService *rechargeOrderSvc.Service
agentRechargeService AgentRechargeServiceInterface
wechatPayment wechat.PaymentServiceInterface
wechatConfigService WechatConfigServiceInterface
paymentStore *postgres.PaymentStore
logger *zap.Logger
}
func NewPaymentHandler(
orderService *orderService.Service,
rechargeService *rechargeService.Service,
rechargeOrderService *rechargeOrderSvc.Service,
agentRechargeService AgentRechargeServiceInterface,
wechatPayment wechat.PaymentServiceInterface,
wechatConfigService WechatConfigServiceInterface,
paymentStore *postgres.PaymentStore,
logger *zap.Logger,
) *PaymentHandler {
return &PaymentHandler{
orderService: orderService,
rechargeService: rechargeService,
rechargeOrderService: rechargeOrderService,
agentRechargeService: agentRechargeService,
wechatPayment: wechatPayment,
wechatConfigService: wechatConfigService,
paymentStore: paymentStore,
logger: logger,
}
}
// WechatPayCallback 微信支付回调(带签名验证)
// POST /api/callback/wechat-pay
func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
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, "微信支付服务未配置")
}
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 h.wechatV2SuccessResponse(c)
}
// TotalFee 为字符串格式的分,解析失败则降级为 0后续 handlePaymentCallback 会记录日志)
totalFee, _ := strconv.ParseInt(result.TotalFee, 10, 64)
if err := h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID, totalFee); err != nil {
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
}
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 h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID, result.TotalAmount)
})
if err != nil {
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
}
default:
return errors.New(errors.CodeWechatCallbackInvalid, "不支持的支付渠道类型")
}
return h.wechatV2SuccessResponse(c)
}
func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, paymentNo, paymentMethod, transactionID string, paidAmount int64) (bool, error) {
if h.paymentStore != nil {
payment, err := h.paymentStore.GetByPaymentNo(ctx, paymentNo)
if err == nil {
switch payment.OrderType {
case model.PaymentOrderTypePackage:
return true, h.orderService.HandlePaymentRecordCallback(ctx, paymentNo, paymentMethod, transactionID, paidAmount)
case model.PaymentOrderTypeRecharge:
if h.rechargeOrderService != nil {
return true, h.rechargeOrderService.HandlePaymentCallback(ctx, paymentNo, paymentMethod, transactionID)
}
return true, fmt.Errorf("充值订单服务未配置,无法处理支付单: %s", paymentNo)
default:
return true, fmt.Errorf("未知支付记录类型: %s", payment.OrderType)
}
}
if err != gorm.ErrRecordNotFound {
return true, errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败")
}
}
return false, nil
}
// dispatchWechatCallback 优先按支付单分发,旧单号继续按前缀兼容处理。
func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, outTradeNo, transactionID string, paidAmount int64) error {
handled, err := h.dispatchPaymentRecordCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID, paidAmount)
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):
if h.rechargeOrderService != nil {
return h.rechargeOrderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentByWechat, transactionID)
}
return fmt.Errorf("充值订单服务未配置,无法处理订单: %s", outTradeNo)
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>`)
}
// AlipayCallback 支付宝异步回调(带验签与多维度安全校验)
// POST /api/callback/alipay
func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
ctx := c.UserContext()
// 从原始请求体解析 form 参数x-www-form-urlencoded
values, err := url.ParseQuery(string(c.Body()))
if err != nil {
h.logger.Error("支付宝回调:解析 form 参数失败", zap.Error(err))
return errors.New(errors.CodeInvalidParam, "回调数据格式错误")
}
// 未验签前只读取 out_trade_no用于回溯创建支付单时所用的配置
outTradeNo := values.Get("out_trade_no")
if outTradeNo == "" {
h.logger.Error("支付宝回调out_trade_no 为空")
return errors.New(errors.CodeInvalidParam, "订单号不能为空")
}
// 按 payment_config_id 加载创建支付单时所用的配置(支持已停用配置)
cfg, err := h.wechatConfigService.GetConfigForCallback(ctx, outTradeNo)
if err != nil || cfg == nil {
h.logger.Error("支付宝回调:加载支付配置失败",
zap.String("out_trade_no", outTradeNo),
zap.Error(err),
)
return errors.New(errors.CodeWechatCallbackInvalid, "支付配置不可用")
}
// 使用支付宝公钥验签DecodeNotification 内部完成签名校验)
notification, err := alipay.DecodeNotification(ctx, cfg, values)
if err != nil {
h.logger.Error("支付宝回调:验签失败",
zap.String("out_trade_no", outTradeNo),
zap.Uint("config_id", cfg.ID),
zap.Error(err),
)
return errors.New(errors.CodeWechatCallbackInvalid, "支付宝回调验签失败")
}
// 校验 app_id 与配置一致,防止跨商户混用
if notification.AppId != cfg.AliAppID {
h.logger.Error("支付宝回调app_id 不匹配",
zap.String("out_trade_no", outTradeNo),
zap.String("notify_app_id", notification.AppId),
zap.String("config_app_id", cfg.AliAppID),
)
return errors.New(errors.CodeWechatCallbackInvalid, "支付宝 app_id 校验失败")
}
// 非终态交易(如 WAIT_BUYER_PAY、TRADE_CLOSED直接返回 success不推进业务
tradeStatus := string(notification.TradeStatus)
if tradeStatus != "TRADE_SUCCESS" && tradeStatus != "TRADE_FINISHED" {
h.logger.Info("支付宝回调:非成功交易状态,忽略",
zap.String("out_trade_no", outTradeNo),
zap.String("trade_status", tradeStatus),
)
return c.SendString("success")
}
// 查询支付记录
payment, err := h.paymentStore.GetByPaymentNo(ctx, outTradeNo)
if err != nil {
if err == gorm.ErrRecordNotFound {
h.logger.Error("支付宝回调:支付记录不存在",
zap.String("out_trade_no", outTradeNo),
)
return errors.New(errors.CodeWechatCallbackInvalid, "支付记录不存在")
}
h.logger.Error("支付宝回调:查询支付记录失败",
zap.String("out_trade_no", outTradeNo),
zap.Error(err),
)
return errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败")
}
// 校验支付方式必须为 alipay防止其他通道的 payment_no 被误用
if payment.PaymentMethod != model.PaymentByAlipay {
h.logger.Error("支付宝回调:支付方式不匹配",
zap.String("out_trade_no", outTradeNo),
zap.String("payment_method", payment.PaymentMethod),
)
return errors.New(errors.CodeWechatCallbackInvalid, "支付通道校验失败")
}
// 校验 payment_config_id 与当前配置一致;旧数据为空时兼容并记录 warn
if 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 {
h.logger.Error("支付宝回调payment_config_id 不匹配",
zap.String("out_trade_no", outTradeNo),
zap.Uint("payment_config_id", *payment.PaymentConfigID),
zap.Uint("callback_config_id", cfg.ID),
)
return errors.New(errors.CodeWechatCallbackInvalid, "支付配置 ID 校验失败")
}
// 校验金额:使用 total_amount原始订单金额而非 buyer_pay_amount
// 避免优惠券/积分场景导致内部订单金额被错误记低
notifyAmountFen, err := alipay.YuanToFen(notification.TotalAmount)
if err != nil {
h.logger.Error("支付宝回调total_amount 转换失败",
zap.String("out_trade_no", outTradeNo),
zap.String("total_amount", notification.TotalAmount),
zap.Error(err),
)
return errors.New(errors.CodeWechatCallbackInvalid, "支付金额格式错误")
}
if notifyAmountFen != payment.Amount {
h.logger.Error("支付宝回调:金额不一致",
zap.String("out_trade_no", outTradeNo),
zap.Int64("notify_amount_fen", notifyAmountFen),
zap.Int64("payment_amount_fen", payment.Amount),
)
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),
)
// 不中断业务,继续分发(幂等业务层会处理状态)
}
// 按支付单 order_type 分发业务
switch payment.OrderType {
case model.PaymentOrderTypePackage:
if err := h.orderService.HandlePaymentRecordCallback(ctx, outTradeNo, model.PaymentByAlipay, notification.TradeNo, payment.Amount); err != nil {
h.logger.Error("支付宝回调:推进套餐订单失败",
zap.String("out_trade_no", outTradeNo),
zap.String("trade_no", notification.TradeNo),
zap.Error(err),
)
return errors.Wrap(errors.CodeInternalError, err, "处理支付宝支付回调失败")
}
h.logger.Info("支付宝回调:套餐订单支付成功",
zap.String("out_trade_no", outTradeNo),
zap.String("trade_no", notification.TradeNo),
zap.String("order_type", payment.OrderType),
)
case model.PaymentOrderTypeRecharge:
if h.rechargeOrderService == nil {
h.logger.Error("支付宝回调:充值订单服务未配置",
zap.String("out_trade_no", outTradeNo),
)
return errors.New(errors.CodeInternalError, "充值订单服务未配置")
}
if err := h.rechargeOrderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentByAlipay, notification.TradeNo); err != nil {
h.logger.Error("支付宝回调:推进充值订单失败",
zap.String("out_trade_no", outTradeNo),
zap.String("trade_no", notification.TradeNo),
zap.Error(err),
)
return errors.Wrap(errors.CodeInternalError, err, "处理支付宝充值回调失败")
}
h.logger.Info("支付宝回调:充值订单支付成功",
zap.String("out_trade_no", outTradeNo),
zap.String("trade_no", notification.TradeNo),
zap.String("order_type", payment.OrderType),
)
default:
h.logger.Error("支付宝回调:未知支付记录类型",
zap.String("out_trade_no", outTradeNo),
zap.String("order_type", payment.OrderType),
)
return errors.New(errors.CodeInternalError, "未知支付记录类型")
}
return c.SendString("success")
}
// FuiouPayCallback 富友支付回调(带签名验证)
// POST /api/callback/fuiou-pay
func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
body, bodySource := fuiouCallbackPayload(c)
if strings.TrimSpace(string(body)) == "" {
return errors.New(errors.CodeFuiouCallbackInvalid, "回调请求体为空")
}
ctx := c.UserContext()
c.Set("Content-Type", "text/plain; charset=utf-8")
// 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置
preNotify, err := fuiou.ParseNotify(body)
if err != nil {
h.logger.Error("富友回调:预解析失败",
zap.Error(err),
zap.String("payload_source", bodySource),
zap.Int("raw_body_len", len(c.Body())),
zap.Int("payload_len", len(body)),
zap.String("content_type", c.Get("Content-Type")),
zap.String("query", string(c.Request().URI().QueryString())),
)
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"))
}
if cfg.ProviderType != model.ProviderTypeFuiou ||
strings.TrimSpace(preNotify.InsCd) != strings.TrimSpace(cfg.FyInsCd) ||
strings.TrimSpace(preNotify.MchntCd) != strings.TrimSpace(cfg.FyMchntCd) {
h.logger.Error("富友回调:支付配置与回调商户不匹配",
zap.String("order_no", preNotify.MchntOrderNo),
zap.Uint("config_id", cfg.ID),
zap.String("config_name", cfg.Name),
zap.String("callback_ins_cd", preNotify.InsCd),
zap.String("config_ins_cd", cfg.FyInsCd),
zap.String("callback_mchnt_cd", preNotify.MchntCd),
zap.String("config_mchnt_cd", cfg.FyMchntCd),
zap.String("provider_type", cfg.ProviderType),
)
return c.Send(fuiou.BuildNotifyFailResponse("payment config mismatch"))
}
client, err := fuiou.NewClient(
cfg.FyInsCd,
cfg.FyMchntCd,
cfg.FyTermID,
cfg.FyAPIURL,
cfg.FyNotifyURL,
cfg.FyPrivateKey,
cfg.FyPublicKey,
h.logger,
)
if err != nil {
h.logger.Error("富友回调:构造客户端失败", zap.Error(err))
return c.Send(fuiou.BuildNotifyFailResponse("client init failed"))
}
notify, err := client.VerifyNotify(body)
if err != nil {
if notify != nil {
h.logger.Warn("富友回调:非成功结果",
zap.String("result_code", notify.ResultCode),
zap.String("result_msg", notify.ResultMsg))
return c.Send(fuiou.BuildNotifySuccessResponse())
}
h.logger.Error("富友回调:验签或解析失败",
zap.Error(err),
zap.String("order_no", preNotify.MchntOrderNo),
zap.Uint("config_id", cfg.ID),
zap.String("config_name", cfg.Name),
zap.String("callback_ins_cd", preNotify.InsCd),
zap.String("config_ins_cd", cfg.FyInsCd),
zap.String("callback_mchnt_cd", preNotify.MchntCd),
zap.String("config_mchnt_cd", cfg.FyMchntCd),
)
return c.Send(fuiou.BuildNotifyFailResponse(err.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 {
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
} else if handled {
return c.Send(fuiou.BuildNotifySuccessResponse())
}
switch {
case strings.HasPrefix(orderNo, "ORD"):
if err := h.orderService.HandlePaymentCallback(ctx, orderNo, "fuiou", orderAmt); err != nil {
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
}
case strings.HasPrefix(orderNo, constants.AssetRechargeOrderPrefix):
if h.rechargeOrderService != nil {
if err := h.rechargeOrderService.HandlePaymentCallback(ctx, orderNo, "fuiou", notify.TransactionId); err != nil {
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
}
return c.Send(fuiou.BuildNotifySuccessResponse())
}
return c.Send(fuiou.BuildNotifyFailResponse("充值订单服务未配置"))
case strings.HasPrefix(orderNo, constants.AgentRechargeOrderPrefix):
if h.agentRechargeService != nil {
if err := h.agentRechargeService.HandlePaymentCallback(ctx, orderNo, "fuiou", notify.TransactionId); err != nil {
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
}
}
default:
return c.Send(fuiou.BuildNotifyFailResponse("unknown order prefix"))
}
return c.Send(fuiou.BuildNotifySuccessResponse())
}
// fuiouCallbackPayload 提取富友回调载荷,兼容 body、form req 和 query req 三种来源。
func fuiouCallbackPayload(c *fiber.Ctx) ([]byte, string) {
if req := strings.TrimSpace(c.FormValue("req")); req != "" {
return []byte(req), "form:req"
}
if req := strings.TrimSpace(c.Query("req")); req != "" {
return []byte(req), "query:req"
}
return c.Body(), "body"
}