## Context

`asset-detail-refactor` 建立了 `/api/admin/assets/` 路径下的完整资产体系（解析、状态、套餐、停复机），但缺少钱包维度。现有钱包体系命名混乱：`CardWallet` / `tb_card_wallet` 实际同时承载 `iot_card` 和 `device` 两种资产，名不副实。此外，H5 端个人客户用钱包支付套餐时（`WalletPay`）代码直接 `UPDATE` 余额，从未写入 `CardWalletTransaction` 流水记录，导致流水表只有充值记录、没有扣款记录。

本次设计目标：改名 + 补流水 + 新增 Admin 查询接口，三件事合并在一个变更里。

## Goals / Non-Goals

**Goals:**
- 数据表改名：`tb_card_wallet*` → `tb_asset_wallet*`，代码全量跟进
- 流水表字段变更：`reference_id (bigint)` → `reference_no (varchar 50)`，存储可读业务编号
- 补写 `WalletPay` 卡钱包路径的 `deduct` 流水，填补现有数据空白
- 新增 Admin 端资产钱包概况接口 `GET /api/admin/assets/:asset_type/:id/wallet`
- 新增 Admin 端资产钱包流水列表接口 `GET /api/admin/assets/:asset_type/:id/wallet/transactions`
- 企业账号禁止访问钱包接口；代理账号通过 `shop_id_tag` 自动过滤

**Non-Goals:**
- H5 端不新增/修改任何接口（现有充值 / 订单接口 JSON 字段名不变）
- 不新增 Admin 端充值单列表接口（充值记录通过流水的 `reference_no` 跳转即可）
- 不做历史数据回填（`WalletPay` 修复只对新数据有效）
- 代理主钱包（`AgentWallet`）不在本次范围

## Decisions

### 决策 1：三张表全部改名，保持 `tb_asset_*` 前缀统一

```
tb_card_wallet              → tb_asset_wallet
tb_card_wallet_transaction  → tb_asset_wallet_transaction
tb_card_recharge_record     → tb_asset_recharge_record
```

代码层 Go 类型名同步改名：
```
CardWallet              → AssetWallet
CardWalletTransaction   → AssetWalletTransaction
CardRechargeRecord      → AssetRechargeRecord
CardWalletStore         → AssetWalletStore
CardWalletTransactionStore → AssetWalletTransactionStore
CardRechargeStore       → AssetRechargeStore
```

Redis Key 常量更名：`RedisCardWalletBalanceKey` → `RedisAssetWalletBalanceKey`

### 决策 2：`reference_id (bigint)` → `reference_no (varchar 50)`

原来存主键 ID，前端无法直接用于展示或跳转；改为存业务编号：
- 充值场景：`reference_no = recharge.RechargeNo`（格式：`CRCH…`）
- 扣款场景：`reference_no = order.OrderNo`（格式：`ORD…`）

现有流水数据全部在开发阶段写入（无生产数据），直接 `ALTER COLUMN`，不需要数据迁移。

### 决策 3：`WalletPay` 卡钱包路径补写 `deduct` 流水

在卡钱包扣款成功后，在同一事务内写入 `AssetWalletTransaction`：

```go
transaction := &model.AssetWalletTransaction{
    AssetWalletID:   wallet.ID,
    ResourceType:    resourceType,           // "iot_card" 或 "device"
    ResourceID:      resourceID,
    UserID:          buyerID,
    TransactionType: "deduct",
    Amount:          -order.TotalAmount,     // 负数
    BalanceBefore:   walletBalanceBefore,
    BalanceAfter:    walletBalanceBefore - order.TotalAmount,
    Status:          1,
    ReferenceType:   strPtr("order"),
    ReferenceNo:     &order.OrderNo,
    Remark:          strPtr("钱包支付套餐"),
    ShopIDTag:       wallet.ShopIDTag,
    EnterpriseIDTag: wallet.EnterpriseIDTag,
}
```

### 决策 4：权限控制复用 `ApplyShopTagFilter`

`tb_asset_wallet` 和 `tb_asset_wallet_transaction` 都有 `shop_id_tag` 字段，已有 `ApplyShopTagFilter` 机制：
- 平台/超管：不添加过滤条件
- 代理用户：`WHERE shop_id_tag IN (当前店铺及下级店铺IDs)`
- 企业账号：在 Handler 层检查 `user_type == UserTypeEnterprise`，直接返回 403

### 决策 5：新接口挂载在现有 `/assets/` 路径下，由 `AssetWalletHandler` 承载

独立的 Handler 文件 `internal/handler/admin/asset_wallet.go`，通过 `AssetWalletService` 提供服务逻辑。路由注册追加到 `internal/routes/asset.go`。

---

## API 合约

