Files
junhong_cmp_fiber/openspec/changes/asset-realname-policy/design.md
huang 505b30aa8f
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m44s
feat: 实现资产实名认证策略功能 (asset-realname-policy)
- 新增 realname_policy 字段到 tb_iot_card、tb_device、tb_iot_card_import_task、tb_device_import_task 四张表
- 策略枚举:none(无需实名)、before_order(先实名后充值/购买)、after_order(先充值/购买后实名)
- C端充值拦截:before_order 策略下未实名用户禁止充值
- C端订单拦截:before_order 策略下未实名用户禁止购买套餐
- C端实名链接拦截:after_order 策略下无充值/订单用户禁止实名认证
- Admin API:PATCH /api/admin/assets/:identifier/realname-mode
- 新增错误码 CodeRealnameNotAvailable=1189
2026-04-17 17:03:41 +08:00

87 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 中重复实现。
**决策 3after_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
无(所有关键问题已在探索阶段与业务方确认)