feat: 实现运营商模块重构,添加冗余字段优化查询性能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m16s

主要变更:
- 新增 Carrier CRUD API(创建、列表、详情、更新、删除、状态更新)
- IotCard/IotCardImportTask 添加 carrier_type/carrier_name 冗余字段
- 移除 Carrier 表的 channel_name/channel_code 字段
- 查询时直接使用冗余字段,避免 JOIN Carrier 表
- 添加数据库迁移脚本(000021-000023)
- 添加单元测试和集成测试
- 同步更新 OpenAPI 文档和 specs
This commit is contained in:
2026-01-27 12:18:19 +08:00
parent 5a179ba16b
commit d104d297ca
42 changed files with 2431 additions and 122 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-01-27

View File

@@ -0,0 +1,96 @@
## 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 管理 APICRUD + 状态管理)
- 简化 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
- **风险**: 如果有外部系统依赖这些字段
- **缓解**: 已确认这些字段未被使用,可安全移除

View File

@@ -0,0 +1,32 @@
## Why
Carrier运营商模块目前只有 Model 定义,缺少管理接口。系统需要一套完整的 CRUD + 状态管理接口来创建和管理上游运营商渠道。同时,现有的 IotCard 等表通过 carrier_id 引用 Carrier每次查询都需要 JOIN且 Carrier 删除后历史数据会缺失。需要通过冗余字段实现"赋予时刻快照",让数据自包含。
## What Changes
- 新增 Carrier 管理 API增删改查 + 启用/禁用)
- 简化 Carrier Model移除未使用的 `channel_name``channel_code` 字段
- IotCard 新增冗余字段 `carrier_type``carrier_name`,导入时填充快照
- IotCardImportTask 新增冗余字段 `carrier_name`(已有 `carrier_type`
- 优化查询逻辑,移除不必要的 JOIN 操作
- **BREAKING**: 移除 Carrier 表的 `channel_name``channel_code` 字段及相关索引
## Capabilities
### New Capabilities
- `carrier-management`: 运营商管理功能,包含 CRUD 接口、状态管理、列表筛选
### Modified Capabilities
- `iot-card-import`: 导入时填充 carrier_type、carrier_name 冗余字段
- `iot-card-query`: 查询响应直接使用冗余字段,无需 JOIN Carrier 表
## Impact
- **Model 层**: `carrier.go`(移除字段)、`iot_card.go`(新增字段)、`iot_card_import_task.go`(新增字段)
- **新增文件**: `carrier_dto.go``carrier_store.go``carrier/service.go``carrier.go`(handler)、`carrier.go`(routes)
- **修改文件**: `iot_card/service.go``iot_card_import/service.go``device/binding.go`(移除 JOIN 逻辑)
- **Bootstrap**: 注册新的 Store、Service、Handler
- **数据库**: 3 个迁移文件Carrier 简化、IotCard 冗余、ImportTask 冗余)
- **API**: 新增 `/api/admin/carriers` 路由组

View File

