Files
junhong_cmp_fiber/openspec/specs/iot-card-import-task/spec.md
huang 80c6f6c756
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
主要变更:
- 新增 AssetIdentifier 模型及 Store,统一管理资产标识符(ICCID/IMEI/SN 等)
- 新增迁移:asset_identifier 表、order 表新增 asset_identifier 字段、iot_card.virtual_no NOT NULL 约束
- 资产 Handler/Service/Route 全面重构,支持标识符路由查询与解析
- 新增资产历史订单查询接口,支持跨设备/卡/钱包维度的订单聚合
- 设备与物联卡导入任务强制校验虚拟号,缺失时直接拒绝
- Excel 工具函数优化,前端导入指引文档同步更新
- 归档三个 OpenSpec 提案:asset-identifier-standardization、asset-historical-orders、import-mandatory-virtual-no
- 更新 OpenAPI 文档及相关 DTO

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-07 17:39:36 +08:00

327 lines
13 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.
# iot-card-import-task Specification
## Purpose
TBD - created by archiving change iot-card-standalone-management. Update Purpose after archive.
## Requirements
### Requirement: 导入任务实体定义
系统 SHALL 定义 IoT 卡导入任务(IotCardImportTask)实体,用于跟踪 IoT 卡批量导入的进度和结果。
**实体字段**:
**任务信息**:
- `id`: 任务 ID(主键,BIGINT)
- `task_no`: 任务编号(VARCHAR(50),唯一,格式: IMP-YYYYMMDD-XXXXXX)
- `status`: 任务状态(INT,1-待处理 2-处理中 3-已完成 4-失败)
**导入参数**:
- `carrier_id`: 运营商 ID(BIGINT,必填)
- `carrier_type`: 运营商类型(VARCHAR(10),CMCC/CUCC/CTCC/CBN)
- `batch_no`: 批次号(VARCHAR(100),可选)
- `file_name`: 原始文件名(VARCHAR(255),可选)
**待导入数据**:
- `card_list`: 待导入卡列表(JSONB,结构: [{iccid, msisdn}],替代原 iccid_list)
**进度统计**:
- `total_count`: 总数(INT,CSV 文件总行数)
- `success_count`: 成功数(INT,成功导入的卡数量)
- `skip_count`: 跳过数(INT,因重复等原因跳过的数量)
- `fail_count`: 失败数(INT,因格式错误等原因失败的数量)
**结果详情**:
- `skipped_items`: 跳过记录详情(JSONB,结构: [{line, iccid, msisdn, reason}])
- `failed_items`: 失败记录详情(JSONB,结构: [{line, iccid, msisdn, reason}])
**时间和错误**:
- `started_at`: 开始处理时间(TIMESTAMP,可空)
- `completed_at`: 完成时间(TIMESTAMP,可空)
- `error_message`: 任务级错误信息(TEXT,可空,如文件解析失败等)
**系统字段**:
- `shop_id`: 店铺 ID(BIGINT,可空,记录发起导入的店铺)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
- `creator`: 创建人 ID(BIGINT)
- `updater`: 更新人 ID(BIGINT)
#### Scenario: 创建导入任务
- **GIVEN** 管理员上传包含 ICCID 和 MSISDN 两列的 CSV 文件
- **WHEN** 系统解析 CSV 并创建导入任务
- **THEN** 系统创建导入任务记录,`card_list` 包含 [{iccid, msisdn}] 结构,`status` 为 1(待处理)
---
### Requirement: 导入任务状态流转
系统 SHALL 管理导入任务的状态流转,确保状态变更符合业务规则。
**状态定义**:
- **1-待处理**: 任务已创建,等待 Worker 处理
- **2-处理中**: Worker 正在处理导入
- **3-已完成**: 导入处理完成(可能有部分失败)
- **4-失败**: 任务级别错误,导入中断
**状态流转规则**:
- 待处理(1) → 处理中(2): Worker 开始处理
- 处理中(2) → 已完成(3): 处理完成
- 处理中(2) → 失败(4): 发生严重错误
- 待处理(1) → 失败(4): 文件验证失败等
#### Scenario: 正常状态流转
- **WHEN** 导入任务经历完整生命周期
- **THEN** 状态依次变更: 待处理(1) → 处理中(2) → 已完成(3)
#### Scenario: 异常状态流转
- **WHEN** 导入任务处理过程中发生严重错误
- **THEN** 状态变更: 待处理(1) → 处理中(2) → 失败(4)
---
### Requirement: 导入任务创建权限控制
系统 SHALL 仅允许超级管理员与平台用户创建 IoT 卡导入任务。
#### Scenario: 平台用户创建导入任务
- **WHEN** 平台用户请求创建导入任务
- **THEN** 系统创建导入任务并返回任务信息
#### Scenario: 非平台用户创建导入任务被拒绝
- **WHEN** 非平台用户(代理账号/企业账号等)请求创建导入任务
- **THEN** 系统返回 403Forbidden并返回统一错误码 `CodeForbidden`
---
### Requirement: 导入任务列表查询
系统 SHALL 支持查询导入任务列表,用于管理和监控导入任务。
**查询条件**:
- 任务状态(status): 可选,1-待处理 2-处理中 3-已完成 4-失败
- 运营商 ID(carrier_id): 可选
- 批次号(batch_no): 可选,模糊匹配
- 创建时间范围: 可选
**分页**:
- 默认每页 20 条,最大每页 100 条
- 默认按创建时间倒序排列
**权限**:
- 仅超级管理员/平台用户可查询导入任务列表
#### Scenario: 查询所有导入任务
- **WHEN** 平台管理员查询导入任务列表
- **THEN** 系统返回导入任务列表,包含任务编号、状态、运营商、总数、成功数、跳过数、失败数、创建时间
#### Scenario: 按状态筛选导入任务
- **WHEN** 平台管理员查询状态为 2(处理中) 的导入任务
- **THEN** 系统返回所有正在处理的导入任务列表
#### Scenario: 非平台用户查询导入任务列表被拒绝
- **WHEN** 非平台用户(代理账号/企业账号等)查询导入任务列表
- **THEN** 系统返回 403Forbidden并返回统一错误码 `CodeForbidden`
---
### Requirement: 导入任务详情查询
系统 SHALL 支持查询单个导入任务的详细信息,包括跳过/失败记录详情。
**详情信息**:
- 任务基本信息: 任务编号、状态、运营商、批次号、文件名
- 进度统计: 总数、成功数、跳过数、失败数
- 时间信息: 创建时间、开始时间、完成时间
- 跳过记录详情: 行号、ICCID、原因
- 失败记录详情: 行号、ICCID、原因
- 错误信息: 任务级错误(如有)
**权限**:
- 仅超级管理员/平台用户可查询导入任务详情
#### Scenario: 查询导入任务详情
- **WHEN** 平台管理员查询导入任务(ID 为 1)的详情
- **THEN** 系统返回任务完整信息,包括跳过和失败记录的详细列表
#### Scenario: 非平台用户查询导入任务详情被拒绝
- **WHEN** 非平台用户(代理账号/企业账号等)查询导入任务详情
- **THEN** 系统返回 403Forbidden并返回统一错误码 `CodeForbidden`
#### Scenario: 查询导入任务的跳过记录
- **WHEN** 管理员查询导入任务(ID 为 1)的跳过记录
- **THEN** 系统返回跳过记录列表,每条包含: 行号(line)、ICCID、原因(如"ICCID 已存在")
#### Scenario: 查询导入任务的失败记录
- **WHEN** 管理员查询导入任务(ID 为 1)的失败记录
- **THEN** 系统返回失败记录列表,每条包含: 行号(line)、ICCID、原因(如"电信 ICCID 必须为 19 位")
---
### Requirement: 导入任务数据校验
系统 SHALL 对导入任务数据进行校验,确保数据完整性和一致性。
**校验规则**:
- 任务编号(task_no): 必填,系统自动生成,格式 IMP-YYYYMMDD-XXXXXX,唯一
- 任务状态(status): 必填,枚举值 1(待处理) | 2(处理中) | 3(已完成) | 4(失败)
- 运营商 ID(carrier_id): 必填,必须是有效的运营商 ID
- 总数(total_count): 必填,≥ 0
- 成功数(success_count): 必填,≥ 0,≤ total_count
- 跳过数(skip_count): 必填,≥ 0,≤ total_count
- 失败数(fail_count): 必填,≥ 0,≤ total_count
- 数量一致性: success_count + skip_count + fail_count ≤ total_count
#### Scenario: 创建任务时运营商 ID 无效
- **WHEN** 创建导入任务时 carrier_id 不存在
- **THEN** 系统拒绝创建,返回错误信息"运营商 ID 无效"
#### Scenario: 更新任务时数量不一致
- **WHEN** 更新导入任务时 success_count + skip_count + fail_count > total_count
- **THEN** 系统拒绝更新,返回错误信息"统计数量不一致"
### Requirement: Excel 文件格式规范
系统 SHALL 要求 Excel 文件必须包含 ICCID 和 MSISDN 两列,并支持可选的 `virtual_no` 列。
**文件格式要求**:
- **文件格式**: 仅支持 `.xlsx` (Excel 2007+)
- **Sheet**: 读取第一个sheet或优先读取名为"导入数据"的sheet
- **表头行**: 第1行可选但建议包含
- **表头识别关键字**:
- ICCID 列: iccid/ICCID/卡号/号码
- MSISDN 列: msisdn/MSISDN/接入号/手机号/电话/号码
- virtual_no 列(**必填**: virtual_no/VirtualNo/虚拟号/设备号
- **列数要求**: 至少 3 列ICCID、MSISDN、virtual_novirtual_no 为必填列
- **列格式**: 应设置为文本格式(避免长数字被转为科学记数法)
**解析规则**:
- 自动检测表头第1行包含识别关键字则跳过
- 自动去除单元格首尾空格
- 跳过空行
- ICCID 为空的行记录为失败
- MSISDN 为空的行记录为失败
- virtual_no 为空的行:**[BREAKING]** 记录为失败行,失败原因为"虚拟号(virtual_no)不能为空"(原行为为跳过该列)
- Excel 中不包含 virtual_no 列时:所有数据行均因 VirtualNo 为空而失败,返回错误提示"Excel 文件缺少 virtual_no 列,请使用最新模板"
**virtual_no 导入规则**:
- virtual_no 为必填列,为空则该行记录为失败
- virtual_no 全局唯一(跨卡和设备),重复则失败,原因为"虚拟号已被占用: <值>"
- **批次级唯一性检查**:在执行导入前,先检查整批中所有 virtual_no 是否与数据库现存值重复;有任意冲突则**整批失败**,响应中返回冲突的 virtual_no 及行号列表
**示例 Excel 内容**:
```
| ICCID | MSISDN | 虚拟号 |
|----------------------|-------------|-----------|
| 89860012345678901234 | 13800000001 | CARD-001 |
| 89860012345678901235 | 13800000002 | |
```
#### Scenario: 正常导入ICCID 和 VirtualNo 均有值)
- **WHEN** Excel 中某行 ICCID="898600XXXXX"virtual_no="CARD-001"msisdn="13800000001"
- **THEN** 导入成功卡记录写入数据库VirtualNo 和 ICCID 同步注册到 `tb_asset_identifier`
#### Scenario: VirtualNo 为空的行被拒绝
- **WHEN** Excel 中某行 ICCID="898600YYYYY"virtual_no 列为空或未填写
- **THEN** 该行计入失败,失败原因为"虚拟号(virtual_no)不能为空"
- **THEN** 其他合法行继续导入,不因此行中断
#### Scenario: VirtualNo 重复被拒绝
- **WHEN** Excel 中某行的 virtual_no 与已有卡/设备的 VirtualNo 重复(跨表)
- **THEN** 该行计入失败,原因为"虚拟号已被占用: <值>"
#### Scenario: Excel 文件中无 VirtualNo 列
- **WHEN** Excel 表头中不包含 `virtual_no`/`虚拟号`/`设备号` 等可识别列名
- **THEN** 所有数据行均因 VirtualNo 为空而失败,导入结果中 `fail_count = 总行数``success_count = 0`
- **THEN** 返回错误提示:"Excel 文件缺少 virtual_no 列,请使用最新模板"
#### Scenario: 批次中有 virtual_no 与现存数据重复
- **WHEN** Excel 中某行 virtual_no = "CARD-001",但数据库中另一张卡已有 virtual_no = "CARD-001"
- **THEN** 系统拒绝整批导入,响应返回冲突的 virtual_no 值和行号,提示"虚拟号重复,整批导入已终止"
#### Scenario: 支持中文表头
- **GIVEN** Excel 文件表头为 `卡号 | 接入号 | 虚拟号`
- **WHEN** 系统解析该 Excel 文件
- **THEN** 系统正确识别三列,按规则处理 virtual_no
#### Scenario: 导入任务完成后的结果报告
- **WHEN** 导入任务处理完毕
- **THEN** 结果包含:`success_count``fail_count``skip_count`、失败明细列表(含行号、原因)
- **THEN** VirtualNo 为空的失败行在明细中明确体现行号和原因
#### Scenario: 拒绝非Excel格式文件
- **GIVEN** 上传文件扩展名为 .csv
- **WHEN** 系统尝试解析该文件
- **THEN** 系统返回错误"不支持的文件格式 .csv请上传Excel文件(.xlsx)"
#### Scenario: MSISDN 为空的行记录失败
- **GIVEN** Excel 文件第二行 MSISDN 为空
- **WHEN** 系统解析该 Excel 文件
- **THEN** 第一条记录解析成功,第二条记录标记为失败,原因为"MSISDN 不能为空"
#### Scenario: 长数字无损解析
- **GIVEN** Excel 文件中 ICCID 列设置为文本格式,包含 20 位数字 "89860012345678901234"
- **WHEN** 系统解析该 Excel 文件
- **THEN** ICCID 完整保留为 "89860012345678901234",无精度损失,无科学记数法
---
### Requirement: 导入时填充 MSISDN 字段
系统 SHALL 在创建 IoT 卡记录时填充 MSISDN 字段。
**处理规则**:
-`card_list` 中获取 ICCID 和 MSISDN
- 创建 `IotCard` 记录时同时设置 `iccid``msisdn` 字段
#### Scenario: 创建卡记录时填充 MSISDN
- **GIVEN** 导入任务包含卡数据 [{iccid: "898600...", msisdn: "13800000001"}]
- **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 写入导入任务记录