### 接口一：查询资产钱包概况

```
GET /api/admin/assets/:asset_type/:id/wallet
```

**路径参数**：

| 参数 | 类型 | 说明 |
|------|------|------|
| `asset_type` | string | 资产类型：`card` 或 `device` |
| `id` | uint | 资产数据库 ID |

**请求体**：无

**成功响应** `200 OK`：

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "wallet_id":        123,
    "resource_type":    "iot_card",
    "resource_id":      456,
    "balance":          10000,
    "frozen_balance":   0,
    "available_balance": 10000,
    "currency":         "CNY",
    "status":           1,
    "status_text":      "正常",
    "created_at":       "2026-03-10T00:00:00Z",
    "updated_at":       "2026-03-10T00:00:00Z"
  },
  "timestamp": 1741564800
}
```

**响应字段说明**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `wallet_id` | uint | 钱包数据库 ID |
| `resource_type` | string | `iot_card` 或 `device` |
| `resource_id` | uint | 对应卡或设备的数据库 ID |
| `balance` | int64 | 总余额（分） |
| `frozen_balance` | int64 | 冻结余额（分） |
| `available_balance` | int64 | 可用余额 = balance - frozen_balance（分） |
| `currency` | string | 币种，目前固定 `CNY` |
| `status` | int | 钱包状态：1-正常 2-冻结 3-关闭 |
| `status_text` | string | 状态文本 |
| `created_at` | string | 创建时间（RFC3339） |
| `updated_at` | string | 更新时间（RFC3339） |

**错误响应**：

| 场景 | HTTP 状态码 | 错误码 | 错误消息 |
|------|------------|--------|---------|
| `asset_type` 非法 | 400 | `CodeInvalidParam` | 无效的资产类型 |
| `id` 非法 | 400 | `CodeInvalidParam` | 无效的资产ID |
| 资产不存在 | 404 | `CodeNotFound` | 资产不存在 |
| 钱包不存在 | 404 | `CodeNotFound` | 该资产暂无钱包记录 |
| 企业账号调用 | 403 | `CodeForbidden` | 企业账号无权查看钱包信息 |

---

### 接口二：查询资产钱包流水列表

```
GET /api/admin/assets/:asset_type/:id/wallet/transactions
```

**路径参数**：同上

**查询参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `page` | int | 否 | 页码，默认 1 |
| `page_size` | int | 否 | 每页数量，默认 20，最大 100 |
| `transaction_type` | string | 否 | 类型过滤：`recharge` / `deduct` / `refund` |
| `start_time` | string | 否 | 开始时间（RFC3339） |
| `end_time` | string | 否 | 结束时间（RFC3339） |

**成功响应** `200 OK`：

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "list": [
      {
        "id":                   1,
        "transaction_type":     "deduct",
        "transaction_type_text": "扣款",
        "amount":               -3000,
        "balance_before":       10000,
        "balance_after":        7000,
        "reference_type":       "order",
        "reference_no":         "ORD20260310001",
        "remark":               "钱包支付套餐",
        "created_at":           "2026-03-10T14:20:00Z"
      },
      {
        "id":                   2,
        "transaction_type":     "recharge",
        "transaction_type_text": "充值",
        "amount":               10000,
        "balance_before":       0,
        "balance_after":        10000,
        "reference_type":       "recharge",
        "reference_no":         "CRCH20260309001",
        "remark":               "钱包充值",
        "created_at":           "2026-03-09T09:15:00Z"
      }
    ],
    "total":       2,
    "page":        1,
    "page_size":   20,
    "total_pages": 1
  },
  "timestamp": 1741564800
}
```

**响应字段说明（单条流水）**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | uint | 流水记录 ID |
| `transaction_type` | string | 交易类型：`recharge`/`deduct`/`refund` |
| `transaction_type_text` | string | 交易类型文本：充值/扣款/退款 |
| `amount` | int64 | 变动金额（分），充值为正数，扣款/退款为负数 |
| `balance_before` | int64 | 变动前余额（分） |
| `balance_after` | int64 | 变动后余额（分） |
| `reference_type` | string | 关联业务类型：`recharge` 或 `order`（可空） |
| `reference_no` | string | 关联业务编号：充值单号（CRCH…）或订单号（ORD…）（可空） |
| `remark` | string | 备注（可空） |
| `created_at` | string | 流水创建时间（RFC3339） |

**错误响应**：同接口一

---

## 接口调用流程图

### 场景一：Admin 查看资产钱包详情（典型调用链）

