- 新增 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
5.7 KiB
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
- 创建数据库迁移文件,为四张表新增
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'; - 迁移前:REALNAME-03 注释代码继续保持注释(不影响线上)
- 迁移后:代码上线,实名拦截以新字段为准;存量全为
none,等于放行,行为不变 - 回滚策略:字段加 DEFAULT,删除新增字段即可,代码回滚到上一版本
Open Questions
无(所有关键问题已在探索阶段与业务方确认)