Files
junhong_cmp_fiber/openspec/specs/iot-card-import-task/spec.md
huang afc6d7fc76
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 50s
docs: 归档 excel-import-refactor,同步更新主 specs
- 将变更目录移至 archive/2026-04-09-excel-import-refactor
- device-import spec:更新为固定列位置读取(IMEI 调整至第5列),新增 MaxSimSlots 范围校验说明
- iot-card-import-task spec:更新为固定列位置读取,新增"导入批次支持卡业务类型"需求

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 17:37:44 +08:00

339 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、虚拟号三列按固定列位置读取表头行内容不影响解析结果。
**文件格式要求**:
- **文件格式**: 仅支持 `.xlsx` (Excel 2007+)
- **Sheet**: 读取第一个sheet或优先读取名为"导入数据"的sheet
- **表头行**: 第1行固定为表头永远跳过内容不限中文、英文均可
- **列位置(固定,不可变)**:
- 第1列索引0: ICCID
- 第2列索引1: MSISDN
- 第3列索引2: 虚拟号(必填)
- **列格式**: 应设置为文本格式(避免长数字被转为科学记数法)
**解析规则**:
- 按列索引取值,不识别列名,第一行永远跳过
- 自动去除单元格首尾空格
- 若 ICCID、MSISDN、virtual_no 三列皆为空,视为空行跳过,不计入 total不计入失败
- ICCID 为空(但其他列非空)的行记录为失败,失败原因为"ICCID 不能为空"
- MSISDN 为空(但其他列非空)的行记录为失败,失败原因为"MSISDN 不能为空"
- virtual_no第3列为空的行记录为失败失败原因为"虚拟号(virtual_no)不能为空"
**virtual_no 导入规则**:
- virtual_no 为必填,为空则该行记录为失败
- virtual_no 全局唯一(跨卡和设备),重复则失败,原因为"虚拟号已被占用: <值>"
#### Scenario: 正常导入ICCID 和 VirtualNo 均有值)
- **WHEN** Excel 中某行第1列 ICCID="898600XXXXX"第3列 virtual_no="CARD-001"第2列 msisdn="13800000001"
- **THEN** 导入成功卡记录写入数据库VirtualNo 和 ICCID 同步注册到 `tb_asset_identifier`
#### Scenario: 中文表头正常导入
- **GIVEN** Excel 文件第1行表头为 `ICCID | 接入号 | 虚拟号`(任意中文或英文内容)
- **WHEN** 系统解析该 Excel 文件
- **THEN** 系统跳过第1行从第2行开始按列位置解析数据
#### Scenario: VirtualNo 为空的行被拒绝
- **WHEN** Excel 中某行第3列为空
- **THEN** 该行计入失败,失败原因为"虚拟号(virtual_no)不能为空"
- **THEN** 其他合法行继续导入,不因此行中断
#### Scenario: VirtualNo 重复被拒绝
- **WHEN** Excel 中某行第3列的值与已有卡/设备的 VirtualNo 重复(跨表)
- **THEN** 该行计入失败,原因为"虚拟号已被占用: <值>"
#### Scenario: 三列皆空时跳过不计入 total
- **WHEN** Excel 中某行 ICCID、MSISDN、virtual_no 三列均为空
- **THEN** 该行视为空行跳过,不计入 total不计入失败
#### Scenario: 导入任务完成后的结果报告
- **WHEN** 导入任务处理完毕
- **THEN** 结果包含:`success_count``fail_count``skip_count`、失败明细列表(含行号、原因)
- **THEN** VirtualNo 为空的失败行在明细中明确体现行号和原因
#### Scenario: 拒绝非Excel格式文件
- **GIVEN** 上传文件扩展名为 .csv
- **WHEN** 系统尝试解析该文件
- **THEN** 系统返回错误"不支持的文件格式 .csv请上传Excel文件(.xlsx)"
#### Scenario: MSISDN 为空的行记录失败
- **WHEN** Excel 中某行第2列为空
- **THEN** 该行标记为失败,原因为"MSISDN 不能为空"
#### Scenario: 长数字无损解析
- **GIVEN** Excel 文件中第1列设置为文本格式包含 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 写入导入任务记录
---
### Requirement: 导入批次支持卡业务类型
系统 SHALL 在 IoT 卡导入请求中支持 `card_category` 参数,以批次为单位指定导入卡的业务类型,默认为普通卡。
**请求字段**:
- `card_category`: 卡业务类型(枚举:`normal` / `industry`,可选,默认 `normal`
**任务字段**:
- `IotCardImportTask` 新增 `card_category` 字段VARCHAR(20),默认 `normal`
**导入行为**:
- 导入任务中所有卡统一使用 `card_category` 的值,不支持同一批次混用
#### Scenario: 不传 card_category 时默认为普通卡
- **WHEN** 导入请求未包含 `card_category` 字段
- **THEN** 导入的所有卡 `card_category``normal`
#### Scenario: 指定行业卡导入
- **WHEN** 导入请求中 `card_category` = `industry`
- **THEN** 该批次导入的所有卡 `card_category` 均为 `industry`
#### Scenario: 传入无效 card_category
- **WHEN** 导入请求中 `card_category` = `unknown`(非枚举值)
- **THEN** 系统返回参数校验错误,提示卡业务类型无效