feat: 实现运营商模块重构,添加冗余字段优化查询性能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m16s
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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-27
|
||||
@@ -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 管理 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
|
||||
- **风险**: 如果有外部系统依赖这些字段
|
||||
- **缓解**: 已确认这些字段未被使用,可安全移除
|
||||
@@ -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` 路由组
|
||||
@@ -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** 系统返回参数校验错误
|
||||
@@ -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 写入导入任务记录
|
||||
@@ -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 记录的冗余字段
|
||||
@@ -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 文档
|
||||
@@ -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`:状态(INT,1-启用 2-禁用)
|
||||
- `carrier_type`:运营商类型(VARCHAR(20),枚举值:"CMCC" | "CUCC" | "CTCC" | "CBN")
|
||||
- `description`:运营商描述(VARCHAR(500),可选)
|
||||
- `status`:状态(INT,1-启用 0-禁用)
|
||||
- `creator`:创建人 ID(BIGINT)
|
||||
- `updater`:更新人 ID(BIGINT)
|
||||
- `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 无效
|
||||
|
||||
|
||||
@@ -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 写入导入任务记录
|
||||
|
||||
|
||||
@@ -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 记录的冗余字段
|
||||
|
||||
|
||||
Reference in New Issue
Block a user