Files
junhong_cmp_fiber/internal/handler/callback/payment.go
huang 0a2961ecd6
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s
fix: 支付回调补写 actual_paid_amount 字段,从支付平台回调中提取实付金额
- HandlePaymentCallback 新增 actualPaidAmount int64 参数,Updates 写入 actual_paid_amount
- 微信 V2 回调从 TotalFee 字符串解析分值;V3 直接使用 TotalAmount
- 支付宝回调优先取 buyer_pay_amount(实付),回退 total_amount,元转分使用 math.Round
- 富友回调从 OrderAmt 字符串解析分值
- dispatchWechatCallback 同步增加 paidAmount 参数透传
2026-04-17 11:12:35 +08:00

305 lines
11 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"
"math"
"net/http"
"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/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"
)
// 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)
}
// dispatchWechatCallback 按订单号前缀将支付结果分发到对应业务
func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, outTradeNo, transactionID string, paidAmount int64) error {
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>`)
}
// parseYuanToFen 将支付宝元为单位的金额字符串转换为分
// 支付宝回调金额字段buyer_pay_amount / total_amount为字符串格式的元如 "2.00"
// 使用 math.Round 避免浮点运算精度误差
func parseYuanToFen(yuan string) int64 {
if yuan == "" {
return 0
}
f, err := strconv.ParseFloat(yuan, 64)
if err != nil {
return 0
}
return int64(math.Round(f * 100))
}
type AlipayCallbackRequest struct {
OrderNo string `json:"out_trade_no" form:"out_trade_no"`
BuyerPayAmount string `json:"buyer_pay_amount" form:"buyer_pay_amount"`
TotalAmount string `json:"total_amount" form:"total_amount"`
}
// AlipayCallback 支付宝回调
// POST /api/callback/alipay
func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
var req AlipayCallbackRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if req.OrderNo == "" {
return errors.New(errors.CodeInvalidParam, "订单号不能为空")
}
ctx := c.UserContext()
// 优先取 buyer_pay_amount买家实付扣除优惠券后回退到 total_amount
rawAmount := req.BuyerPayAmount
if rawAmount == "" {
rawAmount = req.TotalAmount
}
paidAmount := parseYuanToFen(rawAmount)
// 按订单号前缀分发
switch {
case strings.HasPrefix(req.OrderNo, "ORD"):
if err := h.orderService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentMethodAlipay, paidAmount); err != nil {
return err
}
case strings.HasPrefix(req.OrderNo, constants.AssetRechargeOrderPrefix):
if h.rechargeOrderService != nil {
return h.rechargeOrderService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentByAlipay, "")
}
return fmt.Errorf("充值订单服务未配置,无法处理订单: %s", req.OrderNo)
case strings.HasPrefix(req.OrderNo, constants.AgentRechargeOrderPrefix):
if h.agentRechargeService != nil {
if err := h.agentRechargeService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentMethodAlipay, ""); err != nil {
return err
}
}
default:
return errors.New(errors.CodeInvalidParam, "未知订单号前缀")
}
return c.SendString("success")
}
// FuiouPayCallback 富友支付回调(带签名验证)
// POST /api/callback/fuiou-pay
func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
body := c.Body()
if len(body) == 0 {
return errors.New(errors.CodeFuiouCallbackInvalid, "回调请求体为空")
}
ctx := c.UserContext()
c.Set("Content-Type", "text/xml; charset=gbk")
// 预解析订单号(不验签),用于按 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(
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))
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
}
orderNo := notify.MchntOrderNo
// OrderAmt 为字符串格式的分,解析失败则降级为 0
orderAmt, _ := strconv.ParseInt(notify.OrderAmt, 10, 64)
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())
}