Files
junhong_cmp_fiber/openspec/changes/iccid-dual-column-lookup/specs/iot-card/spec.md
huang e049080f6c
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m47s
分裂iccid长度
2026-04-21 10:55:12 +08:00

6.5 KiB
Raw Blame History

ADDED Requirements

Requirement: ICCID 双列存储

tb_iot_card SHALL 新增 iccid_19 varchar(19)iccid_20 varchar(20) 两列。

  • iccid_19:存储 ICCID 前 19 位(所有卡均有值,对应 19 位运营商卡即为完整 ICCID
  • iccid_20:存储完整 20 位 ICCID仅 20 位运营商卡有值19 位运营商卡为 SQL NULL
  • 两列均建立 Partial Index过滤 deleted_at IS NULLiccid_20 额外过滤 NULL

Go Model 类型约束

  • ICCID19 字段类型为 string(非指针,所有卡必填)
  • ICCID20 字段类型为 *string指针19 位卡赋值 nil → GORM 写入 NULL禁止赋空字符串 ""GORM 不会将其转为 NULL会破坏 Partial Index 语义)

Scenario: 导入 19 位运营商卡

  • WHEN 导入 ICCID 长度为 19 位的卡
  • THEN iccid_19 = ICCID 原值,iccid_20 = NULL

Scenario: 导入 20 位运营商卡

  • WHEN 导入 ICCID 长度为 20 位的卡
  • THEN iccid_19 = ICCID 前 19 位,iccid_20 = ICCID 原值

Requirement: ICCID 按长度路由查询

系统 SHALL 根据上游传入 ICCID 的字符串长度,路由到对应列进行精确查询。

  • 上游传入 19 位 → 查询 WHERE iccid_19 = ?
  • 上游传入 20 位 → 查询 WHERE iccid_20 = ?
  • 上游传入其他长度 → 记录 Error 日志,返回未找到,不执行查询
  • 禁止跨列降级:任意列 Miss 时直接记录 Warn 日志,不再查另一列

Scenario: 上游传入 19 位命中 19 位卡

  • WHEN 上游传入 19 位 ICCID数据库存在对应 19 位卡
  • THEN 系统查询 iccid_19 列,精确命中,返回卡记录

Scenario: 上游传入 20 位命中 20 位卡

  • WHEN 上游传入 20 位 ICCID数据库存在对应 20 位卡
  • THEN 系统查询 iccid_20 列,精确命中,返回卡记录

