All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m47s
83 lines
8.0 KiB
Markdown
83 lines
8.0 KiB
Markdown
## 1. 数据库迁移
|
||
|
||
> **注意**:1.1–1.5 全部写在**同一个迁移文件**中,保持原子性;执行迁移后再发布代码。
|
||
>
|
||
> **索引注意**:`CREATE INDEX CONCURRENTLY` 不能在事务块内执行,golang-migrate 默认开启事务,迁移文件中必须使用 `CREATE INDEX IF NOT EXISTS`(不带 CONCURRENTLY)。生产环境若需零停机建索引,可在发布窗口期手动执行 CONCURRENTLY 版本。(与本项目 `000058_add_covering_index_for_deep_pagination.up.sql` 范式一致。)
|
||
|
||
- [x] 1.1 新建迁移文件,给 `tb_iot_card` 添加 `iccid_19 varchar(19)` 和 `iccid_20 varchar(20)` 两列(允许 NULL,使用 `ADD COLUMN IF NOT EXISTS`)
|
||
- [x] 1.1a 迁移文件中执行冲突检测(结果须为 0 行,否则停止迁移人工处理):
|
||
- 检测 19 位前缀重复:`SELECT LEFT(iccid, 19), COUNT(*) FROM tb_iot_card WHERE deleted_at IS NULL GROUP BY LEFT(iccid, 19) HAVING COUNT(*) > 1`
|
||
- 检测异常长度 ICCID:`SELECT id, iccid, LENGTH(iccid) AS len FROM tb_iot_card WHERE deleted_at IS NULL AND LENGTH(iccid) NOT IN (19, 20)`(异常记录回填后将永远无法通过新查询路径命中)
|
||
- [x] 1.2 迁移文件中回填存量数据:`iccid_19 = LEFT(iccid, 19)`,`iccid_20 = iccid`(仅当 LENGTH(iccid)=20,否则 NULL)
|
||
- [x] 1.3 迁移文件中为 `iccid_19` 创建 Partial Index:`CREATE INDEX IF NOT EXISTS idx_iot_card_iccid_19 ON tb_iot_card (iccid_19) WHERE deleted_at IS NULL`(**禁止** CONCURRENTLY,原因见上方注意事项)
|
||
- [x] 1.4 迁移文件中为 `iccid_20` 创建 Partial Index:`CREATE INDEX IF NOT EXISTS idx_iot_card_iccid_20 ON tb_iot_card (iccid_20) WHERE deleted_at IS NULL AND iccid_20 IS NOT NULL`(**禁止** CONCURRENTLY)
|
||
- [x] 1.5 同一迁移文件中,给 `tb_personal_customer_iccid` 添加 `iccid_19 varchar(19)` 列(允许 NULL)并回填数据(`iccid_19 = LEFT(iccid, 19)`),创建对应 Partial Index:`CREATE INDEX IF NOT EXISTS idx_personal_customer_iccid_19 ON tb_personal_customer_iccid (iccid_19) WHERE deleted_at IS NULL`
|
||
- [x] 1.6 执行迁移,验证所有卡记录的 `iccid_19` 和 `iccid_20` 已正确回填(使用 PostgreSQL MCP 查询验证)
|
||
|
||
## 2. Model 层更新
|
||
|
||
- [x] 2.1 在 `internal/model/iot_card.go` 的 `IotCard` 结构体中新增以下两个字段,补充 gorm 标签和中文注释:
|
||
- `ICCID19 string`(非指针,所有卡必填,gorm tag 示例:`gorm:"column:iccid_19;type:varchar(19);comment:ICCID前19位"`)
|
||
- `ICCID20 *string`(**指针类型**,19 位卡为 `nil`,GORM 写入 NULL;20 位卡为非 nil 指针,gorm tag 示例:`gorm:"column:iccid_20;type:varchar(20);comment:完整20位ICCID(仅20位运营商卡有值)"`)
|
||
- 禁止使用 `string` 类型存 `ICCID20`:GORM 不会将空字符串自动转为 NULL,会破坏 `WHERE iccid_20 IS NOT NULL` 索引语义
|
||
- [x] 2.2 在 `internal/model/personal_customer_iccid.go` 的 `PersonalCustomerICCID` 结构体中新增 `ICCID19` 字段,补充 gorm 标签和中文注释
|
||
- [x] 2.3 运行 `lsp_diagnostics` 确认 model 文件无编译错误
|
||
|
||
## 3. 工具函数与校验
|
||
|
||
- [x] 3.1 在 `pkg/utils/iccid.go` 中实现 `SplitICCID` 函数,签名和语义如下:
|
||
```go
|
||
// SplitICCID 按 ICCID 长度拆分为双列存储值
|
||
// 19 位卡:返回 (iccid, nil)
|
||
// 20 位卡:返回 (iccid[:19], &iccid)
|
||
// 其他长度:返回 ("", nil),调用方应将对应卡标记为失败
|
||
func SplitICCID(iccid string) (iccid19 string, iccid20 *string)
|
||
```
|
||
**禁止**返回空字符串 `""` 作为 `iccid20`:ICCID20 字段类型为 `*string`,19 位卡必须返回 `nil`,GORM 才会写入 NULL。
|
||
|
||
- [x] 3.2 更新 `pkg/validator/iccid.go` 的 `ValidateICCID` 函数,增加长度校验:ICCID 长度必须为 19 或 20 位,否则返回 `ICCIDValidationResult{Valid: false, Message: "ICCID 长度必须为19或20位"}`;防止非法长度 ICCID 通过导入写入数据库后永远 miss
|
||
|
||
## 4. IotCard 写入路径适配
|
||
|
||
- [x] 4.1 在 `internal/task/iot_card_import.go` 的 `processBatch()` 中,IotCard 初始化时调用 `utils.SplitICCID` 赋值 `ICCID19` 和 `ICCID20` 字段(此步骤在 task 3.2 的长度校验之后,理论上不会出现异常长度,但防御性保留:若 `iccid19 == ""`,标记 fail 并跳过,不写入数据库)
|
||
- [x] 4.2 使用 PostgreSQL MCP 验证导入后新记录的 `iccid_19` 和 `iccid_20` 字段已正确写入
|
||
|
||
## 5. IotCardStore 精确查询方法适配
|
||
|
||
- [x] 5.1 改造 `GetByICCID(ctx, iccid)`:按 iccid 长度路由到 `iccid_19` 或 `iccid_20` 列查询;异常长度记录 Error 日志;Miss 记录 Warn 日志
|
||
- [x] 5.2 改造 `GetByICCIDs(ctx, iccids)`:按长度分组,分别查询 `iccid_19 IN ?` 和 `iccid_20 IN ?`,合并去重结果
|
||
- [x] 5.3 改造 `ExistsByICCID(ctx, iccid)`:按长度路由到对应列
|
||
- [x] 5.4 改造 `ExistsByICCIDBatch(ctx, iccids)`:按长度分组查询(19 位组 `Pluck("iccid_19", &list19)`,20 位组 `Pluck("iccid_20", &list20)`),合并结果;因各分组内 Pluck 返回值恰好等于原始 ICCID(19 位组:`iccid_19 == 原始 ICCID`;20 位组:`iccid_20 == 原始 ICCID`),直接以 Pluck 值为 map key,调用方 `existingMap[card.ICCID]` 逻辑无需修改
|
||
- [x] 5.5 运行 `lsp_diagnostics` 确认 `iot_card_store.go` 无编译错误
|
||
|
||
## 6. DeviceSimBindingStore 查询方法适配
|
||
|
||
- [x] 6.1 改造 `UpdateIsCurrentByDeviceID` 内部子查询:按 currentIccid 长度路由到 `iccid_19` 或 `iccid_20` 列查询 iot_card_id;Miss 时记录 Warn 日志(日志字段:`iccid`、`device_id`、`column_used`,该方法持有 `deviceID` 参数可直接记录),不更新数据,不降级
|
||
- [x] 6.2 改造 `GetBoundICCIDs`:按传入 iccids 长度**分组**,分别执行两次 JOIN 查询后在应用层合并:
|
||
- 19 位组:`JOIN tb_iot_card c ON c.id = b.iot_card_id WHERE c.iccid_19 IN ? AND b.bind_status = 1 AND c.deleted_at IS NULL`,SELECT `c.iccid_19 AS iccid`
|
||
- 20 位组:`JOIN tb_iot_card c ON c.id = b.iot_card_id WHERE c.iccid_20 IN ? AND b.bind_status = 1 AND c.deleted_at IS NULL`,SELECT `c.iccid_20 AS iccid`
|
||
- 两组结果合并为 `map[string]bool`,key 使用各组 SELECT 的 iccid 值(即原始 ICCID)
|
||
- [x] 6.3 运行 `lsp_diagnostics` 确认 `device_sim_binding_store.go` 无编译错误
|
||
|
||
## 7. PersonalCustomerICCIDStore 查询方法适配
|
||
|
||
- [x] 7.1 改造 `GetByICCID(ctx, iccid)`:按 iccid 长度路由,19 位查 `iccid_19`,20 位查原 `iccid` 列
|
||
- [x] 7.2 改造 `GetByCustomerAndICCID(ctx, customerID, iccid)`:同上,按长度路由
|
||
- [x] 7.3 改造 `CreateOrUpdateLastUsed(ctx, customerID, iccid)`:写入时同步赋值 `ICCID19` 字段
|
||
- [x] 7.3.5 改造 `ExistsByCustomerAndICCID(ctx, customerID, iccid)`:按 iccid 长度路由,19 位查 `iccid_19`,20 位查原 `iccid` 列;路由策略与 `GetByCustomerAndICCID` 保持一致;Miss 时直接返回 `false`,不降级
|
||
- [x] 7.4 运行 `lsp_diagnostics` 确认 `personal_customer_iccid_store.go` 无编译错误
|
||
|
||
## 8. 架构违规修复
|
||
|
||
- [x] 8.1 在 `internal/service/enterprise_card/service.go` 的 `Service` struct 中新增 `iotCardStore *postgres.IotCardStore` 字段,并更新 `New()` 函数签名增加 `iotCardStore *postgres.IotCardStore` 参数
|
||
- [x] 8.2 更新 `internal/bootstrap/services.go` 第 191 行 `enterpriseCardSvc.New()` 调用,传入 `s.IotCard`(IotCardStore 实例)
|
||
- [x] 8.3 将 `enterprise_card/service.go` 中两处直接拼写 `WHERE iccid IN ?` 的内联 SQL(第 46 行、第 185 行)重构为调用 `s.iotCardStore.GetByICCIDs()`
|
||
- [x] 8.4 运行 `lsp_diagnostics` 确认 `enterprise_card/service.go` 和 `bootstrap/services.go` 无编译错误
|
||
|
||
## 9. 整体验证
|
||
|
||
- [x] 9.1 执行 `go build ./...` 确认全量编译通过
|
||
- [x] 9.2 使用 PostgreSQL MCP 模拟上游场景验证:上游传入 19 位 ICCID,确认正确命中 19 位卡的 `is_current` 更新
|
||
- [x] 9.3 使用 PostgreSQL MCP 模拟上游场景验证:上游传入 20 位 ICCID,确认正确命中 20 位卡的 `is_current` 更新
|
||
- [x] 9.4 确认日志中 Miss 场景正确输出 Warn 日志(可通过修改测试数据触发)
|