## Context

当前系统中，资产（IoT 卡和设备）的操作接口混用两套参数规范：
- **B 端大多数接口**：使用数据库主键 ID（`/assets/:asset_type/:id/`、`/devices/:id/`），前端持有无业务含义的整数 ID
- **部分接口**：使用业务标识符（`/assets/card/:iccid/stop`、`/devices/by-identifier/:identifier/wifi`），但命名不一致
- **C 端接口**：已规范使用 `identifier` query 参数（ICCID/VirtualNo）

主要问题：
1. IoT 卡和设备各自表内有 VirtualNo 唯一索引，但**跨表没有全局唯一约束**，理论上可写入相同虚拟号
2. 订单创建接口传入 `iot_card_id`/`device_id` 主键，响应也只返回 ID，不含标识符
3. `purchase_validation` 未校验绑定设备的卡，允许对非独立卡（`is_standalone=false`）单独购买套餐
4. `Resolve` 服务对两张表做 OR 多字段查询，性能随数据量增长有退化风险

## Goals / Non-Goals

**Goals:**
- 建立全局资产标识符注册表（`tb_asset_identifier`），数据库层保证 VirtualNo/ICCID 跨表全局唯一
- B 端所有资产操作接口统一使用 `:identifier` 路径参数（ICCID 或 VirtualNo）
- `Resolve` 主查询路径改走注册表，降低查询复杂度
- 订单接口改用 `identifier` 传参，响应补充标识符快照字段
- 修复绑定设备的卡可单独购买套餐的 Bug

**Non-Goals:**
- IoT 卡导入强制 VirtualNo（属于提案三，独立推进）
- 资产往期订单查询接口（属于提案二，独立推进）
- C 端接口变更（C 端已规范，本次不动）
- Resolve 接口的 IMEI/SN/MSISDN fallback 查询（保留现有逻辑）

## Decisions

### 决策 1：使用独立注册表（tb_asset_identifier）实现跨表全局唯一

**选择**：新增 `tb_asset_identifier` 表，`identifier` 字段加 `UNIQUE` 约束

**备选方案**：
- *应用层检查*：插入前先查两张表，存在则拒绝。问题：高并发下存在竞态窗口，同一标识符可能被两个并发请求同时通过检查
- *数据库触发器*：在 `tb_device` 和 `tb_iot_card` 上建触发器查对方表。问题：触发器调试困难，项目中无此先例，违背"逻辑在代码层"原则

**理由**：注册表方案通过数据库 UNIQUE 约束消除竞态——两个并发写入同一标识符时，数据库只允许一个成功，另一个收到约束冲突错误并回滚事务，安全可靠且符合项目"禁止外键，关联在代码层维护"的原则。

**表结构**：
```sql
CREATE TABLE tb_asset_identifier (
    id          BIGSERIAL PRIMARY KEY,
    identifier  VARCHAR(100) NOT NULL,
    asset_type  VARCHAR(20)  NOT NULL,  -- 'iot_card' | 'device'
    asset_id    BIGINT       NOT NULL,
    created_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    CONSTRAINT uq_asset_identifier UNIQUE (identifier)
);
CREATE INDEX idx_asset_identifier_asset ON tb_asset_identifier(asset_type, asset_id);
```

**写入时机**：
- 创建设备/IoT 卡时，在**同一事务**内写入注册表
- 软删除时，同步从注册表删除（或标记 deleted_at，避免已删除的标识符阻塞复用）
- VirtualNo 更新时（目前无此场景，预留），先删旧记录再插新记录

### 决策 2：Resolve 主路径改走注册表，保留 fallback

**选择**：`Resolve` 方法优先查 `tb_asset_identifier`（精确匹配），未命中再 fallback 到原有跨表 OR 查询（处理 IMEI/SN/MSISDN 等非注册标识符）

**理由**：注册表只存 VirtualNo 和 ICCID（全局唯一的标识符），IMEI/SN/MSISDN 因非唯一不写入注册表。Resolve 接口仍需支持这些模糊标识符（方便管理员查找），但精确路径走注册表可大幅提升性能。

```
Resolve(identifier) 逻辑：
  1. 查 tb_asset_identifier WHERE identifier = ?
     → 命中：得到 asset_type + asset_id，按 type 查完整记录
     → 未命中：fallback 到原有逻辑（OR 多字段查 device/iot_card）
```

### 决策 3：B 端接口统一 :identifier，不区分 card/device 类型

