feat: 业务逻辑补全 — 佣金待审记录、C端订单重构、支付抽象、富友支付、卡设备状态联动
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled

F-1: 佣金链断裂时创建 status=99 零额待审记录,新增修正接口 POST /commission-records/:id/resolve
F-4: C端订单查询从 Handler 迁移至 Service 层,移除 SkipPermissionCtx
J-1: 富友支付 JSAPI/MiniApp 预下单实现,回调补全签名验证
J-2: 平台钱包代购支持(buyerType 为空时使用资产所属代理钱包)
J-3: 套餐激活后自动更新卡/设备 status=3,最后套餐过期后更新 status=4
支付抽象: 引入 PaymentProvider 接口 + 微信/富友适配器,CreateOrder 支持多支付渠道
修复: 设备/IoT卡响应 DTO 移除 omitempty,空值字段返回 null 而非省略
This commit is contained in:
2026-03-28 16:57:39 +08:00
parent 65e461eff7
commit 623a622298
26 changed files with 970 additions and 447 deletions

View File

@@ -2,14 +2,13 @@ package callback
import (
"context"
"encoding/xml"
"fmt"
"net/http"
"net/url"
"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"
@@ -26,11 +25,18 @@ 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)
}
type PaymentHandler struct {
orderService *orderService.Service
rechargeService *rechargeService.Service
agentRechargeService AgentRechargeServiceInterface
wechatPayment wechat.PaymentServiceInterface
wechatConfigService WechatConfigServiceInterface
logger *zap.Logger
}
func NewPaymentHandler(
@@ -38,12 +44,16 @@ func NewPaymentHandler(
rechargeService *rechargeService.Service,
agentRechargeService AgentRechargeServiceInterface,
wechatPayment wechat.PaymentServiceInterface,
wechatConfigService WechatConfigServiceInterface,
logger *zap.Logger,
) *PaymentHandler {
return &PaymentHandler{
orderService: orderService,
rechargeService: rechargeService,
agentRechargeService: agentRechargeService,
wechatPayment: wechatPayment,
wechatConfigService: wechatConfigService,
logger: logger,
}
}
@@ -130,7 +140,7 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
return c.SendString("success")
}
// FuiouPayCallback 富友支付回调
// FuiouPayCallback 富友支付回调(带签名验证)
// POST /api/callback/fuiou-pay
func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
body := c.Body()
@@ -139,63 +149,60 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
}
ctx := c.UserContext()
c.Set("Content-Type", "text/xml; charset=gbk")
// TODO: 按 payment_config_id 加载配置创建 fuiou.Client 验签
// 当前留桩:解析但不验签
// 解析 req= 参数
formValue := string(body)
if strings.HasPrefix(formValue, "req=") {
formValue = formValue[4:]
activeConfig, err := h.wechatConfigService.GetActiveConfig(ctx)
if err != nil || activeConfig == nil {
h.logger.Error("富友回调:加载支付配置失败", zap.Error(err))
return c.Send(fuiou.BuildNotifyFailResponse("payment config unavailable"))
}
decoded, err := url.QueryUnescape(formValue)
client, err := fuiou.NewClient(
activeConfig.FyInsCd,
activeConfig.FyMchntCd,
activeConfig.FyTermID,
activeConfig.FyAPIURL,
activeConfig.FyNotifyURL,
activeConfig.FyPrivateKey,
activeConfig.FyPublicKey,
h.logger,
)
if err != nil {
return errors.New(errors.CodeFuiouCallbackInvalid, "回调数据解码失败")
h.logger.Error("富友回调:构造客户端失败", zap.Error(err))
return c.Send(fuiou.BuildNotifyFailResponse("client init failed"))
}
utf8Data, err := fuiou.GBKToUTF8([]byte(decoded))
notify, err := client.VerifyNotify(body)
if err != nil {
return errors.New(errors.CodeFuiouCallbackInvalid, "GBK 转 UTF-8 失败")
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()))
}
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())
}