Files
junhong_cmp_fiber/internal/handler/callback/payment.go
huang 7c64e433e8 feat: 改造支付回调 Handler,支持富友回调和多订单类型按前缀分发
- payment.go: WechatPayCallback 改造为按订单号前缀分发(ORD→套餐订单、CRCH→资产充值、ARCH→代理充值);新增 FuiouPayCallback(GBK→UTF-8+XML解析+验签+分发);修复 RechargeOrderPrefix 废弃引用
- order.go: 注册 POST /api/callback/fuiou-pay 路由(无需认证)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-16 23:30:17 +08:00

202 lines
6.7 KiB
Go

package callback
import (
"context"
"encoding/xml"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp/fasthttpadaptor"
"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"
"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/response"
"github.com/break/junhong_cmp_fiber/pkg/wechat"
)
// AgentRechargeServiceInterface 代理充值服务接口
type AgentRechargeServiceInterface interface {
HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error
}
type PaymentHandler struct {
orderService *orderService.Service
rechargeService *rechargeService.Service
agentRechargeService AgentRechargeServiceInterface
wechatPayment wechat.PaymentServiceInterface
}
func NewPaymentHandler(
orderService *orderService.Service,
rechargeService *rechargeService.Service,
agentRechargeService AgentRechargeServiceInterface,
wechatPayment wechat.PaymentServiceInterface,
) *PaymentHandler {
return &PaymentHandler{
orderService: orderService,
rechargeService: rechargeService,
agentRechargeService: agentRechargeService,
wechatPayment: wechatPayment,
}
}
// WechatPayCallback 微信支付回调(带签名验证)
// POST /api/callback/wechat-pay
func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
if h.wechatPayment == nil {
return errors.New(errors.CodeWechatCallbackInvalid, "微信支付服务未配置")
}
var httpReq http.Request
fasthttpadaptor.ConvertRequest(c.Context(), &httpReq, true)
ctx := context.Background()
_, err := h.wechatPayment.HandlePaymentNotify(&httpReq, func(result *wechat.PaymentNotifyResult) error {
if result.TradeState != "SUCCESS" {
return nil
}
// TODO: 按 payment_config_id 加载配置验签(当前留桩,仍用 wechatPayment 单例验签)
// 按订单号前缀分发
outTradeNo := result.OutTradeNo
switch {
case strings.HasPrefix(outTradeNo, "ORD"):
return h.orderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat)
case strings.HasPrefix(outTradeNo, constants.AssetRechargeOrderPrefix):
return h.rechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, result.TransactionID)
case strings.HasPrefix(outTradeNo, constants.AgentRechargeOrderPrefix):
if h.agentRechargeService != nil {
return h.agentRechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, result.TransactionID)
}
return fmt.Errorf("代理充值服务未配置,无法处理订单: %s", outTradeNo)
default:
return fmt.Errorf("未知订单号前缀: %s", outTradeNo)
}
})
if err != nil {
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
}
return response.Success(c, map[string]string{"return_code": "SUCCESS"})
}
type AlipayCallbackRequest struct {
OrderNo string `json:"out_trade_no" form:"out_trade_no"`
}
// 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()
// 按订单号前缀分发
switch {
case strings.HasPrefix(req.OrderNo, "ORD"):
if err := h.orderService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentMethodAlipay); err != nil {
return err
}
case strings.HasPrefix(req.OrderNo, constants.AssetRechargeOrderPrefix):
if err := h.rechargeService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentMethodAlipay, ""); err != nil {
return err
}
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()
// TODO: 按 payment_config_id 加载配置创建 fuiou.Client 验签
// 当前留桩:解析但不验签
// 解析 req= 参数
formValue := string(body)
if strings.HasPrefix(formValue, "req=") {
formValue = formValue[4:]
}
decoded, err := url.QueryUnescape(formValue)
if err != nil {
return errors.New(errors.CodeFuiouCallbackInvalid, "回调数据解码失败")
}
utf8Data, err := fuiou.GBKToUTF8([]byte(decoded))
if err != nil {
return errors.New(errors.CodeFuiouCallbackInvalid, "GBK 转 UTF-8 失败")
}
xmlStr := strings.Replace(string(utf8Data), `encoding="GBK"`, `encoding="UTF-8"`, 1)
var notify fuiou.NotifyRequest
if err := xml.Unmarshal([]byte(xmlStr), &notify); err != nil {
return errors.New(errors.CodeFuiouCallbackInvalid, "解析回调 XML 失败")
}
if notify.ResultCode != "000000" {
c.Set("Content-Type", "text/xml; charset=gbk")
return c.Send(fuiou.BuildNotifySuccessResponse())
}
// 按订单号前缀分发
orderNo := notify.MchntOrderNo
switch {
case strings.HasPrefix(orderNo, "ORD"):
if err := h.orderService.HandlePaymentCallback(ctx, orderNo, "fuiou"); err != nil {
c.Set("Content-Type", "text/xml; charset=gbk")
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
}
case strings.HasPrefix(orderNo, constants.AssetRechargeOrderPrefix):
if err := h.rechargeService.HandlePaymentCallback(ctx, orderNo, "fuiou", notify.TransactionId); err != nil {
c.Set("Content-Type", "text/xml; charset=gbk")
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
}
case strings.HasPrefix(orderNo, constants.AgentRechargeOrderPrefix):
if h.agentRechargeService != nil {
if err := h.agentRechargeService.HandlePaymentCallback(ctx, orderNo, "fuiou", notify.TransactionId); err != nil {
c.Set("Content-Type", "text/xml; charset=gbk")
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
}
}
default:
c.Set("Content-Type", "text/xml; charset=gbk")
return c.Send(fuiou.BuildNotifyFailResponse("unknown order prefix"))
}
c.Set("Content-Type", "text/xml; charset=gbk")
return c.Send(fuiou.BuildNotifySuccessResponse())
}