Files
junhong_cmp_fiber/docs/fix-agent-wallet-order-creation/功能总结.md
huang 5bb0ff0ddf
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
fix: 修复代理钱包订单创建逻辑,拆分后台/H5端下单方法并归档变更
- 拆分订单创建为 CreateAdminOrder(后台一步支付)和 CreateH5Order(H5 两步支付)
- 新增 CreateAdminOrderRequest DTO,后台仅允许 wallet/offline 支付方式
- 同步 delta specs 到主规格(order-payment 更新 + admin-order-creation 新增)
- 归档 fix-agent-wallet-order-creation 变更
- 新增 implement-order-expiration 变更提案
2026-02-28 16:31:31 +08:00

15 KiB
Raw Blame History

代理钱包订单创建功能总结

概述

fix-agent-wallet-order-creation 提案修复了代理在后台使用钱包支付创建订单的问题,实现了代理钱包一步购买(扣款 + 激活)、代理代购、订单角色追踪等核心功能。

<EFBFBD><EFBFBD>景问题

问题描述

代理在后台使用钱包支付wallet创建订单时系统只创建待支付订单payment_status = 1),不扣款也不激活套餐,导致订单无法完成。后台没有支付接口,代理无法对待支付订单进行支付。

业务场景

  • 代理自购:代理为自己的卡/设备购买套餐,从自己钱包扣自己的成本价
  • 代理代购:代理为下级代理的卡/设备购买套餐,从自己钱包扣自己的成本价,但订单金额显示下级成本价
  • 平台代购(现有逻辑):平台使用 offline 支付为代理创建订单,不扣款,立即激活,产生佣金

核心功能

1. 订单角色追踪

新增字段tb_order 表):

  • operator_id (INT, 可空):操作者 ID谁下的单
  • operator_type (VARCHAR, 可空):操作者类型(platform / agent
  • actual_paid_amount (BIGINT, 可空):实际支付金额(分)
  • purchase_role (VARCHAR):订单角色枚举

订单角色枚举internal/model/order.go

const (
    PurchaseRoleSelfPurchase              = "self_purchase"               // 自己购买
    PurchaseRolePurchasedByParent         = "purchased_by_parent"         // 上级代理购买
    PurchaseRolePurchasedByPlatform       = "purchased_by_platform"       // 平台代购
    PurchaseRolePurchaseForSubordinate    = "purchase_for_subordinate"    // 给下级购买
)

索引

  • idx_orders_operator_id (operator_id):支持"我作为操作者的订单"查询
  • idx_orders_purchase_role (purchase_role):支持按角色筛选

2. 后台钱包一步支付

行为变更

  • 原逻辑:后台 wallet 订单 → 创建待支付订单(payment_status = 1)→ 无法支付
  • 新逻辑:后台 wallet 订单 → 立即扣款 + 激活套餐 → 订单已支付(payment_status = 2

区别于 H5 端

  • H5 端 wallet 订单仍使用两步流程:创建待支付订单 → 调用 WalletPay 接口支付
  • 后台 wallet 订单一步完成,无需后续支付接口

权限调整

  • 允许代理、平台、超管使用 wallet 支付方式
  • offline 支付方式仍限制为平台和超管

3. 价格计算逻辑

区分"订单金额"和"实际支付"

场景 订单金额total_amount 实际支付actual_paid_amount 说明
代理自购 操作者成本价 操作者成本价 两者相同
代理代购 买家成本价 操作者成本价 操作者实际扣款少于订单金额(赚取差价)
平台代购 买家成本价 NULL 平台不扣款

示例

一级代理 A 成本价80 元
二级代理 B 成本价100 元

A 为 B 的卡购买套餐:
- total_amount = 10000100 元B 看到的订单金额)
- actual_paid_amount = 800080 元A 实际扣款)
- A 赚取差价20 元

成本价查询 通过 ShopPackageAllocation 表查询店铺对套餐的成本价。


4. 钱包流水记录扩展

新增字段tb_agent_wallet_transaction 表):

  • transaction_subtype (VARCHAR):交易子类型(细分 order_payment 场景)
  • related_shop_id (INT, 可空):关联店铺 ID代购时记录下级店铺

交易子类型枚举pkg/constants/wallet.go

const (
    WalletTransactionSubtypeSelfPurchase          = "self_purchase"
    WalletTransactionSubtypePurchaseForSubordinate = "purchase_for_subordinate"
)

流水示例

  • 自购transaction_subtype = "self_purchase"remark = "购买套餐"
  • 代购transaction_subtype = "purchase_for_subordinate"related_shop_id = 下级店铺 IDremark = "为下级代理【XX】购买套餐"

5. 订单查询增强

OR 查询逻辑OrderStore.List()

WHERE (buyer_type = 'agent' AND buyer_id = ?) OR operator_id = ?

代理可以看到两类订单:

  1. 作为买家的订单(buyer_id = 自己):别人为自己代购、自己购买
  2. 作为操作者的订单(operator_id = 自己):自己为下级代购

新增查询参数

  • purchase_role可选筛选订单角色类型self_purchase / purchased_by_parent / purchased_by_platform / purchase_for_subordinate

