feat: 实现资产实名认证策略功能 (asset-realname-policy)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m44s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m44s
- 新增 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
This commit is contained in:
86
openspec/changes/asset-realname-policy/design.md
Normal file
86
openspec/changes/asset-realname-policy/design.md
Normal file
@@ -0,0 +1,86 @@
|
||||
## 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
|
||||
|
||||
无(所有关键问题已在探索阶段与业务方确认)
|
||||
Reference in New Issue
Block a user