478 lines
12 KiB
Markdown
478 lines
12 KiB
Markdown
# 项目异步文件上传功能 - 子计划3:Controller和文档
|
||
|
||
> **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 映射(支持批量插入和统计)
|
||
- ✅ 线程池配置(容量100,AbortPolicy拒绝策略)
|
||
- ✅ Service 接口和实现(核心异步处理逻辑)
|
||
- ✅ Controller 接口(批量上传、查询、统计、详情)
|
||
- ✅ API 文档
|
||
|
||
**核心特性:**
|
||
- ✅ 双层异步架构(调度线程 + 文件处理线程池)
|
||
- ✅ 智能重试机制(线程池满时等待30秒重试1次)
|
||
- ✅ 完整的状态追踪(4种状态)
|
||
- ✅ 批量插入优化(使用自定义XML)
|
||
- ✅ 完善的参数校验和异常处理
|
||
- ✅ Swagger API 文档
|
||
|
||
**后续优化方向:**
|
||
- ⏳ 完善流水分析平台接口调用(当前为模拟逻辑)
|
||
- ⏳ 实现自定义日志 Appender(独立批次日志文件)
|
||
- ⏳ 前端页面开发
|
||
- ⏳ 更完善的轮询和重试机制
|
||
- ⏳ 性能监控和告警
|
||
|
||
**部署检查清单:**
|
||
- [ ] 数据库表已创建
|
||
- [ ] 线程池配置正确(容量100)
|
||
- [ ] 文件上传大小限制配置(50MB)
|
||
- [ ] 流水分析平台地址配置正确
|
||
- [ ] 日志目录权限正确
|
||
- [ ] 应用启动成功
|
||
- [ ] Swagger 文档可访问
|
||
|
||
---
|
||
|
||
**所有子计划执行完成!**
|