6. 佣金逻辑调整

规则

  • 代理代购:操作者已赚取成本价差(自己成本价 vs 下级成本价),不产生佣金
  • 平台代购:平台不扣款,按买家成本价计算差价佣金,激励上级代理

实现

// 只有平台代购operator_id == nil才入队佣金计算
if order.OperatorID == nil {
    s.enqueueCommissionCalculation(ctx, order.ID)
}

7. 幂等性和并发控制

乐观锁(钱包扣款):

result := tx.Model(&model.AgentWallet{}).
    Where("id = ? AND balance >= ? AND version = ?", walletID, amount, version).
    Updates(map[string]any{
        "balance": gorm.Expr("balance - ?", amount),
        "version": gorm.Expr("version + 1"),
    })

幂等性检查(订单创建):

  • 使用 Redis 业务键:order:idempotency:{buyer_type}:{buyer_id}:{order_type}:{carrier_type}:{carrier_id}:{sorted_package_ids}
  • TTL3 分钟
  • 分布式锁防止并发:order:create:lock:{carrier_type}:{carrier_id}

API 变更

后台订单创建 API Breaking Change

端点POST /api/admin/orders

请求参数变更

字段 变更前 变更后 说明
payment_method 可选,任意值 必填,仅允许 walletoffline 不传或传其他值均返回 1001 错误

行为变更

  • wallet 支付:订单直接完成(payment_status = 2),无需后续支付接口
  • offline 支付:逻辑保持不变
  • 传入 wechat/alipay → 返回 {"code": 1001, "msg": "请求参数解析失败"}

响应新增字段

{
  "operator_id": 123,
  "operator_type": "agent",
  "operator_name": "一级代理 A",
  "actual_paid_amount": 8000,
  "purchase_role": "purchase_for_subordinate",
  "is_purchased_by_parent": false,
  "purchase_remark": "为下级代理【二级代理 B】购买"
}

H5 端订单创建 API无变更

端点POST /api/h5/orders

行为完全不变,仍支持 wallet/wechat/alipay,仍创建待支付订单。

订单列表 API

端点GET /api/admin/orders

新增查询参数

  • purchase_role (可选):订单角色筛选
    • self_purchase:自己购买
    • purchased_by_parent:上级代理购买
    • purchased_by_platform:平台代购
    • purchase_for_subordinate:给下级购买

查询逻辑变更

  • 代理可以看到 buyer_id = 自己operator_id = 自己 的所有订单

数据库变更

订单表tb_order

新增字段

ALTER TABLE tb_order ADD COLUMN operator_id INT;
ALTER TABLE tb_order ADD COLUMN operator_type VARCHAR(20);
ALTER TABLE tb_order ADD COLUMN actual_paid_amount BIGINT;
ALTER TABLE tb_order ADD COLUMN purchase_role VARCHAR(50);

COMMENT ON COLUMN tb_order.operator_id IS '操作者ID谁下的单';
COMMENT ON COLUMN tb_order.operator_type IS '操作者类型platform/agent';
COMMENT ON COLUMN tb_order.actual_paid_amount IS '实际支付金额(分)';
COMMENT ON COLUMN tb_order.purchase_role IS '订单角色self_purchase/purchased_by_parent/purchased_by_platform/purchase_for_subordinate';

新增索引

CREATE INDEX CONCURRENTLY idx_orders_operator_id ON tb_order(operator_id);
CREATE INDEX CONCURRENTLY idx_orders_purchase_role ON tb_order(purchase_role);

钱包流水表tb_agent_wallet_transaction

新增字段(如果不存在):

ALTER TABLE tb_agent_wallet_transaction ADD COLUMN transaction_subtype VARCHAR(50);
ALTER TABLE tb_agent_wallet_transaction ADD COLUMN related_shop_id INT;

COMMENT ON COLUMN tb_agent_wallet_transaction.transaction_subtype IS '交易子类型(细分 order_payment 场景)';
COMMENT ON COLUMN tb_agent_wallet_transaction.related_shop_id IS '关联店铺ID代购时记录下级店铺';

代码结构

Service 层新增方法

internal/service/order/service.go

  1. getCostPrice(ctx, shopID, packageID) (int64, error)

    • 查询店铺对套餐的成本价(通过 ShopPackageAllocation
  2. createWalletTransaction(ctx, tx, walletID, orderID, amount, purchaseRole, relatedShopID) error

    • 创建钱包流水,根据 purchaseRole 填充 subtype 和 remark
  3. createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID) (*dto.OrderResponse, error)

    • 钱包支付订单创建方法,事务内完成:订单创建 + 扣款 + 流水 + 激活套餐

Create() 方法重构

// 场景判断
if req.PaymentMethod == "offline":
    // 平台代购场景(保持现有逻辑)
    return s.createOrderWithActivation(...)
else if req.PaymentMethod == "wallet":
    // 获取资源所属店铺 ID
    if 资源属于操作者:
        // 代理自购场景
        buyer = operator
        purchase_role = "self_purchase"
        total_amount = actual_paid_amount = 操作者成本价
    else:
        // 代理代购场景
        buyer = 资源所属者
        operator = 操作者
        purchase_role = "purchase_for_subordinate"
        total_amount = 买家成本价
        actual_paid_amount = 操作者成本价

    return s.createOrderWithWalletPayment(...)