**选择**：路由改为 `/admin/assets/:identifier/*`，系统内部通过 Resolve 得到 asset_type，无需路径中显式传类型

**备选方案**：
- *保留 :asset_type 前缀*：`/admin/assets/card/:iccid/packages`。问题：调用方需要知道"这是卡还是设备"才能构造 URL，而 identifier 本身已能唯一定位
- *两段式 Resolve + ID*：前端先调 Resolve 拿 ID 再操作。问题：前端耦合两个接口，且 ID 无业务含义

**理由**：identifier 已能唯一定位资产（通过注册表），asset_type 冗余；URL 语义更自然（`/assets/ICCID-001/packages` 而非 `/assets/card/ICCID-001/packages`）。

**注意**：仅操作类接口（packages/realtime/stop/start 等）使用统一 `:identifier`。设备绑卡管理（`/devices/:virtual_no/cards`）因属于设备专属操作，路径保留 `devices` 前缀但改用 VirtualNo。

### 决策 4：订单创建改用 identifier，系统内部解析 asset_type

**选择**：`CreateOrderRequest` 去掉 `iot_card_id`/`device_id`，改为 `identifier` 字段，`order_type` 字段同时废弃（由 identifier 解析结果决定）

**理由**：与接口统一规范一致；同时自然修复"绑定设备的卡可单独购买"Bug——解析 identifier 后得到 `is_standalone=false` 的卡，直接在 purchase_validation 层拦截。

**Order 模型快照**：订单记录中增加 `asset_identifier VARCHAR(100)` 字段，存储下单时资产的标识符快照（类似 `order_no` 的快照思路），便于历史查询而不依赖关联查询。

## Risks / Trade-offs

| 风险 | 缓解措施 |
|------|---------|
| **Breaking 变更**：B 端所有资产路由改变，前端需全量更新 | 系统处于测试阶段，前端可同步切换；通过 OpenAPI 文档明确新路由 |
| **存量资产未在注册表**：现有 tb_device 和 tb_iot_card 数据需回填注册表 | 提供数据迁移脚本，回填时检查 VirtualNo 跨表唯一性；直接清库（测试阶段）则无此问题 |
| **注册表成为热点**：资产创建时额外写一张表 | 注册表操作在事务内，单次 INSERT；正常导入批量处理，影响极小 |
| **Resolve fallback 性能**：IMEI/SN 仍走全表 OR 查询 | fallback 路径为次要场景，精确路径（注册表）已涵盖主要查询；长期可为 IMEI/SN 建索引 |
| **order_type 废弃兼容**：历史订单含 order_type 字段 | 历史数据保留，新创建订单由系统根据 identifier 填入 order_type；API 不再接受 order_type 输入 |

## Migration Plan

**步骤一：数据库迁移**
1. 创建迁移文件，建立 `tb_asset_identifier` 表
2. 执行回填脚本：将所有现有设备的 VirtualNo 和 IoT 卡的 ICCID/VirtualNo 写入注册表
3. 测试阶段：直接清库则跳过步骤 2

**步骤二：后端实现（可分 PR 推进）**
1. 新增 `AssetIdentifier` 模型和 `asset_identifier_store`
2. 改造 `Resolve` 服务，注册表主路径 + fallback
3. 更新 `purchase_validation`，增加 `is_standalone` 校验
4. 更新 `Order` 模型，增加 `asset_identifier` 快照字段
5. 重写 B 端路由：`asset.go`、`device.go`、`iot_card.go`、`order.go`
6. 更新 DTO：`asset_dto.go`、`order_dto.go`、`client_order_dto.go`
7. 更新 Handler 层参数解析
8. 更新 `gendocs` 文档生成

**步骤三：验证**
1. 全量 API 回归测试
2. 并发写入同一 VirtualNo 压测（验证注册表唯一约束）
3. Resolve fallback 路径验证（IMEI/SN 查询）

**回滚方案**：保留旧路由映射（别名路由）72 小时，确认前端切换完毕后删除

## Open Questions

- ~~已解决：VirtualNo 跨表全局唯一策略（独立注册表）~~
- ~~已解决：B 端路由统一方案（方案 B，统一 :identifier）~~
- ~~已解决：订单传参方式（identifier，不传 asset_type）~~
- **待确认**：Order 模型中 `asset_identifier` 快照字段的数据库类型选 `VARCHAR(100)` 是否满足所有标识符长度（ICCID 最长 20 位，VirtualNo 最长 100 位）
