Files
junhong_cmp_fiber/docs/7月迭代/需求08-设备批量分配Excel.md
2026-07-16 15:07:59 +08:00

207 lines
8.3 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.
# 需求08设备批量分配代理或套餐系列Excel导入
> 状态:待评审
> 模板:前端静态资源,后端仅负责上传校验和异步处理。
---
## 背景
设备号不连续,无法通过号段批量分配。需要通过上传 Excel 表(表头:设备号)完成两项独立操作:
1. 批量分配设备给代理。
2. 批量分配设备给套餐系列。
两项操作可以复用解析、异步任务和失败明细基础设施,但**一个任务只能执行一个业务命令**,不能在一次提交中同时修改 `shop_id``series_id`
`Device` 表已有 `shop_id *uint``series_id *uint` 字段,分配即更新这两个字段。
```mermaid
sequenceDiagram
actor User as 平台员工
participant Web as 设备管理前端
participant API as BatchAllocation API
participant DB as PostgreSQL
participant Worker as Asynq Worker
User->>Web: 选择“分配代理”或“分配套餐系列”并上传 Excel
Web->>API: POST /api/admin/devices/batch-assign-shop 或 batch-assign-series
API->>DB: 创建任务并保存上传文件引用
API-->>Web: task_id
API->>Worker: 入队处理任务
Worker->>DB: 按设备号批量查询并条件更新
Worker->>DB: 写成功数、失败数和失败明细
Web->>API: 轮询任务详情
API-->>Web: 处理结果
```
---
## 数据库变更
新建批量分配任务表(不复用 `DeviceImportTask`,业务语义不同):
```sql
CREATE TABLE tb_device_batch_allocation_task (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
creator BIGINT NOT NULL DEFAULT 0,
updater BIGINT NOT NULL DEFAULT 0,
task_no VARCHAR(30) NOT NULL,
source_file_key VARCHAR(500) NOT NULL,
operation_type VARCHAR(30) NOT NULL,
target_id BIGINT NOT NULL,
operator_id BIGINT NOT NULL,
total_count INT NOT NULL DEFAULT 0,
success_count INT NOT NULL DEFAULT 0,
fail_count INT NOT NULL DEFAULT 0,
status INT NOT NULL DEFAULT 1,
failed_items JSONB,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX idx_device_batch_allocation_task_no ON tb_device_batch_allocation_task(task_no) WHERE deleted_at IS NULL;
CREATE INDEX idx_device_batch_allocation_task_operation ON tb_device_batch_allocation_task(operation_type, status, created_at DESC);
```
状态常量1=处理中2=已完成3=失败。
`operation_type``assign_shop`(分配代理)或 `assign_series`(分配套餐系列)。`target_id` 随操作类型分别指向代理店铺或套餐系列;不建立数据库外键。
失败明细存 JSONB`failed_items`),失败条数有限,无需单独明细表。
---
## Model`internal/model/device_batch_allocation_task.go`
```go
// DeviceBatchAllocationTask 设备批量分配任务模型
type DeviceBatchAllocationTask struct {
gorm.Model
BaseModel `gorm:"embedded"`
TaskNo string `gorm:"column:task_no;type:varchar(30);uniqueIndex:idx_device_batch_allocation_task_no,where:deleted_at IS NULL;not null" json:"task_no"`
SourceFileKey string `gorm:"column:source_file_key;type:varchar(500);not null;comment:待处理Excel对象存储Key" json:"source_file_key"`
OperationType string `gorm:"column:operation_type;type:varchar(30);not null;comment:操作类型 assign_shop=分配代理 assign_series=分配套餐系列" json:"operation_type"`
TargetID uint `gorm:"column:target_id;not null;comment:操作目标ID代理店铺或套餐系列" json:"target_id"`
OperatorID uint `gorm:"column:operator_id;not null;comment:操作人ID" json:"operator_id"`
TotalCount int `gorm:"column:total_count;default:0;comment:总记录数" json:"total_count"`
SuccessCount int `gorm:"column:success_count;default:0;comment:成功数" json:"success_count"`
FailCount int `gorm:"column:fail_count;default:0;comment:失败数" json:"fail_count"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1=处理中 2=已完成 3=失败" json:"status"`
FailedItems ImportResultItems `gorm:"column:failed_items;type:jsonb;comment:失败记录详情" json:"failed_items"`
StartedAt *time.Time `gorm:"column:started_at" json:"started_at"`
CompletedAt *time.Time `gorm:"column:completed_at" json:"completed_at"`
}
func (DeviceBatchAllocationTask) TableName() string {
return "tb_device_batch_allocation_task"
}
```
> `ImportResultItems` 复用 `internal/model/device_import_task.go` 中已定义的类型。
---
## API 设计
### 1. 前端静态 Excel 模板
模板由前端项目作为静态资源随版本发布,后端不提供模板下载接口。
- 文件建议命名:`设备批量分配模板-v1.xlsx`
- 表头:`设备号`(一列)
- 前端“下载模板”按钮直接下载静态文件。
- 后端只校验上传文件的表头、行数和内容,不读取或生成前端模板文件。
模板字段发生变化时,前后端必须在同一发布批次升级;旧模板仍可能被用户保存在本地,因此后端错误需要明确指出缺失或未知表头。
### 2. 上传并提交
```
POST /api/admin/devices/batch-assign-shop
Content-Type: multipart/form-data
字段:
shop_id: 123 (必填,分配给哪个代理)
file: <Excel文件>
```
```text
POST /api/admin/devices/batch-assign-series
Content-Type: multipart/form-data
字段:
series_id: 45 (必填,分配给哪个套餐系列)
file: <Excel文件>
```
两个接口内部创建同一类任务表记录,但分别写入 `operation_type=assign_shop|assign_series`。请求中不接受另一个目标字段,避免前端通过隐藏参数把两个业务动作合并。
**处理逻辑Asynq 异步)**
1. API 将上传文件保存到对象存储,把 `source_file_key` 写入任务Worker 从对象存储下载并解析 Excel
2. 对设备号去重后分批查询 `tb_device`(按 `virtual_no``imei`),禁止逐行查询
3. `assign_shop` 仅按状态/归属条件批量更新 `shop_id``assign_series` 仅更新 `series_id`
4. 未找到的:记录失败原因"设备号不存在"。
5. `assign_shop` 遇到已分配给其他代理的设备:记录"该设备已分配,请先回收"`assign_series` 不改变代理归属。
6. Worker 重试时只处理仍未满足目标结果的设备;已经分配到同一目标的记录按幂等成功处理。
临时本地文件路径不能进入 Asynq 载荷。任务结束后源文件按统一对象存储生命周期清理,清理失败不影响任务结果。
限制:单文件最大 10MB、去重前最多 1000 行、Worker 每批最多 200 条、任务最多保留 1000 条失败明细。超过限制在 API 或解析阶段返回明确错误,不创建无限时长任务。
### 3. 查询任务状态
```
GET /api/admin/devices/batch-allocation/{task_id}
```
响应:
```json
{
"task_no": "DBA20240101001",
"operation_type": "assign_shop",
"target_id": 123,
"status": 2,
"status_name": "已完成",
"total_count": 100,
"success_count": 95,
"fail_count": 5,
"failed_items": [
{ "line": 3, "virtual_no": "12345", "reason": "设备号不存在" },
{ "line": 7, "virtual_no": "67890", "reason": "该设备已分配,请先回收" }
],
"started_at": "2024-01-01T10:00:00Z",
"completed_at": "2024-01-01T10:00:05Z"
}
```
---
## 前端对接
### 入口
设备管理页 > 批量操作:
- "批量分配代理"
- "批量分配套餐系列"
### 操作流程
1. 点击其中一个批量操作入口。
2. "批量分配代理"弹框只显示代理选择器和 Excel 上传区域;"批量分配套餐系列"弹框只显示套餐系列选择器和 Excel 上传区域。
3. 上传后点击"提交",分别调用 `POST /api/admin/devices/batch-assign-shop``POST /api/admin/devices/batch-assign-series`
4. 提示"任务提交成功,正在处理..."
5. 轮询 `GET /api/admin/devices/batch-allocation/{task_id}` 直到 `status != 1`
6. 展示结果成功X条失败X条失败明细在页面展示
### Excel 规范
- 表头:`设备号`
- 每行一个设备号支持虚拟号或IMEI