Files
ccdi/doc/plans/2026-03-05-async-file-upload-part3-controller.md

478 lines
12 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.
# 项目异步文件上传功能 - 子计划3Controller和文档
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 实现文件上传的 REST API 接口,提供批量上传、查询、统计等功能
**Architecture:** RESTful API 设计参数校验异常处理Swagger 文档
**Tech Stack:** Spring MVC, Swagger/OpenAPI 3.0, Jackson
---
## Task 1: Controller 实现
**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java`
**Step 1: 创建 Controller**
创建文件 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java`:
```java
package com.ruoyi.ccdi.project.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.dto.CcdiFileUploadQueryDTO;
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO;
import com.ruoyi.ccdi.project.service.ICcdiFileUploadService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.SecurityUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.concurrent.RejectedExecutionException;
/**
* 文件上传 Controller
*
* @author ruoyi
* @date 2026-03-05
*/
@Slf4j
@RestController
@RequestMapping("/ccdi/file-upload")
@Tag(name = "文件上传管理", description = "项目文件上传相关接口")
public class CcdiFileUploadController extends BaseController {
@Resource
private ICcdiFileUploadService fileUploadService;
/**
* 批量上传文件(异步)
*/
@PostMapping("/batch")
@Operation(summary = "批量上传文件", description = "异步批量上传流水文件")
public AjaxResult batchUpload(@RequestParam Long projectId,
@RequestParam MultipartFile[] files) {
// 参数校验
if (projectId == null) {
return AjaxResult.error("项目ID不能为空");
}
if (files == null || files.length == 0) {
return AjaxResult.error("请选择要上传的文件");
}
if (files.length > 100) {
return AjaxResult.error("单次最多上传100个文件");
}
// 校验文件大小和格式
for (MultipartFile file : files) {
if (file.isEmpty()) {
return AjaxResult.error("文件不能为空");
}
if (file.getSize() > 50 * 1024 * 1024) {
return AjaxResult.error("文件 " + file.getOriginalFilename() + " 超过50MB限制");
}
String fileName = file.getOriginalFilename();
if (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls")) {
return AjaxResult.error("文件 " + fileName + " 格式不支持仅支持Excel文件");
}
}
try {
String username = SecurityUtils.getUsername();
String batchId = fileUploadService.batchUploadFiles(projectId, files, username);
return AjaxResult.success("上传任务已提交", batchId);
} catch (RejectedExecutionException e) {
log.warn("线程池已满,拒绝上传请求: projectId={}, fileCount={}", projectId, files.length);
return AjaxResult.error("系统繁忙,请稍后再试");
} catch (Exception e) {
log.error("批量上传失败: projectId={}", projectId, e);
return AjaxResult.error("上传失败:" + e.getMessage());
}
}
/**
* 查询上传记录列表
*/
@GetMapping("/list")
@Operation(summary = "查询上传记录列表", description = "分页查询文件上传记录")
public TableDataInfo list(CcdiFileUploadQueryDTO queryDTO) {
Page<CcdiFileUploadRecord> page = new Page<>(getPageNum(), getPageSize());
Page<CcdiFileUploadRecord> result = fileUploadService.selectPage(page, queryDTO);
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 查询上传统计
*/
@GetMapping("/statistics/{projectId}")
@Operation(summary = "查询上传统计", description = "统计各状态的文件数量")
public AjaxResult getStatistics(@PathVariable Long projectId) {
CcdiFileUploadStatisticsVO statistics = fileUploadService.countByStatus(projectId);
return AjaxResult.success(statistics);
}
/**
* 查询记录详情
*/
@GetMapping("/detail/{id}")
@Operation(summary = "查询记录详情", description = "根据ID查询文件上传记录详情")
public AjaxResult getDetail(@PathVariable Long id) {
CcdiFileUploadRecord record = fileUploadService.getById(id);
return AjaxResult.success(record);
}
}
```
**Step 2: 编译验证**
```bash
cd ccdi-project
mvn clean compile
```
Expected: BUILD SUCCESS
**Step 3: 提交**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java
git commit -m "feat: 添加文件上传Controller"
```
---
## Task 2: API 文档
**Files:**
- Create: `doc/api-docs/ccdi-file-upload-api.md`
**Step 1: 创建 API 文档**
创建文件 `doc/api-docs/ccdi-file-upload-api.md`:
```markdown
# 文件上传 API 文档
## 1. 批量上传文件
### 接口地址
POST /ccdi/file-upload/batch
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| projectId | Long | 是 | 项目ID |
| files | File[] | 是 | 文件数组最多100个单个最大50MB |
### 请求示例
```bash
curl -X POST "http://localhost:8080/ccdi/file-upload/batch" \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "projectId=1" \
-F "files=@/path/to/file1.xlsx" \
-F "files=@/path/to/file2.xlsx"
```
### 返回示例
```json
{
"code": 200,
"msg": "上传任务已提交",
"data": "a1b2c3d4e5f6g7h8"
}
```
### 返回字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| code | Integer | 状态码200表示成功 |
| msg | String | 提示信息 |
| data | String | 批次ID用于追踪上传任务 |
### 错误码说明
| code | msg | 说明 |
|------|-----|------|
| 500 | 项目ID不能为空 | 缺少必填参数 |
| 500 | 请选择要上传的文件 | 文件数组为空 |
| 500 | 单次最多上传100个文件 | 文件数量超限 |
| 500 | 文件 xxx 超过50MB限制 | 文件大小超限 |
| 500 | 文件 xxx 格式不支持仅支持Excel文件 | 文件格式错误 |
| 500 | 系统繁忙,请稍后再试 | 线程池已满 |
---
## 2. 查询上传记录列表
### 接口地址
GET /ccdi/file-upload/list
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| projectId | Long | 否 | 项目ID |
| fileStatus | String | 否 | 文件状态uploading/parsing/parsed_success/parsed_failed |
| fileName | String | 否 | 文件名称(模糊查询) |
| uploadUser | String | 否 | 上传人 |
| pageNum | Integer | 否 | 页码默认1 |
| pageSize | Integer | 否 | 每页数量默认10 |
### 请求示例
```bash
curl -X GET "http://localhost:8080/ccdi/file-upload/list?projectId=1&fileStatus=parsed_success&pageNum=1&pageSize=10" \
-H "Authorization: Bearer YOUR_TOKEN"
```
### 返回示例
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"id": 1,
"projectId": 1,
"lsfxProjectId": 100,
"logId": 123456,
"fileName": "流水1.xlsx",
"fileSize": 2621440,
"fileStatus": "parsed_success",
"enterpriseNames": "张三,李四",
"accountNos": "622xxx,623xxx",
"uploadTime": "2026-03-05 10:30:00",
"uploadUser": "admin"
}
],
"total": 100
}
```
### 返回字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| rows | Array | 记录列表 |
| total | Long | 总记录数 |
---
## 3. 查询上传统计
### 接口地址
GET /ccdi/file-upload/statistics/{projectId}
### 路径参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| projectId | Long | 是 | 项目ID |
### 请求示例
```bash
curl -X GET "http://localhost:8080/ccdi/file-upload/statistics/1" \
-H "Authorization: Bearer YOUR_TOKEN"
```
### 返回示例
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"uploading": 2,
"parsing": 3,
"parsedSuccess": 15,
"parsedFailed": 1,
"total": 21
}
}
```
### 返回字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| uploading | Long | 上传中数量 |
| parsing | Long | 解析中数量 |
| parsedSuccess | Long | 解析成功数量 |
| parsedFailed | Long | 解析失败数量 |
| total | Long | 总数量 |
---
## 4. 查询记录详情
### 接口地址
GET /ccdi/file-upload/detail/{id}
### 路径参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | Long | 是 | 记录ID |
### 请求示例
```bash
curl -X GET "http://localhost:8080/ccdi/file-upload/detail/1" \
-H "Authorization: Bearer YOUR_TOKEN"
```
### 返回示例
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"id": 1,
"projectId": 1,
"lsfxProjectId": 100,
"logId": 123456,
"fileName": "流水1.xlsx",
"fileSize": 2621440,
"fileStatus": "parsed_success",
"enterpriseNames": "张三,李四",
"accountNos": "622xxx,623xxx",
"errorMessage": null,
"uploadTime": "2026-03-05 10:30:00",
"uploadUser": "admin"
}
}
```
---
## 5. 文件状态说明
| 状态 | 说明 |
|------|------|
| uploading | 文件上传中 |
| parsing | 文件解析中 |
| parsed_success | 文件解析成功 |
| parsed_failed | 文件解析失败 |
---
## 6. 通用说明
### 认证方式
所有接口需要在请求头中携带 Token
```
Authorization: Bearer YOUR_TOKEN
```
### 获取 Token
```bash
POST /login/test?username=admin&password=admin123
```
### 响应格式
所有接口统一返回格式:
```json
{
"code": 200,
"msg": "操作成功",
"data": {}
}
```
### 错误处理
当发生错误时,返回格式:
```json
{
"code": 500,
"msg": "错误信息"
}
```
```
**Step 2: 提交文档**
```bash
git add doc/api-docs/ccdi-file-upload-api.md
git commit -m "docs: 添加文件上传API文档"
```
---
## Task 3: 最终提交和推送
**Step 1: 查看所有修改**
```bash
git status
git log --oneline -10
```
**Step 2: 推送到远程仓库**
```bash
git push origin dev
```
Expected: 推送成功
**Step 3: 验证 Swagger 文档**
```bash
# 启动应用后访问
# http://localhost:8080/swagger-ui/index.html
# 查找 "文件上传管理" 分组
```
---
## 子计划3完成检查清单
- [ ] Controller实现完成
- [ ] 参数校验正确
- [ ] 异常处理完善
- [ ] API文档创建完成
- [ ] Swagger注解正确
- [ ] 所有代码已提交并推送到远程仓库
---
## 功能总结
**已完成的完整功能:**
- ✅ 数据库表创建和索引
- ✅ 实体类、DTO、VO 创建
- ✅ Mapper 接口和 XML 映射(支持批量插入和统计)
- ✅ 线程池配置容量100AbortPolicy拒绝策略
- ✅ Service 接口和实现(核心异步处理逻辑)
- ✅ Controller 接口(批量上传、查询、统计、详情)
- ✅ API 文档
**核心特性:**
- ✅ 双层异步架构(调度线程 + 文件处理线程池)
- ✅ 智能重试机制线程池满时等待30秒重试1次
- ✅ 完整的状态追踪4种状态
- ✅ 批量插入优化使用自定义XML
- ✅ 完善的参数校验和异常处理
- ✅ Swagger API 文档
**后续优化方向:**
- ⏳ 完善流水分析平台接口调用(当前为模拟逻辑)
- ⏳ 实现自定义日志 Appender独立批次日志文件
- ⏳ 前端页面开发
- ⏳ 更完善的轮询和重试机制
- ⏳ 性能监控和告警
**部署检查清单:**
- [ ] 数据库表已创建
- [ ] 线程池配置正确容量100
- [ ] 文件上传大小限制配置50MB
- [ ] 流水分析平台地址配置正确
- [ ] 日志目录权限正确
- [ ] 应用启动成功
- [ ] Swagger 文档可访问
---
**所有子计划执行完成!**