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

5.7 KiB
Raw Blame History

Context

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

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

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

Goals / Non-Goals

Goals:

  • IotCardDevice 模型上新增 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

决策 1realname_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 字段:
    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

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