```
前端（Admin 页面）                API 服务                       数据库
       │                            │                              │
       │── ① 解析资产 ─────────────→│                              │
       │  GET /assets/resolve/:identifier                          │
       │                            │── AssetService.Resolve() ──→ │
       │                            │   SELECT iot_card/device     │
       │← 返回 {asset_type, asset_id}│←────────────────────────────│
       │                            │                              │
       │── ② 查询钱包概况 ──────────→│                              │
       │  GET /assets/card/456/wallet                              │
       │                            │── AssetWalletService         │
       │                            │   .GetWallet()              │
       │                            │── SELECT tb_asset_wallet ──→ │
       │                            │   WHERE resource_type='iot_card'
       │                            │   AND resource_id=456        │
       │← 返回 {balance, available…}│←────────────────────────────│
       │                            │                              │
       │── ③ 查询流水列表 ──────────→│                              │
       │  GET /assets/card/456/wallet/transactions?page=1          │
       │                            │── AssetWalletService         │
       │                            │   .ListTransactions()       │
       │                            │── SELECT tb_asset_wallet_transaction
       │                            │   WHERE resource_type='iot_card'
       │                            │   AND resource_id=456        │
       │                            │   ORDER BY created_at DESC   │
       │← 返回流水列表 ─────────────│←────────────────────────────│
       │  [{type:deduct, ref:ORD…},  │                              │
       │   {type:recharge, ref:CRCH…}]                             │
       │                            │                              │
       │── ④（可选）前端用 reference_no 跳转到充值单或订单详情页   │
```

### 场景二：H5 个人客户钱包支付（补写流水后的完整事务）

```
H5 前端                    order.Service.WalletPay()            数据库（事务）
    │                              │                                  │
    │── POST /orders/:id/wallet-pay→│                                  │
    │                              │                                  │
    │                              │── ① 查询订单 ─────────────────→ │
    │                              │   SELECT tb_order WHERE id=:id   │
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ② 查询 AssetWallet ─────────→ │
    │                              │   SELECT tb_asset_wallet         │
    │                              │   WHERE resource_type+resource_id│
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ③ 开启事务 ─────────────────→ │
    │                              │   BEGIN                          │
    │                              │                                  │
    │                              │── ④ 更新订单支付状态 ─────────→ │
    │                              │   UPDATE tb_order                │
    │                              │   SET payment_status=paid        │
    │                              │   WHERE id=:id AND status=pending│
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ⑤ 扣减钱包余额（乐观锁）────→ │
    │                              │   UPDATE tb_asset_wallet         │
    │                              │   SET balance=balance-amount     │
    │                              │   WHERE id=:id AND version=:v    │
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ⑥ ★ 写入扣款流水（新增）────→ │
    │                              │   INSERT tb_asset_wallet_transaction
    │                              │   (transaction_type="deduct",    │
    │                              │    amount=-totalAmount,          │
    │                              │    reference_type="order",       │
    │                              │    reference_no=order.OrderNo)   │
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ⑦ 激活套餐 ─────────────────→ │
    │                              │   activatePackage()              │
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ⑧ 提交事务 ─────────────────→ │
    │                              │   COMMIT                         │
    │← 200 OK ──────────────────── │                                  │
```

---

## 字段变更对现有接口的影响分析

### H5 充值接口（**无 breaking change**）

以下 H5 接口 JSON 响应字段名保持不变，前端零感知：

| 接口 | JSON 字段 | Go 字段改名 | 影响 |
|------|-----------|------------|------|
| `POST /api/h5/wallets/recharge` 响应 | `wallet_id` | `CardRechargeRecord.CardWalletID` → `AssetRechargeRecord.AssetWalletID` | JSON tag 不变，**无影响** |
| `GET /api/h5/wallets/recharges` 响应 | `wallet_id` | 同上 | JSON tag 不变，**无影响** |
| `GET /api/h5/wallets/recharges/:id` 响应 | `wallet_id` | 同上 | JSON tag 不变，**无影响** |

### 新增接口中 `reference_no` 字段（**全新字段，无旧调用方**）

`tb_asset_wallet_transaction.reference_no` 是首次通过 API 暴露。变更前该字段叫 `reference_id`（uint），从未被任何接口返回，不存在已有调用方，**无 breaking change**。

---

## Risks / Trade-offs

**[风险 1] `WalletPay` 中余额快照时机**

扣款前需记录 `balance_before`，但乐观锁可能因并发重试导致实际余额与快照不符。缓解：在事务内先查询钱包余额作为快照，再执行乐观锁更新；`balance_before` = 查询值，`balance_after` = 查询值 - amount，若乐观锁失败则整个事务回滚，流水不写入。

**[风险 2] 表改名期间的零停机**

开发阶段无生产数据，直接 migration 改名，无需双写策略。

**[Trade-off] 不回填历史扣款流水**

`WalletPay` 修复前的历史订单无对应 `deduct` 流水。接受：开发阶段，测试数据可清空重来，不引入回填复杂度。
