## Context

系统存在一段注释掉的实名拦截代码（`REALNAME-03`，位于 `internal/service/client_order/service.go` 第 143 行），原逻辑为"普通卡（`card_category=normal`）需实名才能购买套餐，行业卡（`industry`）无需实名"。由于业务还未明确，该逻辑被整体注释，由网关侧临时托管。

现在业务明确了两种场景：**先实名后充值**（充值/购买前拦截未实名资产）与**先充值后实名**（充值/购买放行，实名链接在未充值前拦截）。且行业卡在特定业务下也可能需要实名，原来依赖 `card_category` 的硬编码逻辑不够灵活。

需要在资产层面（`IotCard` 和 `Device`）引入可配置的 `realname_policy` 字段，取代原有硬编码逻辑，支持导入时批量初始化策略、后台管理接口修改策略、所有查询接口返回策略字段、以及 C 端三接口按策略执行拦截。

## Goals / Non-Goals

**Goals:**
- 在 `IotCard` 和 `Device` 模型上新增 `realname_policy` 字段（string 枚举）
- IoT 卡导入和设备导入支持批量初始化 `realname_policy`
- 后台新增 `PATCH /api/admin/assets/:identifier/realname-mode` 接口修改单资产策略
- C 端充值（C3/C4）、购买套餐/订单（D1/D4）、实名链接（E1）按策略拦截
- 所有涉及卡/设备/资产的查询接口返回 `realname_policy` 字段
- 存量数据迁移默认值为 `none`，不影响现有行为

**Non-Goals:**
- 不修改登录层的实名检查（已有机制，由网关托管）
- 不实现用户通知（after_order 实名提醒等）
- 不修改 Carrier 层的 `realname_link_type`（运营商提供实名链接的方式，是另一个维度）
- 不实现按套餐粒度的实名策略（只到资产粒度）

## Decisions

**决策 1：`realname_policy` 使用 string 枚举而非 bool**

选 `string` 而非 `bool`（如 `realname_required`）：业务存在三种状态（无需实名 / 先实名后充值 / 先充值后实名），bool 只能表达两种。枚举值：`none` / `before_order` / `after_order`。

**决策 2：设备卡场景以 Device.realname_policy 为准**

当一张卡绑定到设备时（设备卡），取 `Device.realname_policy`；未绑定设备时（单卡），取 `IotCard.realname_policy`。这与现有系统中"设备层面的验证规则高于卡层面"的设计一致（登录前的设备验证已体现此原则）。

判断逻辑封装在 Service 层公共方法 `GetEffectiveRealnamePolicy(card *model.IotCard, device *model.Device) string`，避免在多个 Service 中重复实现。

**决策 3：after_order 场景下实名链接的前置条件**

`after_order` 模式下，用户调用 `GET /api/c/v1/realname/link` 时，系统检查该资产是否存在有效充值或已支付订单记录（`status=2` 的订单或 `status=2` 的充值单）。无记录则拦截；有记录则放行。

逻辑：先充值才有资格实名，避免用户跳过充值直接实名。检查范围：该资产当前 generation 内的充值记录或套餐订单记录。

**决策 4：后台更新接口复用现有资产解析器**

遵循 `UpdatePollingStatus` 的模式（`PATCH /api/admin/assets/:identifier/polling-status`）：
- 接口路径：`PATCH /api/admin/assets/:identifier/realname-mode`
- 通过 `assetService.Resolve()` 将标识符解析为 asset_type + asset_id
- asset_type=card → 更新 `IotCard.realname_policy`
- asset_type=device → 更新 `Device.realname_policy`
- 单接口覆盖两种资产类型，与现有模式完全一致

**决策 5：存量数据默认值为 `none`**

迁移时所有存量卡和设备的 `realname_policy` 默认值为 `none`（无需实名），保持与现有行为完全一致（目前实名拦截已被注释，等于放行）。运营方后续按需批量更新。

**决策 6：命名区分**

`RealnamePolicy`（资产的实名策略：何时需要实名）与现有 `RealnameLinkType`（运营商提供实名链接的方式：none/template/gateway）是两个独立维度，字段名不同，不产生混淆。DTO 中 response 字段名为 `realname_policy`。

## Risks / Trade-offs

**[风险 1] after_order 拦截查询增加数据库压力**
→ 缓解：查询只取 `COUNT(*) > 0`，命中索引（generation + asset_id + status），可加 Redis 缓存（key 为 `realname:after_order_eligible:{asset_type}:{asset_id}:{generation}`，充值/购买成功时主动写入，TTL 24h）。初期数据量小，可不加缓存，待观察后决定。

**[风险 2] DTO 改动面大，遗漏字段**
→ 缓解：所有涉及卡/设备/资产的 DTO 已在探索阶段完整盘点，tasks.md 按文件逐一列举，不依赖记忆。

**[风险 3] 存量数据全为 `none`，初始化后无法区分"未配置"与"主动设为 none"**
→ 可接受：`none` 本身是有效业务值（无需实名），与未配置语义相同，不需要额外区分。

## Migration Plan

1. 创建数据库迁移文件，为四张表新增 `realname_policy` 字段：
   ```sql
   ALTER TABLE tb_iot_card ADD COLUMN realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
   ALTER TABLE tb_device ADD COLUMN realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
   ALTER TABLE tb_iot_card_import_task ADD COLUMN realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
   ALTER TABLE tb_device_import_task ADD COLUMN realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
   ```
2. 迁移前：REALNAME-03 注释代码继续保持注释（不影响线上）
3. 迁移后：代码上线，实名拦截以新字段为准；存量全为 `none`，等于放行，行为不变
4. 回滚策略：字段加 DEFAULT，删除新增字段即可，代码回滚到上一版本

## Open Questions

无（所有关键问题已在探索阶段与业务方确认）