Scenario: 上游传入 19 位但数据库无对应记录

  • WHEN 上游传入 19 位 ICCID数据库中不存在匹配的 iccid_19
  • THEN 系统记录 Warn 日志(含 ICCID 和 deviceID返回未找到不修改任何数据

Scenario: 上游传入 20 位但数据库无对应记录

  • WHEN 上游传入 20 位 ICCID数据库中不存在匹配的 iccid_20
  • THEN 系统记录 Warn 日志(含 ICCID 和 deviceID返回未找到不修改任何数据

Scenario: 上游传入异常长度 ICCID

  • WHEN 上游传入长度既非 19 也非 20 的 ICCID
  • THEN 系统记录 Error 日志,直接返回未找到,不执行数据库查询

Requirement: IotCard 精确查询方法适配

IotCardStore 的精确查询方法 SHALL 全部适配双列查询路由策略。

受影响方法:GetByICCIDGetByICCIDsExistsByICCIDExistsByICCIDBatch

Scenario: GetByICCID 单卡精确查询

  • WHEN 调用 GetByICCID(ctx, iccid) 且 ICCID 长度为 19 或 20 位
  • THEN 系统 SHALL 根据长度路由到 iccid_19iccid_20 列查询,走 Partial Index

Scenario: GetByICCIDs 批量精确查询

  • WHEN 调用 GetByICCIDs(ctx, iccids) 且 iccids 中所有 ICCID 长度一致(均为 19 或均为 20
  • THEN 系统 SHALL 路由到对应列使用 IN ? 查询

Scenario: GetByICCIDs 混合长度输入

  • WHEN 调用 GetByICCIDs(ctx, iccids) 且 iccids 中同时包含 19 位和 20 位 ICCID
  • THEN 系统 SHALL 按长度分组,分别查询 iccid_19 IN ?iccid_20 IN ?,合并结果返回

Requirement: DeviceSimBinding 按 ICCID 查询适配

DeviceSimBindingStore 中涉及 ICCID 查询的方法 SHALL 适配双列路由策略。

Scenario: UpdateIsCurrentByDeviceID 子查询适配

  • WHEN 调用 UpdateIsCurrentByDeviceID(ctx, deviceID, currentIccid) 且 currentIccid 非空
  • THEN 子查询 SHALL 根据 currentIccid 长度路由到 iccid_19iccid_20 列查询 iot_card_id
  • AND Miss 时记录 Warn 日志,不更新 is_current不降级

Scenario: GetBoundICCIDs JOIN 查询适配

  • WHEN 调用 GetBoundICCIDs(ctx, iccids) 查询已绑定设备的 ICCID 列表
  • THEN JOIN 条件 SHALL 按传入 iccids 长度路由到对应列

Requirement: PersonalCustomerICCIDStore 查询适配

PersonalCustomerICCIDStore 的精确查询方法 SHALL 适配双列查询路由策略。

tb_personal_customer_iccid 表 SHALL 新增 iccid_19 varchar(19) 列及对应索引。

受影响方法:GetByICCIDGetByCustomerAndICCIDExistsByCustomerAndICCIDCreateOrUpdateLastUsed

Scenario: 个人客户 ICCID 记录写入时同步回填

  • WHEN 系统创建或更新 PersonalCustomerICCID 记录
  • THEN iccid_19 SHALL 同步赋值为 ICCID 前 19 位

Scenario: 个人客户 ICCID 查询按长度路由

  • WHEN 调用 GetByICCID(ctx, iccid)GetByCustomerAndICCID(ctx, customerID, iccid)
  • THEN 系统 SHALL 根据 iccid 长度路由到 iccid_1919 位)或原 iccid20 位)查询

Scenario: 个人客户 ICCID 绑定存在性校验按长度路由

  • WHEN 调用 ExistsByCustomerAndICCID(ctx, customerID, iccid) 检查客户是否已绑定该 ICCID
  • THEN 系统 SHALL 根据 iccid 长度路由到 iccid_1919 位)或原 iccid20 位)查询,与 GetByCustomerAndICCID 保持一致的路由策略
  • AND 若路由结果 Miss返回 false(未绑定),不降级查另一列

设计说明tb_personal_customer_iccid 仅新增 iccid_19 列而不添加 iccid_20 列。原因:该表的 ICCID 来自个人客户手动输入(扫码/手动填写),而非上游 IoT 平台回调。上游平台才是 19/20 位格式不一致问题的来源;个人客户输入的格式与数据库存储格式一致,因此 20 位查询走原 iccid 列即可,无需双列适配。


Requirement: 内联 ICCID SQL 归还 Store 层

enterprise_card/service.go 中两处直接拼写 WHERE iccid IN ? 的内联 SQL SHALL 重构为调用 IotCardStore.GetByICCIDs(),消除架构违规并恢复数据权限过滤。

该重构需要同步变更:

  1. enterprise_card.Service struct 新增 iotCardStore *postgres.IotCardStore 字段
  2. New() 函数签名增加 iotCardStore *postgres.IotCardStore 参数
  3. internal/bootstrap/services.go 调用处传入 s.IotCard

Scenario: 企业卡分配预览调用 Store 层查询

  • WHEN 执行企业卡分配预览或分配操作,需要按 ICCIDs 查询卡列表
  • THEN 系统 SHALL 通过 IotCardStore.GetByICCIDs() 查询,不允许 Service 层直接拼写 ICCID 过滤 SQL