Files
junhong_cmp_fiber/openspec/changes/archive/2026-04-09-feat-iot-card-device-virtual-no/design.md
huang e9df1e7ded
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m28s
feat: IoT卡新增绑定设备虚拟号快照字段
- 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>
2026-04-09 10:50:46 +08:00

93 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 中更新卡字段无需在事务内(绑定记录已提交),失败仅影响展示字段,不影响业务一致性;设备导入在事务内批量更新效率更高。
### 决策 4Store 层新增 `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
无。所有关键决策已在探索阶段与用户确认。