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

@@ -16,6 +16,7 @@ import (
"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/middleware"
"github.com/break/junhong_cmp_fiber/pkg/queue"
"github.com/break/junhong_cmp_fiber/pkg/wechat"
@@ -194,6 +195,14 @@ func (s *Service) CreateLegacy(ctx context.Context, req *dto.CreateOrderRequest,
} else if req.PaymentMethod == model.PaymentMethodWallet {
// ==== 场景 2代理钱包支付wallet====
if buyerType == "" {
if resourceShopID == nil {
return nil, errors.New(errors.CodeInvalidParam, "不支持的钱包支付场景")
}
buyerType = model.BuyerTypeAgent
buyerID = *resourceShopID
}
// 只有代理账号可以使用钱包支付
if buyerType != model.BuyerTypeAgent {
return nil, errors.New(errors.CodeInvalidParam, "只有代理账号可以使用钱包支付")
@@ -2379,14 +2388,126 @@ func (s *Service) GetPurchaseCheck(ctx context.Context, req *dto.PurchaseCheckRe
return response, nil
}
// FuiouPayJSAPI 富友公众号 JSAPI 支付发起(留桩)
// TODO: 实现富友支付发起逻辑
func (s *Service) FuiouPayJSAPI(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) error {
return errors.New(errors.CodeFuiouPayFailed, "富友支付发起暂未实现")
func (s *Service) FuiouPayJSAPI(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.FuiouPayJSAPIResponse, error) {
return s.fuiouPreCreate(ctx, orderID, openID, buyerType, buyerID, "JSAPI", false)
}
// FuiouPayMiniApp 富友小程序支付发起(留桩)
// TODO: 实现富友小程序支付发起逻辑
func (s *Service) FuiouPayMiniApp(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) error {
return errors.New(errors.CodeFuiouPayFailed, "富友小程序支付发起暂未实现")
func (s *Service) FuiouPayMiniApp(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.FuiouPayJSAPIResponse, error) {
return s.fuiouPreCreate(ctx, orderID, openID, buyerType, buyerID, "LETPAY", true)
}
func (s *Service) fuiouPreCreate(
ctx context.Context,
orderID uint,
openID string,
buyerType string,
buyerID uint,
tradeType string,
useMiniappAppID bool,
) (*dto.FuiouPayJSAPIResponse, error) {
if strings.TrimSpace(openID) == "" {
return nil, errors.New(errors.CodeInvalidParam, "openID 不能为空")
}
order, err := s.orderStore.GetByID(ctx, orderID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "订单不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单失败")
}
if order.BuyerType != buyerType || order.BuyerID != buyerID {
return nil, errors.New(errors.CodeForbidden, "无权操作此订单")
}
if order.PaymentStatus != model.PaymentStatusPending {
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
}
activeConfig, err := s.wechatConfigService.GetActiveConfig(ctx)
if err != nil {
return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "查询生效支付配置失败")
}
if activeConfig == nil || activeConfig.ProviderType != model.ProviderTypeFuiou {
return nil, errors.New(errors.CodeFuiouPayFailed, "未找到生效的富友支付配置")
}
subAppID := activeConfig.OaAppID
if useMiniappAppID {
subAppID = activeConfig.MiniappAppID
}
if strings.TrimSpace(activeConfig.FyInsCd) == "" ||
strings.TrimSpace(activeConfig.FyMchntCd) == "" ||
strings.TrimSpace(activeConfig.FyTermID) == "" ||
strings.TrimSpace(activeConfig.FyPrivateKey) == "" ||
strings.TrimSpace(activeConfig.FyPublicKey) == "" ||
strings.TrimSpace(activeConfig.FyAPIURL) == "" ||
strings.TrimSpace(activeConfig.FyNotifyURL) == "" ||
strings.TrimSpace(subAppID) == "" {
return nil, errors.New(errors.CodeFuiouPayFailed, "富友支付配置不完整")
}
client, err := fuiou.NewClient(
activeConfig.FyInsCd,
activeConfig.FyMchntCd,
activeConfig.FyTermID,
activeConfig.FyAPIURL,
activeConfig.FyNotifyURL,
activeConfig.FyPrivateKey,
activeConfig.FyPublicKey,
s.logger,
)
if err != nil {
return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "创建富友支付客户端失败")
}
items, err := s.orderItemStore.ListByOrderIDs(ctx, []uint{orderID})
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单明细失败")
}
description := "套餐购买"
if len(items) > 0 {
description = items[0].PackageName
}
termIP := "127.0.0.1"
if ip := middleware.GetIPFromContext(ctx); ip != nil && strings.TrimSpace(*ip) != "" {
termIP = *ip
}
resp, err := client.WxPreCreate(
order.OrderNo,
strconv.FormatInt(order.TotalAmount, 10),
description,
termIP,
tradeType,
subAppID,
openID,
)
if err != nil {
s.logger.Error("富友预下单失败",
zap.Uint("order_id", orderID),
zap.String("order_no", order.OrderNo),
zap.String("trade_type", tradeType),
zap.Error(err),
)
return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "富友预下单失败")
}
s.logger.Info("富友预下单成功",
zap.Uint("order_id", orderID),
zap.String("order_no", order.OrderNo),
zap.String("trade_type", tradeType),
)
return &dto.FuiouPayJSAPIResponse{
AppID: resp.SdkAppid,
TimeStamp: resp.SdkTimestamp,
NonceStr: resp.SdkNoncestr,
Package: resp.SdkPackage,
SignType: resp.SdkSigntype,
PaySign: resp.SdkPaysign,
}, nil
}