All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m28s
- tb_iot_card 新增 device_virtual_no 字段,存储绑定设备的虚拟号快照 - 列表/详情接口响应新增 device_virtual_no 字段 - 列表接口 is_standalone 改为可选参数(不传返回全部卡) - 移除列表接口 virtual_no 查询参数 - 绑卡/解绑时同步更新 device_virtual_no 快照 - 设备导入时批量写入 device_virtual_no 快照 - 归档 feat-iot-card-device-virtual-no 变更提案 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
93 lines
5.0 KiB
Markdown
93 lines
5.0 KiB
Markdown
## Context
|
||
|
||
当前 `tb_iot_card` 表有 `is_standalone` 物化列(由 DB 触发器维护),`ListStandalone` 查询硬编码 `WHERE is_standalone = true`,导致绑定设备的卡不可见。设备表 `tb_device` 有 `virtual_no` 字段,设备与卡的绑定关系存于 `tb_device_sim_binding`。
|
||
|
||
`is_standalone` 的引入背景是:30M 行表上 `NOT EXISTS` 子查询造成 9s+ 延迟,物化为布尔列后降至 <500ms。本次新增 `device_virtual_no` 采用相同的物化快照思路,避免 JOIN `tb_device_sim_binding` + `tb_device` 带来的性能回退。
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- 列表/详情接口展示绑定设备的 IoT 卡
|
||
- 列表/详情响应新增 `device_virtual_no` 字段
|
||
- `is_standalone` 作为可选查询参数(不传返回全部卡)
|
||
- 移除列表接口的 `virtual_no` 查询参数
|
||
- 绑定/解绑/设备导入时同步维护 `device_virtual_no` 快照
|
||
|
||
**Non-Goals:**
|
||
- 修改路由名称(保持 `/api/admin/iot-cards/standalone`)
|
||
- 改变已绑定设备的卡不可单独分配/回收的限制
|
||
- 支持通过设备虚拟号搜索卡(`device_virtual_no` 不作为搜索条件)
|
||
- 使用 DB 触发器维护 `device_virtual_no`(应用层维护)
|
||
|
||
## Decisions
|
||
|
||
### 决策 1:快照字段存在 `tb_iot_card`,由应用层维护
|
||
|
||
**选择:** 在 `tb_iot_card` 新增 `device_virtual_no VARCHAR(100) NOT NULL DEFAULT ''`,应用层在 BindCard / UnbindCard / device_import 时写入/清空。
|
||
|
||
**备选方案:**
|
||
- **DB 触发器**:`is_standalone` 使用触发器因为纯粹基于 `tb_device_sim_binding` 状态,`device_virtual_no` 需要跨表读 `tb_device.virtual_no`,触发器可以做但调试难度高、出错难追溯。
|
||
- **每次 JOIN 查询**:不引入新列,查询时 JOIN。百万级数据下分页列表性能不可接受,违背 `is_standalone` 的设计初衷。
|
||
|
||
**结论:** 应用层快照,写入路径明确(3 处),可读性和可维护性优于触发器。
|
||
|
||
### 决策 2:`is_standalone` 改为可选过滤参数,Store 层支持
|
||
|
||
**选择:** `ListStandaloneIotCardRequest` 新增 `IsStandalone *bool`,Store 层 `ListStandalone` 仅在非 nil 时加条件,不传则不过滤。
|
||
|
||
**理由:** 最小改动,前端可按需传 `is_standalone=true` 保留原有"只看独立卡"场景。
|
||
|
||
### 决策 3:写入时机覆盖全部 3 个入口
|
||
|
||
| 入口 | 文件 | 操作 |
|
||
|------|------|------|
|
||
| `BindCard` | `internal/service/device/binding.go` | 事务外:`UPDATE tb_iot_card SET device_virtual_no = ? WHERE id = ?` |
|
||
| `UnbindCard` | `internal/service/device/binding.go` | 事务外:`UPDATE tb_iot_card SET device_virtual_no = '' WHERE id = ?` |
|
||
| 设备导入 | `internal/task/device_import.go` | 事务内:批量 `UPDATE tb_iot_card SET device_virtual_no = ? WHERE id IN (?)` |
|
||
|
||
**说明:** BindCard / UnbindCard 中更新卡字段无需在事务内(绑定记录已提交),失败仅影响展示字段,不影响业务一致性;设备导入在事务内批量更新效率更高。
|
||
|
||
### 决策 4:Store 层新增 `UpdateDeviceVirtualNo` 方法
|
||
|
||
在 `IotCardStore` 新增:
|
||
```go
|
||
// UpdateDeviceVirtualNo 更新卡的设备虚拟号快照
|
||
func (s *IotCardStore) UpdateDeviceVirtualNo(ctx context.Context, cardID uint, deviceVirtualNo string) error
|
||
// BatchUpdateDeviceVirtualNo 批量更新卡的设备虚拟号快照(设备导入时使用)
|
||
func (s *IotCardStore) BatchUpdateDeviceVirtualNo(ctx context.Context, cardIDs []uint, deviceVirtualNo string) error
|
||
```
|
||
|
||
Device Service 需要注入 `IotCardStore`(目前已有),通过该方法更新,不直接操作 GORM。
|
||
|
||
## Risks / Trade-offs
|
||
|
||
**[快照不一致]** 极端情况下(写 BindCard 成功但更新 `device_virtual_no` 失败)会出现短暂不一致。
|
||
→ 缓解:日志记录失败,不 panic;最终一致性由运维工具或重新触发绑定修复。实际概率极低(同一进程内两次 DB 写入)。
|
||
|
||
**[历史数据回填]** 迁移 SQL 执行期间可能短暂锁表(大表 UPDATE)。
|
||
→ 缓解:使用 `UPDATE ... WHERE device_virtual_no = ''` 分批或一次性(数据量视实际情况),低峰期执行。
|
||
|
||
**[Device Service 依赖 IotCardStore]** device binding.go 需要调用 IotCardStore 方法,形成跨 Service 包的 Store 依赖。
|
||
→ 接受:当前 `device.Service` 已注入 `iotCardStore`(用于绑定验证),添加新方法调用不引入新依赖。
|
||
|
||
## Migration Plan
|
||
|
||
1. 执行新迁移:`ALTER TABLE tb_iot_card ADD COLUMN device_virtual_no VARCHAR(100) NOT NULL DEFAULT ''`
|
||
2. 回填数据:
|
||
```sql
|
||
UPDATE tb_iot_card c
|
||
SET device_virtual_no = d.virtual_no
|
||
FROM tb_device_sim_binding b
|
||
JOIN tb_device d ON d.id = b.device_id
|
||
WHERE b.iot_card_id = c.id
|
||
AND b.bind_status = 1
|
||
AND b.deleted_at IS NULL
|
||
AND d.deleted_at IS NULL;
|
||
```
|
||
3. 部署新代码(BindCard / UnbindCard / device_import 均写入快照)
|
||
4. 回滚:删除列即可(`ALTER TABLE tb_iot_card DROP COLUMN device_virtual_no`),代码回滚到旧版本
|
||
|
||
## Open Questions
|
||
|
||
无。所有关键决策已在探索阶段与用户确认。
|