## Context Carrier(运营商)模块当前状态: - Model 已定义于 `internal/model/carrier.go`,表名 `tb_carrier` - 被 IotCard、IotCardImportTask、PollingConfig 通过 `carrier_id` 引用 - 缺少管理接口,数据通过其他方式手动管理 - `channel_name`、`channel_code` 字段未被任何地方使用 - 查询 IotCard 时需要 JOIN Carrier 表获取 carrier_name CarrierType 固定为 4 种:CMCC(中国移动)、CUCC(中国联通)、CTCC(中国电信)、CBN(中国广电) ## Goals / Non-Goals **Goals:** - 提供完整的 Carrier 管理 API(CRUD + 状态管理) - 简化 Carrier Model,移除冗余字段 - IotCard/ImportTask 存储冗余快照,实现数据自包含 - 优化查询性能,减少不必要的 JOIN **Non-Goals:** - PollingConfig 保持 carrier_id 引用方式(配置语义,NULL 表示所有运营商) - 不修改 Carrier 的业务逻辑(仅提供管理接口) - 不支持 CarrierType 动态扩展(固定 4 种) ## Decisions ### D1: 冗余快照 vs 保持引用 **决策**: IotCard/ImportTask 采用冗余快照存储 carrier_type、carrier_name **理由**: - 查询时无需 JOIN,性能更好 - Carrier 软删除后历史数据完整 - 符合"赋予时刻快照"的业务语义——卡分配后运营商信息不应变化 **备选方案**: - 保持纯引用:需要限制 Carrier 删除,查询需要 JOIN - 软引用 + 视图:复杂度高,维护成本大 ### D2: PollingConfig 不添加冗余字段 **决策**: PollingConfig 保持 carrier_id 引用 **理由**: - PollingConfig 是配置表,`carrier_id = NULL` 有特殊含义(所有运营商) - 配置应该跟随 Carrier 变化,不需要历史快照 - 场景不同于数据记录 ### D3: CarrierType 枚举校验 **决策**: 创建时 carrier_type 必须是 CMCC/CUCC/CTCC/CBN 之一,使用 validator 枚举校验 **理由**: - 运营商类型固定,不会动态扩展 - 前端下拉选择,后端强校验 - 避免脏数据 ### D4: 删除策略 **决策**: Carrier 可以软删除,不检查关联数据 **理由**: - IotCard 已有冗余字段,不依赖 Carrier 表 - ImportTask 同理 - PollingConfig 的 carrier_id 可能变成悬空引用,但配置场景可接受(管理员负责) ### D5: API 路由设计 **决策**: 遵循项目现有 RESTful 风格 ``` POST /api/admin/carriers 创建 GET /api/admin/carriers 列表(分页+筛选) GET /api/admin/carriers/:id 详情 PUT /api/admin/carriers/:id 更新 DELETE /api/admin/carriers/:id 软删除 PUT /api/admin/carriers/:id/status 启用/禁用 ``` ## Risks / Trade-offs ### R1: 数据冗余导致存储增加 - **风险**: 每条 IotCard 多存 carrier_type(20B) + carrier_name(100B) - **缓解**: 可接受的存储开销,换取查询性能和数据独立性 ### R2: 迁移时需要填充历史数据 - **风险**: 现有 IotCard/ImportTask 记录需要通过 UPDATE 填充冗余字段 - **缓解**: 迁移脚本从 Carrier 表 JOIN 填充,一次性操作 ### R3: carrier_code/carrier_type 创建后不可修改 - **风险**: 如果创建错误,只能删除重建 - **缓解**: 前端选择 + 后端校验,降低出错概率;错误情况下软删除后重建 ### R4: 移除 channel 字段是 BREAKING CHANGE - **风险**: 如果有外部系统依赖这些字段 - **缓解**: 已确认这些字段未被使用,可安全移除