Store 层变更

internal/store/postgres/order_store.go

List() 方法

// 代理用户:查询作为买家或操作者的订单
if shopID, ok := filters["shop_id"].(uint); ok {
    query = query.Where(
        "(buyer_type = ? AND buyer_id = ?) OR operator_id = ?",
        model.BuyerTypeAgent, shopID, shopID,
    )
}

// 支持 purchase_role 精确匹配筛选
if purchaseRole, ok := filters["purchase_role"].(string); ok {
    query = query.Where("purchase_role = ?", purchaseRole)
}

Handler 层变更

internal/handler/admin/order.go

Create() 方法

  • 修改 wallet 支付方式的权限检查,允许代理、平台、超管使用
  • offline 支付方式仍限制为平台和超管

List() 方法

  • 从查询参数解析 purchase_role
  • 传递给 Service 层的 List() 方法

使用指南

代理自购场景

请求

POST /api/admin/orders
Authorization: Bearer {agent_token}
Content-Type: application/json

{
  "order_type": 1,
  "iot_card_id": 101,
  "package_ids": [201],
  "payment_method": "wallet"
}

响应

{
  "code": 0,
  "data": {
    "id": 1001,
    "order_no": "ORD202602281234567890",
    "payment_status": 2,
    "operator_id": 10,
    "buyer_id": 10,
    "operator_type": "agent",
    "purchase_role": "self_purchase",
    "total_amount": 8000,
    "actual_paid_amount": 8000
  },
  "msg": "订单创建成功"
}

代理代购场景

请求

POST /api/admin/orders
Authorization: Bearer {parent_agent_token}
Content-Type: application/json

{
  "order_type": 1,
  "iot_card_id": 201,
  "package_ids": [301],
  "payment_method": "wallet"
}

响应

{
  "code": 0,
  "data": {
    "id": 1002,
    "order_no": "ORD202602281234567891",
    "payment_status": 2,
    "operator_id": 10,
    "buyer_id": 20,
    "operator_type": "agent",
    "operator_name": "一级代理 A",
    "purchase_role": "purchase_for_subordinate",
    "total_amount": 10000,
    "actual_paid_amount": 8000,
    "purchase_remark": "为下级代理【二级代理 B】购买"
  },
  "msg": "订单创建成功"
}

订单列表查询

请求

GET /api/admin/orders?purchase_role=purchase_for_subordinate&page=1&page_size=20
Authorization: Bearer {agent_token}

响应

{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1002,
        "purchase_role": "purchase_for_subordinate",
        "operator_id": 10,
        "buyer_id": 20,
        "total_amount": 10000,
        "actual_paid_amount": 8000
      }
    ],
    "total": 1
  },
  "msg": "success"
}

迁移和部署

数据库迁移

迁移脚本

  • migrations/000067_add_operator_fields_to_orders.up.sql
  • migrations/000068_add_transaction_subtype_to_wallet_transaction.up.sql

回滚脚本

  • migrations/000067_add_operator_fields_to_orders.down.sql
  • migrations/000068_add_transaction_subtype_to_wallet_transaction.down.sql

数据回填(可选):

  • migrations/backfill_order_purchase_role.sql:回填历史平台代购订单

部署步骤

  1. 测试环境验证

    • 执行迁移脚本
    • 验证索引创建成功
    • 手工测试三种代购场景
  2. 灰度发布

    • 代码部署到灰度环境
    • 观察日志和监控指标
    • 验证订单创建、查询、钱包扣款功能
  3. 生产环境部署

    • 低峰期执行数据库迁移
    • 部署代码
    • 监控错误日志和业务指标
    • 验证核心功能

监控指标

关键指标

  • 订单创建成功率(按 payment_method 分组)
  • 钱包扣款成功率
  • 错误日志:余额不足、并发冲突、套餐激活失败
  • 订单创建耗时P95、P99

告警规则

  • 钱包扣款失败率 > 5%
  • 订单创建失败率 > 10%
  • 并发冲突次数 > 100/分钟

兼容性说明

向后兼容

  • 现有订单字段为空值:不影响已有订单查询
  • 平台代购offline逻辑不变:保持现有行为
  • H5 钱包支付不受影响H5 端仍使用两步流程
  • 数据权限保持一致:订单角色追踪不影响现有数据权限逻辑

破坏性变更

。所有新增字段均为 nullable新增逻辑不影响现有流程。


测试覆盖

集成测试场景

  1. 代理自购:代理为自己的卡购买套餐,验证扣款、激活、流水
  2. 代理代购:一级代理为二级代理购买,验证价格差异、佣金不产生
  3. 平台代购:平台 offline 代购,验证不扣款、佣金产生
  4. 订单查询:验证 OR 查询逻辑、purchase_role 筛选
  5. 边界场景:余额不足、并发扣款、幂等性

验证结果

  • 编译通过:go build ./...
  • OpenAPI 文档更新:新增字段已包含
  • 迁移脚本执行成功

相关文档