Files
huang d81bd242a4
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m19s
fix(force-recharge): 补充强充配置缺失的接口和数据库字段
- 订单管理:增加 payment_method 字段支持,合并代购订单逻辑
- 套餐系列分配:增加强充配置字段(enable_force_recharge、force_recharge_amount、force_recharge_trigger_type)
- 数据库迁移:添加 force_recharge_trigger_type 字段
- 测试:更新订单服务测试用例
- OpenSpec:归档 fix-force-recharge-missing-interfaces 变更
2026-01-31 15:34:32 +08:00

10 KiB
Raw Permalink Blame History

Context

add-force-recharge-system 功能归档后,发现两个关键遗漏:

  1. 套餐系列分配强充配置不可管理

    • 数据库字段已添加:enable_force_rechargeforce_recharge_amountforce_recharge_trigger_type
    • Service 层已使用这些字段进行强充验证(RechargeService.GetRechargeCheckOrderService.GetPurchaseCheck
    • 但 DTO 层完全没有暴露这些字段,管理员无法通过 API 配置
  2. 后台订单创建逻辑重复

    • 存在两套 DTOCreateOrderRequestCreatePurchaseOnBehalfRequest(字段完全相同)
    • 存在两个 Service 方法:CreateCreatePurchaseOnBehalf(核心逻辑相似,仅支付方式和成本价计算不同)
    • 实际业务中,后台订单只有两种支付方式:
      • wallet:扣代理钱包,需要强充验证,触发佣金
      • offline:线下已收款,不扣钱包,不触发佣金(即代购)

现有架构

  • 强充验证逻辑完整:RechargeServiceOrderService 已实现
  • 数据模型完整:ShopSeriesAllocation.enable_force_recharge 等字段已存在
  • 仅缺少管理接口暴露

Goals / Non-Goals

Goals:

  • 暴露强充配置字段到套餐系列分配的 CRUD 接口
  • 统一后台订单创建接口,使用 payment_method 字段区分普通订单和代购订单
  • 删除重复代码和冗余 DTO
  • 保持现有业务逻辑不变(强充验证、佣金计算、成本价计算)

Non-Goals:

  • 不修改强充验证逻辑(已在 add-force-recharge-system 中实现)
  • 不修改数据库结构(字段已存在)
  • 不修改 H5 订单接口H5 仅支持微信/支付宝支付,不涉及 offline
  • 不修改佣金计算逻辑(CommissionCalculationService 已正确处理 is_purchase_on_behalf

Decisions

决策 1强充配置字段作为可选字段暴露

决策:在套餐系列分配的 DTO 中增加强充配置字段,作为可选字段(omitempty

理由

  • 强充是累计充值强充的可选配置enable_force_recharge 默认 false
  • 与一次性佣金配置保持一致(也是可选配置)
  • 向后兼容:现有数据默认值为 false/0不影响现有逻辑

实现

// CreateShopSeriesAllocationRequest
type CreateShopSeriesAllocationRequest struct {
    // ... 现有字段
    EnableForceRecharge      *bool  `json:"enable_force_recharge" description:"是否启用强充(累计充值强充)"`
    ForceRechargeAmount      *int64 `json:"force_recharge_amount" description:"强充金额(分,0表示使用阈值金额)"`
    ForceRechargeTriggerType *int   `json:"force_recharge_trigger_type" description:"强充触发类型(1:单次充值, 2:累计充值)"`
}

// UpdateShopSeriesAllocationRequest (同上)

// ShopSeriesAllocationResponse
type ShopSeriesAllocationResponse struct {
    // ... 现有字段
    EnableForceRecharge      bool  `json:"enable_force_recharge" description:"是否启用强充"`
    ForceRechargeAmount      int64 `json:"force_recharge_amount" description:"强充金额(分)"`
    ForceRechargeTriggerType int   `json:"force_recharge_trigger_type" description:"强充触发类型"`
}

替代方案

  • 独立接口配置强充:增加接口复杂度,与现有设计不一致
  • 强制字段:破坏向后兼容性,现有创建请求会失败

决策 2使用 payment_method 字段统一订单创建

决策:在 CreateOrderRequest 增加 payment_method 必填字段,删除 CreatePurchaseOnBehalfRequest

理由

  • 后台订单本质上只有两种支付方式:walletoffline
  • is_purchase_on_behalf 是业务标识,应由系统自动设置,不应由前端传递
  • 减少 DTO 冗余,统一接口设计

映射关系

payment_method = "wallet"  → is_purchase_on_behalf = false (普通订单)
payment_method = "offline" → is_purchase_on_behalf = true  (代购订单)

实现

type CreateOrderRequest struct {
    OrderType     string `json:"order_type" validate:"required,oneof=single_card device"`
    IotCardID     *uint  `json:"iot_card_id" validate:"required_if=OrderType single_card"`
    DeviceID      *uint  `json:"device_id" validate:"required_if=OrderType device"`
    PackageIDs    []uint `json:"package_ids" validate:"required,min=1,max=10"`
    PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet offline"` // 新增
}

替代方案

  • 保留两个 DTO代码重复维护成本高
  • 使用 is_purchase_on_behalf 字段:业务标识不应由前端控制,存在安全风险

决策 3Service 层合并逻辑但保留代码分支

决策:合并 Service.CreateService.CreatePurchaseOnBehalf 为统一方法,内部使用 if/else 分支处理

理由

  • 两个方法核心流程相似(验证 → 计算价格 → 创建订单 → 激活套餐 → 触发佣金)
  • 关键差异仅在:
    • 成本价计算:普通订单用卖家成本价,代购用买家成本价
    • 支付状态:普通订单待支付,代购直接已支付
    • 佣金触发:通过 is_purchase_on_behalf 标识控制
  • 统一方法减少接口暴露,降低复杂度

实现结构

func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, userType string, shopID, userID uint) (*dto.OrderResponse, error) {
    // 1. 通用验证(资源存在、套餐有效)
    validationResult, err := s.validatePurchase(ctx, req)
    
    // 2. 根据 payment_method 分支处理
    var order *model.Order
    if req.PaymentMethod == model.PaymentMethodOffline {
        // 代购逻辑
        order = s.buildPurchaseOnBehalfOrder(ctx, req, validationResult, userID)
    } else {
        // 普通逻辑
        order = s.buildNormalOrder(ctx, req, validationResult, shopID, userID)
    }
    
    // 3. 通用创建流程(保存订单 → 激活套餐 → 触发佣金)
    return s.createOrderCommon(ctx, order, validationResult)
}

替代方案

  • 保留两个独立方法Handler 需要路由逻辑,接口复杂度高
  • 完全合并为一个线性方法:可读性差,分支逻辑混乱

决策 4Handler 层权限验证前置

决策:在 OrderHandler.Create 中根据 payment_method 进行权限验证,再调用 Service

理由

  • 权限验证是 Handler 层职责
  • 提前拦截非法请求,避免无效的 Service 调用
  • 明确业务规则offline 仅平台可用wallet 代理和平台都可用

实现

func (h *OrderHandler) Create(c *fiber.Ctx) error {
    var req dto.CreateOrderRequest
    // ... 解析请求
    
    ctx := c.UserContext()
    userType := middleware.GetUserTypeFromContext(ctx)
    shopID := middleware.GetShopIDFromContext(ctx)
    userID := middleware.GetUserIDFromContext(ctx)
    
    // 权限验证
    if req.PaymentMethod == model.PaymentMethodOffline {
        if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
            return errors.New(errors.CodeForbidden, "只有平台可以使用线下支付")
        }
    } else if req.PaymentMethod == model.PaymentMethodWallet {
        if userType != constants.UserTypeAgent && 
           userType != constants.UserTypePlatform && 
           userType != constants.UserTypeSuperAdmin {
            return errors.New(errors.CodeForbidden, "无权创建订单")
        }
    }
    
    // 调用统一方法
    order, err := h.service.Create(ctx, &req, userType, shopID, userID)
    return response.Success(c, order)
}

替代方案

  • Service 层验证违反分层原则Handler 应负责权限
  • 中间件验证:无法访问请求体字段

Risks / Trade-offs

风险 1CreateOrderRequest 字段变更可能影响前端

风险:新增 payment_method 必填字段后,现有前端调用会失败

缓解措施

  • 这是后台管理接口,前端由团队控制,可同步修改
  • 如需兼容可设置默认值wallet但不推荐隐式行为会引起混淆

推荐:要求前端同步修改,明确传递 payment_method

风险 2删除 CreatePurchaseOnBehalfRequest 可能影响现有调用

风险:如果其他代码引用了 CreatePurchaseOnBehalfRequest DTO会编译失败

缓解措施

  • 通过 grep 搜索确认无引用(仅在 Service 测试中使用)
  • 修改测试用例,使用 CreateOrderRequest 替代

验证步骤

grep -r "CreatePurchaseOnBehalfRequest" internal/

风险 3Service 方法签名变更可能影响现有调用

风险Service.Create 方法签名变更(增加 userTypeuserID 参数),现有调用方可能失败

缓解措施

  • 通过 LSP 查找所有调用方
  • 仅有 OrderHandler.Create 和测试用例调用,影响范围可控

影响范围

  • internal/handler/admin/order.go
  • internal/handler/h5/order.goH5 订单不使用 offline不受影响
  • internal/service/order/service_test.go

权衡 4合并方法增加了单个方法的复杂度

权衡Service.Create 方法内部有 if/else 分支,复杂度略微增加

接受理由

  • 两个独立方法的维护成本更高(重复代码、接口复杂)
  • 内部分支清晰,使用辅助方法拆分逻辑(buildPurchaseOnBehalfOrderbuildNormalOrder
  • 测试覆盖两种分支场景,确保正确性

Migration Plan

部署步骤

  1. 代码变更

    • 修改 DTO3 个文件)
    • 修改 Service2 个文件)
    • 修改 Handler1 个文件)
    • 修改测试用例2 个文件)
  2. 测试验证

    • 运行单元测试确保所有测试通过
    • 运行 lsp_diagnostics 检查类型错误
    • 本地验证接口功能
  3. 部署

    • 无数据库迁移,直接部署即可
    • 通知前端团队同步修改 POST /api/admin/orders 调用
  4. 验证

    • 测试套餐系列分配的创建/更新/查询,确认强充配置正常显示
    • 测试后台订单创建wallet 和 offline确认业务逻辑正确

回滚策略

如果发现问题,可以:

  1. 回滚代码到上一版本Git revert
  2. 无数据库变更,回滚无风险
  3. 强充配置字段可选,即使旧代码未传递也不会出错

兼容性保障

  • 强充配置字段:可选字段,默认值 false/0现有数据不受影响
  • 订单创建接口payment_method 必填,需前端同步修改(可控)
  • 删除的 DTO 和方法:仅内部使用,无外部依赖

Open Questions

无待解决问题。