Files
junhong_cmp_fiber/.scratch/ur49-device-batch-allocation-csv/PRD.md
2026-07-21 15:26:07 +09:00

183 lines
17 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.
# PRDUR#49 设备 CSV 批量分配代理或套餐系列
Status: ready-for-agent
---
## Problem Statement
设备号不连续现有按页面勾选、ID 列表、号段或筛选条件操作不适合运营人员手中已有的一批离散设备清单。七月需求需要通过上传文件异步完成两种批量操作:把设备分配给代理,或者为设备设置套餐系列。
原需求稿使用 Excel但本需求只有一列设备标识。Excel 文件更大、解析依赖更重,也容易让人误以为可以在同一表中混合多个业务动作。用户决定本期只使用 CSV同时要求新入口继承现有平台和代理权限不得因旧稿流程图写了“平台员工”而收窄为平台专用。
## Solution
设备管理页提供“批量分配代理”和“批量分配套餐系列”两个独立 CSV 入口。前端先通过现有对象存储预签名能力直传单列 UTF-8 CSV再分别向两个业务接口提交 `file_key` 与唯一目标 ID。接口创建批量任务并向 Asynq 仅发送结构化 `task_id`Worker 下载 CSV、完整校验、去重后按每批 200 个标识批量查询并执行对应命令。
批量用例复用现有设备分配和系列绑定业务规则:平台分配平台库存设备,代理只能向直属下级分配自己名下设备;代理设置系列时只能操作自己名下设备并使用自己已获授权的系列。一个任务只能修改 `shop_id``series_id` 之一,不能同时修改两者。
## User Stories
1. 作为平台员工,我希望上传一列离散设备号,将平台库存设备批量分配给目标代理。
2. 作为代理,我希望用相同入口将自己名下设备批量分配给直属下级代理。
3. 作为平台或代理,我希望上传设备清单批量设置套餐系列,并沿用现有系列权限。
4. 作为运营人员,我希望使用体积小、结构清晰的 CSV不需要上传 Excel。
5. 作为运营人员,我希望看到任务进度、成功数、幂等数和逐行失败原因,并能修正失败行后重新提交。
6. 作为审计人员,我希望批量任务、实际设备归属变更、绑定卡同步和系列变更都有可追溯记录。
## Implementation Decisions
### 两个独立业务命令
- 批量分配代理:`POST /api/admin/devices/batch-assign-shop`
- 批量分配套餐系列:`POST /api/admin/devices/batch-assign-series`
- 两个接口复用同一任务基础设施,但路径和 DTO 独立;请求中不得接受另一个命令的目标字段,也不增加能同时设置代理和系列的 `operation_type` 通用入口。
- `batch-assign-shop` 只允许更新设备归属及现有用例规定的绑定卡归属、设备状态和分配记录,不修改 `series_id`
- `batch-assign-series` 只允许更新 `series_id`,不修改 `shop_id`、设备归属状态或绑定卡归属。
- 现有 `POST /api/admin/devices/allocate``PATCH /api/admin/devices/series-binding` 继续服务页面选择、ID 列表、号段或筛选操作CSV 接口是新增入口,但底层必须收口到相同业务规则,不能复制一套宽松逻辑。
### CSV 契约
- 本需求只接受 `.csv`,不接受 `.xlsx``.xls` 或把 Excel 改扩展名后的文件;不维护 CSV 与 Excel 两套解析器。
- 文件编码固定为 UTF-8可带 UTF-8 BOM换行允许 LF 或 CRLF。其他编码明确拒绝并提示使用模板重新保存。
- 模板由前端作为静态资源发布,后端不增加模板下载接口。建议文件名为 `设备批量分配模板-v1.csv`
- CSV 只有一列,固定表头为 `设备号`;每个数据行一个设备标识,支持设备 `virtual_no` 或 IMEI 精确匹配不支持模糊匹配、号段、SN 或卡 ICCID。
- 标识去除首尾空白后参与匹配,最大 100 字符;空值、控制字符、公式前缀以及已经变成科学计数法或小数形式的长数字逐行失败,系统不得尝试还原猜测。
- 模板需提示用户不要用会改写长数字的格式保存;前端预览也必须按字符串展示。后端最终仍以收到的原始字符串严格校验。
- 单文件最大 10MB、最多 1000 个数据行。先完整完成结构与行数校验,再进行任何业务写入,避免解析到一半才发现文件整体非法。
- 表头缺失、列数不为 1、CSV 引号结构损坏、非法编码、文件过大或超过 1000 行属于文件级错误,任务直接失败且不处理任何设备。
- 数据行中的空值、非法标识和未找到设备属于行级失败。文件内重复标识保留第一次出现,后续重复行记录“文件内设备号重复”,不重复计为成功。
- 解析使用 Go 标准库 `encoding/csv`;本任务不需要 Excel 解析依赖。错误消息和失败明细必须使用中文。
### 对象存储上传
- 复用 `POST /api/admin/storage/upload-url`,新增受控用途 `device_batch_allocation`,前缀限定在设备批量分配目录,默认 Content-Type 为 `text/csv`,只允许生成 `.csv` Key。
- 前端取得预签名 URL 后直接把 CSV 上传对象存储,再调用业务接口提交 `file_key`;业务接口不使用 `multipart/form-data` 接收文件字节。
- 创建任务前校验 `file_key` 非空、属于该受控用途前缀、扩展名正确且对象存在。Worker 下载后再次校验实际大小、编码和 CSV 内容,不能信任扩展名或客户端 Content-Type。
- 本地临时路径和文件字节均不得写入数据库或 Asynq 载荷。Worker 如需临时文件,必须使用存储层受控临时目录并以 `defer` 清理。
- 源文件清理沿用统一对象存储生命周期;清理失败不得改变已经确定的业务任务结果,但需记录告警。
### 创建任务接口
- 分配代理请求:
```json
{
"file_key": "device-batch-allocations/2026/07/21/example.csv",
"shop_id": 123
}
```
- 分配系列请求:
```json
{
"file_key": "device-batch-allocations/2026/07/21/example.csv",
"series_id": 45
}
```
- `shop_id``series_id` 均为大于 0 的必填目标。CSV 新入口不承担回收设备或清除系列;这些动作继续走现有明确接口。
- 提交时先校验操作者身份、目标资源和目标权限,再创建任务。目标不存在或无权限使用统一安全错误,不能通过批量接口探测其他店铺或系列。
- 成功响应返回 `task_id``task_no``operation_type` 和任务状态。此时只代表任务已成功入队,不代表设备已经分配成功。
- 如果数据库创建任务成功但入队失败,将任务标记为失败并返回统一系统错误;不得留下永久“处理中”的假任务。
### 任务模型与状态
- 新建独立 `tb_device_batch_allocation_task`,不复用语义不同的设备导入任务或导出任务表,也不建立数据库外键。
- 任务至少保存:任务号、源文件 Key、`operation_type`、目标 ID、操作者账号 ID、操作者类型与店铺快照、总数、已处理数、成功数、幂等数、失败数、状态、失败明细、错误摘要、开始时间和完成时间。
- `operation_type` 固定为 `assign_shop|assign_series`
- 状态沿用项目异步任务的 int 生命周期枚举:`1:待处理, 2:处理中, 3:已完成, 4:失败`,响应必须同时返回 `status_name`
- 行级业务失败不把整个任务标为系统失败CSV 被完整处理后,即使全部行都因业务原因失败,任务仍是“已完成”并通过 `fail_count` 和明细表达结果。文件级解析错误、存储错误或不可恢复的基础设施错误才是“失败”。
- `total_count = success_count + fail_count``success_count` 包含实际变更与同目标幂等成功;`idempotent_count` 是成功数的子集。重复 CSV 行计入失败,避免汇总数字无法对齐上传行数。
- 失败明细最多 1000 条,与单文件行数上限一致;每项返回原始行号、脱敏或安全的输入设备号和中文原因。不得把数据库错误或其他租户资源信息原样返回。
### 任务查询契约
- `GET /api/admin/devices/batch-allocation/{task_id}` 返回任务号、命令、目标摘要、状态与名称、总数、已处理数、成功数、幂等数、失败数、进度、失败明细及起止时间。
- 进度只在 `total_count > 0` 时按 `processed_count / total_count` 计算;解析尚未完成时返回 0终态固定 100。
- 代理只能查询自己创建的任务;平台账号按现有设备批量管理权限查询平台范围任务,超级管理员可排障查看全部。无权与不存在使用相同错误语义。
- 前端轮询到完成或失败即停止;失败明细直接在页面展示,不要求另建结果文件下载接口。
### 权限与数据范围
- 用户已确认 CSV 入口继承现有权限,旧稿中的“平台员工”只是示意,不是平台专用限制。
- `assign_shop`
- 平台/超级管理员只能把平台库存设备分配给现有权限允许的目标店铺。
- 代理只能把自己店铺名下的设备分配给直属下级店铺,不能分给自己、旁系、上级或跨层级店铺。
- 已经属于目标店铺的设备按幂等成功,主要用于安全恢复 Worker 重试;不重复写归属记录。
- 已经属于其他店铺的设备逐行失败并提示先按现有流程回收,不能由批量任务直接横向转移。
- `assign_series`
- 平台/超级管理员沿用现有系列绑定范围。
- 代理只能操作自己店铺名下的设备,并且目标系列必须是该代理当前有效授权的系列。
- 已经是目标系列按幂等成功;绑定其他系列时沿用现有系列变更规则更新目标值。
- Worker 不能只信任提交时快照。执行前重新加载操作者账号、账号状态、当前店铺关系、目标资源和系列授权;账号已禁用或权限已撤销时,任务失败或对应行安全拒绝,不继续使用过期权限。
- 重新构造后台任务上下文后仍使用现有 GORM 数据范围与业务校验。未命中和无权操作在行级使用统一“设备不存在或无权限”语义,防止上传清单探测其他代理设备。
### 批量查询与执行
- 去重后的设备标识按每批最多 200 条,用 `virtual_no IN (...) OR imei IN (...)` 批量查询;禁止为每行调用一次 `GetByIdentifier` 形成 N+1。
- 同一输入若因历史脏数据命中多台设备,逐行失败为“设备标识不唯一”,不能使用 `First` 任取一台。实现前应核查 IMEI 重复数据,不能假设当前模型未声明的唯一性。
- 每批先构造标识到设备的唯一映射,再一次性加载目标店铺、系列授权、绑定卡和其他业务校验所需数据。
- 业务规则复用现有 Application 用例,但需提取可接受预加载设备批次的内部命令,不能让 Worker 直接执行裸 `UPDATE tb_device` 绕过绑定卡同步、直属关系、系列授权和审计。
- `assign_shop` 每个写批次在同一 PostgreSQL 事务内更新符合条件的设备、绑定卡归属与状态,并创建资产分配记录;任一数据库写失败则该批事务回滚并由任务机制安全重试。
- `assign_series` 对符合条件的设备批量条件更新 `series_id`,并记录统一审计;不需要更新绑定卡。
- Worker 重试时重新读取当前状态:已到目标值的行按幂等成功,不重复写资产分配记录或重复审计;未到目标值的行继续处理。
- Asynq 载荷必须使用结构体 `{"task_id":...}`,不得预先序列化为 `[]byte` 传给 `EnqueueTask`
### 并发、一致性与审计
- 设备归属和系列更新必须使用带期望当前值的条件更新。Worker 查询后若设备被其他请求改变,当前行返回冲突或按最新状态重新判断,不能覆盖并发操作。
- `assign_shop` 的设备、绑定卡与资产分配记录必须同事务提交;不能出现设备已到下级但绑定卡仍留在上级的中间成功。
- 批量任务允许部分成功,但单个写批次中的基础设施错误不得提交一半。行级业务失败在写事务前分离并记录。
- 创建任务、任务终态和实际批量命令均写统一 Audit Event实际归属变化继续写 `AssetAllocationRecord`。审计至少包含任务号、命令、目标、源文件 Key、安全摘要、总数、成功/幂等/失败数和操作者。
- 行级失败原因不得把其他店铺名称、内部 SQL 或底层错误暴露给无权操作者。源文件中的设备标识按公共审计脱敏规则处理。
### 前端交互
- 设备管理页提供两个独立按钮和弹框“批量分配代理”“批量分配套餐系列”。单个弹框只显示对应目标选择器、CSV 模板下载、文件上传和提交按钮。
- 代理的店铺选择器只展示直属下级;系列选择器只展示自己当前有效授权系列。平台沿用现有目标数据范围。
- 文件选择只接受 `.csv`,上传前展示文件名、大小和前几行文本预览;预览不能把长设备号转换成数字。
- 提交前展示命令名称、目标代理或系列以及 CSV 数据行数;上传和提交期间禁止重复点击。
- 任务提交后轮询详情,展示待处理、处理中、已完成、失败,及总数、成功数、幂等数、失败数和失败原因。
- 部分成功必须明确展示,不能把任务整体标成失败而隐藏已完成的设备。用户修正失败行后可新建任务重试。
### 发布与回滚
- 上线前检查目标环境 Redis、PostgreSQL、Asynq Worker 和对象存储均可用,并用真实开发环境完成 CSV 上传、异步执行和任务查询联调。
- 前后端同批增加 `device_batch_allocation` 上传用途、两个任务创建接口和任务详情。Worker 未部署前不得开放前端入口。
- 发布前核查重复 IMEI并验证平台、代理、直属下级和系列授权的数据范围不自动修改历史设备数据。
- 回滚应用时保留已经产生的任务、资产分配记录与审计事实;不得通过回滚删除已经完成的设备归属或系列变更。
## Testing Decisions
- CSV 解析测试覆盖 UTF-8、UTF-8 BOM、LF、CRLF、中文表头、引号、空行、空值、额外列、非法引号、非 UTF-8、控制字符、公式前缀、科学计数法、超长标识、1000/1001 行和 10MB 边界。
- 上传测试覆盖受控 purpose、`.csv` 扩展名、错误 Content-Type、伪装 Excel、越权或其他用途 `file_key`、对象不存在和 Worker 下载失败。
- 权限测试覆盖平台库存分配、平台非库存拒绝、代理分配自己设备给直属下级、分给自己/旁系/上级/跨级拒绝,以及旧稿“平台员工”不造成代理入口缺失。
- 系列测试覆盖平台代理范围、代理自己设备、代理无授权系列、设备不属于自己、同系列幂等、从其他系列切换和不修改 `shop_id`
- 归属一致性测试验证实际分配会同步所有绑定卡归属与状态、创建分配记录,并在任一写入失败时整批事务回滚。
- 批量查询测试验证每 200 条查询、无 N+1、虚拟号与 IMEI 精确匹配、未找到、无权限、跨字段重复命中和历史重复 IMEI不会任取设备。
- 汇总测试验证重复行、逐行失败、实际成功和幂等成功满足 `total=success+fail`,并正确计算 `idempotent_count` 与进度。
- 任务状态测试覆盖待处理、处理中、部分成功后完成、全部业务失败后完成、文件级失败、入队失败、Worker 崩溃重试和终态不重复执行。
- 并发测试覆盖同设备同时分配、系列同时变更、查询后状态被改变,以及条件更新不会覆盖并发结果。
- Asynq 测试验证载荷是结构体并且只包含 `task_id`,不包含文件字节、临时路径或敏感上下文。
- HTTP 集成测试穿过 Fiber 认证、对象存储、Application、真实 PostgreSQL、真实 Redis、Asynq Worker 和统一响应,验证平台与代理完整链路。
- 前端验收覆盖两个独立 CSV 弹框、静态模板、长数字文本预览、目标选择权限、上传失败、轮询、部分成功与失败明细。
## Out of Scope
- 不支持 Excel也不维护 CSV/Excel 双格式兼容。
- 不在同一任务同时分配代理和套餐系列。
- 不用 CSV 执行设备回收、横向转移或清除套餐系列。
- 不支持号段、模糊匹配、SN 或卡 ICCID这些不属于本期单列设备清单契约。
- 不新增后端模板下载接口或失败结果文件下载接口。
- 不改变现有页面勾选、ID 列表、号段和筛选操作入口。
- 不绕过代理直属下级、设备归属和系列授权规则。
## Further Notes
- 当前同步 `AllocateDevices` 已支持平台代理分配、设备与绑定卡归属同事务更新;当前 `BatchSetSeriesBinding` 已支持平台代理系列权限。这些是新 CSV 用例的业务权威,不应由 Worker 重写。
- 当前设备任意标识查询使用 `First` 且 IMEI 没有模型唯一约束;批量实现必须显式检测多命中,避免脏数据导致错误分配。
- 当前对象存储上传用途把 `iot_import` 写死为 Excel Content-Type本需求需要单独增加 CSV 用途,不能继续复用该错误类型。
- 用户已明确确认:新批量入口继承平台代理现有权限,并用 CSV 取代原稿 Excel。