Files
huang b9c3875c08
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s
feat: 新增数据库迁移,重命名 device_no 为 virtual_no,新增 iot_card.virtual_no 和 package.virtual_ratio 字段
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-14 18:27:28 +08:00

313 lines
12 KiB
Markdown
Raw Permalink 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/虚拟号/设备号
- **列数要求**: 至少 2 列ICCID 和 MSISDNvirtual_no 为可选第三列
- **列格式**: 应设置为文本格式(避免长数字被转为科学记数法)
**解析规则**:
- 自动检测表头第1行包含识别关键字则跳过
- 自动去除单元格首尾空格
- 跳过空行
- ICCID 为空的行记录为失败
- MSISDN 为空的行记录为失败
- virtual_no 为空的行:跳过该列(不填入,保留原值)
**virtual_no 导入规则(只补空白)**:
- 该行 virtual_no 不为空 + 数据库当前值为 NULL填入新值
- 该行 virtual_no 不为空 + 数据库当前值已有值:跳过(不覆盖)
- **批次级唯一性检查**:在执行导入前,先检查整批中所有非空 virtual_no 是否与数据库现存值重复;有任意冲突则**整批失败**,响应中返回冲突的 virtual_no 及行号列表
**示例 Excel 内容**:
```
| ICCID | MSISDN | 虚拟号 |
|----------------------|-------------|-----------|
| 89860012345678901234 | 13800000001 | CARD-001 |
| 89860012345678901235 | 13800000002 | |
```
#### Scenario: 解析标准双列 Excel 文件(无 virtual_no 列)
- **WHEN** Excel 文件只含 ICCID 和 MSISDN 两列,无虚拟号列
- **THEN** 解析结果包含 2 条有效记录virtual_no 字段为空,不影响导入逻辑
#### Scenario: 解析含 virtual_no 列的三列 Excel
- **WHEN** Excel 文件含 ICCID、MSISDN、虚拟号三列某行 virtual_no = "CARD-001",对应卡当前 virtual_no 为 NULL
- **THEN** 解析后该卡的 virtual_no 填入 "CARD-001"
#### Scenario: virtual_no 已有值时不覆盖
- **WHEN** Excel 中某行 virtual_no = "CARD-NEW",但该卡数据库中已有 virtual_no = "CARD-OLD"
- **THEN** 该卡的 virtual_no 保持 "CARD-OLD" 不变,该行跳过(不报错,不计入失败)
#### 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: 拒绝非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 写入导入任务记录