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

390 lines
14 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"
"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>`)
}
// 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)
handled, err := h.dispatchPaymentRecordCallback(ctx, req.OrderNo, model.PaymentMethodAlipay, "", paidAmount)
if err != nil {
return err
}
if handled {
return c.SendString("success")
}
// 按订单号前缀分发
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, 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"
}