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

## 概述

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

## ��景问题

### 问题描述

代理在后台使用钱包支付（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)
