All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
- 拆分订单创建为 CreateAdminOrder(后台一步支付)和 CreateH5Order(H5 两步支付) - 新增 CreateAdminOrderRequest DTO,后台仅允许 wallet/offline 支付方式 - 同步 delta specs 到主规格(order-payment 更新 + admin-order-creation 新增) - 归档 fix-agent-wallet-order-creation 变更 - 新增 implement-order-expiration 变更提案
526 lines
15 KiB
Markdown
526 lines
15 KiB
Markdown
# 代理钱包订单创建功能总结
|
||
|
||
## 概述
|
||
|
||
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 = 10000(100 元,B 看到的订单金额)
|
||
- actual_paid_amount = 8000(80 元,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}`
|
||
- TTL:3 分钟
|
||
- 分布式锁防止并发:`order:create:lock:{carrier_type}:{carrier_id}`
|
||
|
||
---
|
||
|
||
## API 变更
|
||
|
||
### 后台订单创建 API(❗ Breaking Change)
|
||
|
||
**端点**:`POST /api/admin/orders`
|
||
|
||
**请求参数变更**:
|
||
|
||
| 字段 | 变更前 | 变更后 | 说明 |
|
||
|------|--------|--------|------|
|
||
| `payment_method` | 可选,任意值 | **必填**,仅允许 `wallet` 或 `offline` | 不传或传其他值均返回 1001 错误 |
|
||
|
||
**行为变更**:
|
||
- `wallet` 支付:订单直接完成(`payment_status = 2`),无需后续支付接口
|
||
- `offline` 支付:逻辑保持不变
|
||
- 传入 `wechat`/`alipay` → 返回 `{"code": 1001, "msg": "请求参数解析失败"}`
|
||
|
||
**响应新增字段**:
|
||
```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】购买"
|
||
}
|
||
```
|
||
|
||
### 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)
|
||
|
||
**新增字段**:
|
||
```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)
|