# 项目异步文件上传功能 - 子计划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 文档可访问

---

**所有子计划执行完成！**