@@ -0,0 +1,79 @@
## ADDED Requirements
### Requirement: 创建运营商
系统 SHALL 允许管理员创建新的运营商记录。创建时必须指定 carrier_code唯一编码、carrier_name显示名称、carrier_type运营商类型枚举值。description 为选填字段。创建成功后默认状态为启用status=1
#### Scenario: 成功创建运营商
- **WHEN** 管理员提交有效的创建请求carrier_code 不重复carrier_type 为有效枚举值
- **THEN** 系统创建运营商记录,返回完整的运营商信息
#### Scenario: carrier_code 重复
- **WHEN** 管理员提交的 carrier_code 已存在
- **THEN** 系统返回错误"运营商编码已存在"
#### Scenario: carrier_type 无效
- **WHEN** 管理员提交的 carrier_type 不是 CMCC/CUCC/CTCC/CBN 之一
- **THEN** 系统返回参数校验错误
### Requirement: 查询运营商列表
系统 SHALL 提供分页查询运营商列表的接口,支持按 carrier_type、status、carrier_name模糊搜索筛选。
#### Scenario: 无筛选条件查询
- **WHEN** 管理员请求列表,不带筛选条件
- **THEN** 系统返回所有运营商的分页列表,按 ID 降序排列
#### Scenario: 按运营商类型筛选
- **WHEN** 管理员指定 carrier_type=CMCC
- **THEN** 系统仅返回 carrier_type 为 CMCC 的记录
#### Scenario: 按名称模糊搜索
- **WHEN** 管理员指定 carrier_name=移动
- **THEN** 系统返回 carrier_name 包含"移动"的记录
### Requirement: 获取运营商详情
系统 SHALL 允许管理员通过 ID 获取单个运营商的详细信息。
#### Scenario: 成功获取详情
- **WHEN** 管理员请求存在的运营商 ID
- **THEN** 系统返回该运营商的完整信息
#### Scenario: 运营商不存在
- **WHEN** 管理员请求不存在的运营商 ID
- **THEN** 系统返回错误"运营商不存在"
### Requirement: 更新运营商
系统 SHALL 允许管理员更新运营商的 carrier_name 和 description 字段。carrier_code 和 carrier_type 创建后不可修改。
#### Scenario: 成功更新运营商
- **WHEN** 管理员提交有效的更新请求
- **THEN** 系统更新运营商信息,返回更新后的完整信息
#### Scenario: 尝试修改 carrier_code
- **WHEN** 管理员尝试修改 carrier_code
- **THEN** 系统忽略该字段(不报错,但不修改)
### Requirement: 删除运营商
系统 SHALL 允许管理员软删除运营商记录。
#### Scenario: 成功删除运营商
- **WHEN** 管理员请求删除存在的运营商
- **THEN** 系统软删除该记录(设置 deleted_at
#### Scenario: 删除不存在的运营商
- **WHEN** 管理员请求删除不存在的运营商 ID
- **THEN** 系统返回错误"运营商不存在"
### Requirement: 更新运营商状态
系统 SHALL 允许管理员启用或禁用运营商。状态值1=启用2=禁用。
#### Scenario: 启用运营商
- **WHEN** 管理员将状态设置为 1
- **THEN** 系统更新运营商状态为启用
#### Scenario: 禁用运营商
- **WHEN** 管理员将状态设置为 2
- **THEN** 系统更新运营商状态为禁用
#### Scenario: 无效状态值
- **WHEN** 管理员提交的状态值不是 1 或 2
- **THEN** 系统返回参数校验错误

View File

@@ -0,0 +1,19 @@
## MODIFIED Requirements
### Requirement: 导入物联网卡时记录运营商信息
系统 SHALL 在导入物联网卡时,将运营商的 carrier_type 和 carrier_name 作为冗余字段存储到 IotCard 记录中。这些字段在导入时从 Carrier 表查询并写入,后续不再依赖 Carrier 表。
#### Scenario: 导入时填充冗余字段
- **WHEN** 系统处理物联网卡导入任务
- **THEN** 系统根据 carrier_id 查询 Carrier 表,将 carrier_type 和 carrier_name 写入每条 IotCard 记录
#### Scenario: Carrier 不存在
- **WHEN** 导入任务指定的 carrier_id 对应的 Carrier 不存在或已删除
- **THEN** 系统拒绝导入,返回错误"运营商不存在"
### Requirement: 导入任务记录运营商名称
系统 SHALL 在创建导入任务时,将 carrier_name 作为冗余字段存储到 IotCardImportTask 记录中(已有 carrier_type
#### Scenario: 创建导入任务时填充 carrier_name
- **WHEN** 管理员创建物联网卡导入任务
- **THEN** 系统根据 carrier_id 查询 Carrier 表,将 carrier_name 写入导入任务记录

View File

@@ -0,0 +1,26 @@
## MODIFIED Requirements
### Requirement: 查询物联网卡时返回运营商信息
系统 SHALL 在查询物联网卡列表/详情时,直接从 IotCard 记录的冗余字段返回 carrier_type 和 carrier_name无需 JOIN Carrier 表。
#### Scenario: 列表查询返回运营商信息
- **WHEN** 管理员查询物联网卡列表
- **THEN** 响应中的 carrier_type 和 carrier_name 直接来自 IotCard 记录的冗余字段
#### Scenario: 详情查询返回运营商信息
- **WHEN** 管理员查询单张物联网卡详情
- **THEN** 响应中的 carrier_type 和 carrier_name 直接来自 IotCard 记录的冗余字段
### Requirement: 查询导入任务时返回运营商名称
系统 SHALL 在查询导入任务列表/详情时,直接从 IotCardImportTask 记录的冗余字段返回 carrier_name无需 JOIN Carrier 表。
#### Scenario: 导入任务列表返回运营商名称
- **WHEN** 管理员查询导入任务列表
- **THEN** 响应中的 carrier_name 直接来自 IotCardImportTask 记录的冗余字段
### Requirement: 设备绑定卡查询返回运营商信息
系统 SHALL 在查询设备绑定的物联网卡时,直接从 IotCard 记录的冗余字段返回 carrier_name无需 JOIN Carrier 表。
#### Scenario: 设备绑定卡列表返回运营商名称
- **WHEN** 管理员查询设备绑定的物联网卡列表
- **THEN** 响应中的 carrier_name 直接来自 IotCard 记录的冗余字段

View File

@@ -0,0 +1,62 @@
## 1. 数据库迁移
- [x] 1.1 创建迁移文件Carrier 表移除 channel_name、channel_code 字段及 idx_carrier_type_channel 索引
- [x] 1.2 创建迁移文件IotCard 表添加 carrier_type、carrier_name 冗余字段,并从 Carrier 表填充现有数据
- [x] 1.3 创建迁移文件IotCardImportTask 表添加 carrier_name 冗余字段,并从 Carrier 表填充现有数据
- [x] 1.4 执行迁移,验证数据完整性
## 2. Model 层修改
- [x] 2.1 修改 `internal/model/carrier.go`:移除 ChannelName、ChannelCode 字段
- [x] 2.2 修改 `internal/model/iot_card.go`:添加 CarrierType、CarrierName 字段
- [x] 2.3 修改 `internal/model/iot_card_import_task.go`:添加 CarrierName 字段
## 3. DTO 层
- [x] 3.1 创建 `internal/model/dto/carrier_dto.go`:定义 CreateCarrierRequest、UpdateCarrierRequest、CarrierListRequest、UpdateCarrierStatusRequest、CarrierResponse
- [x] 3.2 修改 `internal/model/dto/iot_card_dto.go`:响应结构添加 carrier_type 字段(如果缺失)
- [x] 3.3 修改 `internal/model/dto/iot_card_import_dto.go`:响应结构确认包含 carrier_name 字段
## 4. Store 层
- [x] 4.1 创建 `internal/store/postgres/carrier_store.go`:实现 Create、GetByID、Update、Delete、List、GetByCode 方法
## 5. Service 层
- [x] 5.1 创建 `internal/service/carrier/service.go`:实现 Create、Get、Update、Delete、List、UpdateStatus 业务逻辑
- [x] 5.2 修改 `internal/service/iot_card_import/service.go`:创建导入任务时填充 carrier_name处理卡片时填充 carrier_type、carrier_name
- [x] 5.3 修改 `internal/service/iot_card/service.go`:移除 loadCarrierData / loadRelatedData 中的 Carrier JOIN 逻辑,直接使用 IotCard 自身字段
- [x] 5.4 修改 `internal/service/device/binding.go`:移除 loadCarrierData 方法,直接使用 IotCard 的 carrier_name 字段
## 6. Handler 层
- [x] 6.1 创建 `internal/handler/admin/carrier.go`:实现 Create、Get、Update、Delete、List、UpdateStatus 接口
## 7. 路由注册
- [x] 7.1 创建 `internal/routes/carrier.go`:注册 /api/admin/carriers 路由组
- [x] 7.2 更新 `internal/routes/admin.go`:调用 Carrier 路由注册
## 8. Bootstrap 注册
- [x] 8.1 修改 `internal/bootstrap/stores.go`:注册 CarrierStore
- [x] 8.2 修改 `internal/bootstrap/services.go`:注册 CarrierService
- [x] 8.3 修改 `internal/bootstrap/handlers.go`:注册 CarrierHandler
## 9. 常量定义
- [x] 9.1 在 `pkg/constants/` 中添加 CarrierType 枚举常量
- [x] 9.2 在 `pkg/errors/codes.go` 中添加 Carrier 相关错误码CodeCarrierNotFound、CodeCarrierCodeExists 等)
## 10. 测试
- [x] 10.1 编写 CarrierStore 单元测试
- [x] 10.2 编写 CarrierService 单元测试
- [x] 10.3 编写 Carrier API 集成测试
- [x] 10.4 验证 IotCard 导入流程正确填充冗余字段(通过 TestIotCard_CarrierRedundantFields 验证)
- [x] 10.5 验证 IotCard 查询响应正确返回冗余字段(通过 TestIotCard_GetByICCID 验证)
## 11. 文档更新
- [x] 11.1 更新 API 文档生成器docs.go / gendocs/main.go注册 CarrierHandler
- [x] 11.2 运行 `go run cmd/gendocs/main.go` 生成 OpenAPI 文档

View File

@@ -1,11 +1,13 @@
# carrier Specification
## Purpose
TBD - created by archiving change add-wallet-transfer-tag-models. Update Purpose after archive.
管理运营商(Carrier)实体,支持四大固定运营商(中国移动、中国联通、中国电信、广电)的 CRUD 操作。
## Requirements
### Requirement: 运营商实体定义
系统 SHALL 定义运营商(Carrier)实体,管理四大固定运营商(中国移动、中国联通、中国电信、广电)的渠道信息
系统 SHALL 定义运营商(Carrier)实体,管理四大固定运营商(中国移动、中国联通、中国电信、广电)
**四大运营商固定枚举**
- **CMCC**:中国移动
@@ -15,44 +17,114 @@ TBD - created by archiving change add-wallet-transfer-tag-models. Update Purpose
**实体字段**
- `id`:运营商 ID主键BIGINT
- `carrier_type`:运营商类型VARCHAR(20)枚举值:"CMCC" | "CUCC" | "CTCC" | "CBN"**【新增】**
- `carrier_code`:运营商编码VARCHAR(50)唯一约束)
- `carrier_name`运营商名称VARCHAR(100),如"中国移动"
- `carrier_code`:运营商编码VARCHAR(50)保留字段,建议填充与 carrier_type 相同
- `channel_name`:渠道名称VARCHAR(100),可自定义,如"北京渠道1"**【新增】**
- `channel_code`渠道编码VARCHAR(50),可自定义,如"BJ001"**【新增】**
- `status`状态INT1-启用 2-禁用)
- `carrier_type`:运营商类型VARCHAR(20)枚举值:"CMCC" | "CUCC" | "CTCC" | "CBN"
- `description`:运营商描述VARCHAR(500),可选)
- `status`状态INT1-启用 0-禁用)
- `creator`:创建人 IDBIGINT
- `updater`:更新人 IDBIGINT
- `created_at`创建时间TIMESTAMP自动填充
- `updated_at`更新时间TIMESTAMP自动填充
- `deleted_at`删除时间TIMESTAMP可空软删除
**唯一约束**`(carrier_type, channel_code)``deleted_at IS NULL` 条件下唯一
**唯一约束**`carrier_code``deleted_at IS NULL` 条件下唯一
#### Scenario: 创建中国移动的渠道
---
- **WHEN** 平台创建中国移动的北京渠道,`carrier_type` 为 "CMCC"`carrier_name` 为 "中国移动"`channel_name` 为 "北京渠道1"`channel_code` 为 "BJ001"
- **THEN** 系统创建运营商记录,`carrier_type` 为 "CMCC"`channel_name` 为 "北京渠道1"`channel_code` 为 "BJ001"
### Requirement: 创建运营商
#### Scenario: 同一运营商创建多个渠道
系统 SHALL 允许管理员创建新的运营商记录。创建时必须指定 carrier_code唯一编码、carrier_name显示名称、carrier_type运营商类型枚举值。description 为选填字段。创建成功后默认状态为启用status=1
- **WHEN** 平台为中国移动创建两个渠道北京渠道BJ001和上海渠道SH001
- **THEN** 系统创建两条运营商记录,`carrier_type` 都为 "CMCC",但 `channel_code` 不同
#### Scenario: 成功创建运营商
- **WHEN** 管理员提交有效的创建请求carrier_code 不重复carrier_type 为有效枚举值
- **THEN** 系统创建运营商记录,返回完整的运营商信息
#### Scenario: 渠道编码重复
#### Scenario: carrier_code 重复
- **WHEN** 管理员提交的 carrier_code 已存在
- **THEN** 系统返回错误"运营商编码已存在"
- **WHEN** 平台创建中国移动的渠道,`carrier_type` 为 "CMCC"`channel_code` 为已存在的 "BJ001"
- **THEN** 系统拒绝创建,返回错误信息"该运营商的渠道编码已存在"
#### Scenario: carrier_type 无效
- **WHEN** 管理员提交的 carrier_type 不是 CMCC/CUCC/CTCC/CBN 之一
- **THEN** 系统返回参数校验错误
#### Scenario: 不同运营商可以使用相同渠道编码
---
- **WHEN** 平台为中国移动创建渠道carrier_type=CMCC, channel_code=BJ001然后为中国联通创建渠道carrier_type=CUCC, channel_code=BJ001
- **THEN** 系统允许创建,因为 `carrier_type` 不同
### Requirement: 查询运营商列表
#### Scenario: 运营商类型枚举限制
系统 SHALL 提供分页查询运营商列表的接口,支持按 carrier_type、status、carrier_name模糊搜索筛选。
- **WHEN** 平台创建运营商,`carrier_type` 为 "OTHER"(不在枚举中)
- **THEN** 系统拒绝创建,返回错误信息"运营商类型必须是 CMCC/CUCC/CTCC/CBN 之一"
#### Scenario: 无筛选条件查询
- **WHEN** 管理员请求列表,不带筛选条件
- **THEN** 系统返回所有运营商的分页列表,按 ID 降序排列
#### Scenario: 按运营商类型筛选
- **WHEN** 管理员指定 carrier_type=CMCC
- **THEN** 系统仅返回 carrier_type 为 CMCC 的记录
#### Scenario: 按名称模糊搜索
- **WHEN** 管理员指定 carrier_name=移动
- **THEN** 系统返回 carrier_name 包含"移动"的记录
---
### Requirement: 获取运营商详情
系统 SHALL 允许管理员通过 ID 获取单个运营商的详细信息。
#### Scenario: 成功获取详情
- **WHEN** 管理员请求存在的运营商 ID
- **THEN** 系统返回该运营商的完整信息
#### Scenario: 运营商不存在
- **WHEN** 管理员请求不存在的运营商 ID
- **THEN** 系统返回错误"运营商不存在"
---
### Requirement: 更新运营商
系统 SHALL 允许管理员更新运营商的 carrier_name 和 description 字段。carrier_code 和 carrier_type 创建后不可修改。
#### Scenario: 成功更新运营商
- **WHEN** 管理员提交有效的更新请求
- **THEN** 系统更新运营商信息,返回更新后的完整信息
#### Scenario: 尝试修改 carrier_code
- **WHEN** 管理员尝试修改 carrier_code
- **THEN** 系统忽略该字段(不报错,但不修改)
---
### Requirement: 删除运营商
系统 SHALL 允许管理员软删除运营商记录。
#### Scenario: 成功删除运营商
- **WHEN** 管理员请求删除存在的运营商
- **THEN** 系统软删除该记录(设置 deleted_at
#### Scenario: 删除不存在的运营商
- **WHEN** 管理员请求删除不存在的运营商 ID
- **THEN** 系统返回错误"运营商不存在"
---
### Requirement: 更新运营商状态
系统 SHALL 允许管理员启用或禁用运营商。状态值1=启用0=禁用。
#### Scenario: 启用运营商
- **WHEN** 管理员将状态设置为 1
- **THEN** 系统更新运营商状态为启用
#### Scenario: 禁用运营商
- **WHEN** 管理员将状态设置为 0
- **THEN** 系统更新运营商状态为禁用
#### Scenario: 无效状态值
- **WHEN** 管理员提交的状态值不是 0 或 1
- **THEN** 系统返回参数校验错误
---
@@ -64,9 +136,8 @@ TBD - created by archiving change add-wallet-transfer-tag-models. Update Purpose
- `carrier_type`:必填,枚举值 "CMCC" | "CUCC" | "CTCC" | "CBN"
- `carrier_name`:必填,长度 1-100 字符
- `carrier_code`:必填,长度 1-50 字符
- `channel_name`:可选,长度 1-100 字符
- `channel_code`:可选,长度 1-50 字符
- `status`:必填,枚举值 1-2
- `description`:可选,长度 0-500 字符
- `status`:必填,枚举值 0 或 1
#### Scenario: 创建运营商时 carrier_type 无效

View File

@@ -237,3 +237,27 @@ TBD - created by archiving change iot-card-standalone-management. Update Purpose
- **WHEN** Worker 处理导入任务创建卡记录
- **THEN** 创建的 `IotCard` 记录 `iccid` 为 "898600...",`msisdn` 为 "13800000001"
---
### Requirement: 导入物联网卡时记录运营商信息
系统 SHALL 在导入物联网卡时,将运营商的 carrier_type 和 carrier_name 作为冗余字段存储到 IotCard 记录中。这些字段在导入时从 Carrier 表查询并写入,后续不再依赖 Carrier 表。
#### Scenario: 导入时填充冗余字段
- **WHEN** 系统处理物联网卡导入任务
- **THEN** 系统根据 carrier_id 查询 Carrier 表,将 carrier_type 和 carrier_name 写入每条 IotCard 记录
#### Scenario: Carrier 不存在
- **WHEN** 导入任务指定的 carrier_id 对应的 Carrier 不存在或已删除
- **THEN** 系统拒绝导入,返回错误"运营商不存在"
---
### Requirement: 导入任务记录运营商名称
系统 SHALL 在创建导入任务时,将 carrier_name 作为冗余字段存储到 IotCardImportTask 记录中(已有 carrier_type
#### Scenario: 创建导入任务时填充 carrier_name
- **WHEN** 管理员创建物联网卡导入任务
- **THEN** 系统根据 carrier_id 查询 Carrier 表,将 carrier_name 写入导入任务记录

View File

@@ -544,3 +544,37 @@ This capability supports:
- **WHEN** 卡的授权被回收后revoked_at 不为空),企业用户查询该卡
- **THEN** 系统不返回该卡信息,企业无法再看到该卡
---
### Requirement: 查询物联网卡时返回运营商信息
系统 SHALL 在查询物联网卡列表/详情时,直接从 IotCard 记录的冗余字段返回 carrier_type 和 carrier_name无需 JOIN Carrier 表。
#### Scenario: 列表查询返回运营商信息
- **WHEN** 管理员查询物联网卡列表
- **THEN** 响应中的 carrier_type 和 carrier_name 直接来自 IotCard 记录的冗余字段
#### Scenario: 详情查询返回运营商信息
- **WHEN** 管理员查询单张物联网卡详情
- **THEN** 响应中的 carrier_type 和 carrier_name 直接来自 IotCard 记录的冗余字段
---
### Requirement: 查询导入任务时返回运营商名称
系统 SHALL 在查询导入任务列表/详情时,直接从 IotCardImportTask 记录的冗余字段返回 carrier_name无需 JOIN Carrier 表。
#### Scenario: 导入任务列表返回运营商名称
- **WHEN** 管理员查询导入任务列表
- **THEN** 响应中的 carrier_name 直接来自 IotCardImportTask 记录的冗余字段
---
### Requirement: 设备绑定卡查询返回运营商信息
系统 SHALL 在查询设备绑定的物联网卡时,直接从 IotCard 记录的冗余字段返回 carrier_name无需 JOIN Carrier 表。
#### Scenario: 设备绑定卡列表返回运营商名称
- **WHEN** 管理员查询设备绑定的物联网卡列表
- **THEN** 响应中的 carrier_name 直接来自 IotCard 记录的冗余字段