390 lines
14 KiB
Go
390 lines
14 KiB
Go
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"
|
||
}
|