Files
junhong_cmp_fiber/docs/fix-agent-wallet-order-creation/功能总结.md
huang 8ed3d9da93
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m0s
feat: 实现代理钱包订单创建和订单角色追踪功能
新增功能:
- 代理在后台使用 wallet 支付时,订单直接完成(扣款 + 激活套餐)
- 支持代理自购和代理代购场景
- 新增订单角色追踪字段(operator_id、operator_type、actual_paid_amount、purchase_role)
- 订单查询支持 OR 逻辑(buyer_id 或 operator_id)
- 钱包流水记录交易子类型和关联店铺
- 佣金逻辑调整:代理代购不产生佣金

数据库变更:
- 订单表新增 4 个字段和 2 个索引
- 钱包流水表新增 2 个字段
- 包含迁移脚本和回滚脚本

文档:
- 功能总结文档
- 部署指南
- OpenAPI 文档更新
- Specs 同步(新增 agent-order-role-tracking capability)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-28 14:11:42 +08:00

515 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 代理钱包订单创建功能总结
## 概述
fix-agent-wallet-order-creation 提案修复了代理在后台使用钱包支付创建订单的问题,实现了代理钱包一步购买(扣款 + 激活)、代理代购、订单角色追踪等核心功能。
## <20><>景问题
### 问题描述
代理在后台使用钱包支付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`
```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`
```go
const (
WalletTransactionSubtypeSelfPurchase = "self_purchase"
WalletTransactionSubtypePurchaseForSubordinate = "purchase_for_subordinate"
)
```
**流水示例**
- **自购**`transaction_subtype = "self_purchase"``remark = "购买套餐"`
- **代购**`transaction_subtype = "purchase_for_subordinate"``related_shop_id = 下级店铺 ID``remark = "为下级代理【XX】购买套餐"`
---
### 5. 订单查询增强
**OR 查询逻辑**`OrderStore.List()`
```sql
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 下级成本价),不产生佣金
- **平台代购**:平台不扣款,按买家成本价计算差价佣金,激励上级代理
**实现**
```go
// 只有平台代购operator_id == nil才入队佣金计算
if order.OperatorID == nil {
s.enqueueCommissionCalculation(ctx, order.ID)
}
```
---
### 7. 幂等性和并发控制
**乐观锁**(钱包扣款):
```go
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
**端点**`POST /api/admin/orders`
**行为变更**
- 代理使用 wallet 支付时,订单直接完成(`payment_status = 2`),无需后续支付
- 平台使用 offline 支付逻辑保持不变
**响应新增字段**
```json
{
"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】购买"
}
```
---
### 订单列表 API
**端点**`GET /api/admin/orders`
**新增查询参数**
- `purchase_role` (可选):订单角色筛选
- `self_purchase`:自己购买
- `purchased_by_parent`:上级代理购买
- `purchased_by_platform`:平台代购
- `purchase_for_subordinate`:给下级购买
**查询逻辑变更**
- 代理可以看到 `buyer_id = 自己``operator_id = 自己` 的所有订单
---
## 数据库变更
### 订单表tb_order
**新增字段**
```sql
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';
```
**新增索引**
```sql
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
**新增字段**(如果不存在):
```sql
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()` 方法重构**
```go
// 场景判断
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()` 方法**
```go
// 代理用户:查询作为买家或操作者的订单
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()` 方法
---
## 使用指南
### 代理自购场景
**请求**
```http
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"
}
```
**响应**
```json
{
"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": "订单创建成功"
}
```
---
### 代理代购场景
**请求**
```http
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"
}
```
**响应**
```json
{
"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": "订单创建成功"
}
```
---
### 订单列表查询
**请求**
```http
GET /api/admin/orders?purchase_role=purchase_for_subordinate&page=1&page_size=20
Authorization: Bearer {agent_token}
```
**响应**
```json
{
"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 文档更新:新增字段已包含
- ✅ 迁移脚本执行成功
---
## 相关文档
- [提案文档](../../openspec/changes/fix-agent-wallet-order-creation/proposal.md)
- [设计文档](../../openspec/changes/fix-agent-wallet-order-creation/design.md)
- [任务清单](../../openspec/changes/fix-agent-wallet-order-creation/tasks.md)
- [Specs 规范](../../openspec/changes/fix-agent-wallet-order-creation/specs/)
- [项目规范](../../CLAUDE.md)