docs: 拆分实施计划为3个子计划(数据库、Service、Controller)

This commit is contained in:
wkc
2026-03-05 09:21:21 +08:00
parent 23e3dece7b
commit 73c78043ba
4 changed files with 1470 additions and 1384 deletions

View File

@@ -0,0 +1,483 @@
# 项目异步文件上传功能 - 子计划1数据库和基础组件
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 创建文件上传功能的数据库表、实体类、Mapper接口和基础配置
**Architecture:** 使用 MyBatis Plus 进行数据持久化配置容量100的异步线程池
**Tech Stack:** MySQL 8.0, MyBatis Plus 3.5.10, Spring Boot 3.5.8
---
## Task 1: 数据库表创建
**Files:**
- Create: `sql/ccdi_file_upload_record.sql`
**Step 1: 创建SQL脚本文件**
创建文件 `sql/ccdi_file_upload_record.sql`:
```sql
-- 项目文件上传记录表
-- 用途:记录项目下所有文件的上传记录和处理状态
-- 作者:系统
-- 日期2026-03-05
USE ccdi;
-- 创建文件上传记录表
CREATE TABLE `ccdi_file_upload_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`project_id` bigint(20) NOT NULL COMMENT '项目ID',
`lsfx_project_id` int(11) DEFAULT NULL COMMENT '流水分析平台项目ID',
`log_id` int(11) DEFAULT NULL COMMENT '流水分析平台返回的logId',
`file_name` varchar(255) NOT NULL COMMENT '文件名称',
`file_size` bigint(20) DEFAULT NULL COMMENT '文件大小(字节)',
`file_status` varchar(20) NOT NULL COMMENT '文件状态uploading-上传中parsing-解析中parsed_success-解析成功parsed_failed-解析失败',
`enterprise_names` text COMMENT '主体名称(多个用逗号分隔)',
`account_nos` text COMMENT '主体账号(多个用逗号分隔)',
`error_message` text COMMENT '错误信息(解析失败时记录)',
`upload_time` datetime NOT NULL COMMENT '上传时间',
`upload_user` varchar(64) NOT NULL COMMENT '上传人',
PRIMARY KEY (`id`),
KEY `idx_project_id` (`project_id`),
KEY `idx_log_id` (`log_id`),
KEY `idx_file_status` (`file_status`),
KEY `idx_upload_time` (`upload_time`),
KEY `idx_project_status` (`project_id`, `file_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目文件上传记录表';
```
**Step 2: 执行SQL脚本**
```bash
mysql -h 116.62.17.81 -u root -pKfcx@1234 ccdi < sql/ccdi_file_upload_record.sql
```
**Step 3: 验证表创建成功**
```bash
mysql -h 116.62.17.81 -u root -pKfcx@1234 ccdi -e "SHOW CREATE TABLE ccdi_file_upload_record\G"
```
Expected: 输出表结构,包含所有字段和索引
**Step 4: 提交SQL脚本**
```bash
git add sql/ccdi_file_upload_record.sql
git commit -m "feat: 添加文件上传记录表SQL脚本"
```
---
## Task 2: 实体类创建
**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiFileUploadRecord.java`
**Step 1: 创建实体类**
创建文件 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiFileUploadRecord.java`:
```java
package com.ruoyi.ccdi.project.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 文件上传记录实体
*
* @author ruoyi
* @date 2026-03-05
*/
@Data
@TableName("ccdi_file_upload_record")
public class CcdiFileUploadRecord implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(type = IdType.AUTO)
private Long id;
/** 项目ID */
private Long projectId;
/** 流水分析平台项目ID */
private Integer lsfxProjectId;
/** 流水分析平台返回的logId */
private Integer logId;
/** 文件名称 */
private String fileName;
/** 文件大小(字节) */
private Long fileSize;
/** 文件状态uploading-上传中parsing-解析中parsed_success-解析成功parsed_failed-解析失败 */
private String fileStatus;
/** 主体名称(多个用逗号分隔) */
private String enterpriseNames;
/** 主体账号(多个用逗号分隔) */
private String accountNos;
/** 错误信息(解析失败时记录) */
private String errorMessage;
/** 上传时间 */
private Date uploadTime;
/** 上传人 */
private String uploadUser;
}
```
**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/domain/entity/CcdiFileUploadRecord.java
git commit -m "feat: 添加文件上传记录实体类"
```
---
## Task 3: Mapper 接口和 XML
**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiFileUploadRecordMapper.java`
- Create: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml`
**Step 1: 创建 Mapper 接口**
创建文件 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiFileUploadRecordMapper.java`:
```java
package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 文件上传记录 Mapper 接口
*
* @author ruoyi
* @date 2026-03-05
*/
@Mapper
public interface CcdiFileUploadRecordMapper extends BaseMapper<CcdiFileUploadRecord> {
/**
* 批量插入文件上传记录
*
* @param records 记录列表
* @return 插入条数
*/
int insertBatch(@Param("list") List<CcdiFileUploadRecord> records);
/**
* 统计各状态文件数量
*
* @param projectId 项目ID
* @return 统计结果Map形式key为状态value为数量
*/
List<java.util.Map<String, Object>> countByStatus(@Param("projectId") Long projectId);
}
```
**Step 2: 创建 Mapper XML**
创建文件 `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml`:
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper">
<resultMap type="com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord" id="CcdiFileUploadRecordResult">
<id property="id" column="id" />
<result property="projectId" column="project_id" />
<result property="lsfxProjectId" column="lsfx_project_id" />
<result property="logId" column="log_id" />
<result property="fileName" column="file_name" />
<result property="fileSize" column="file_size" />
<result property="fileStatus" column="file_status" />
<result property="enterpriseNames" column="enterprise_names" />
<result property="accountNos" column="account_nos" />
<result property="errorMessage" column="error_message" />
<result property="uploadTime" column="upload_time" />
<result property="uploadUser" column="upload_user" />
</resultMap>
<sql id="selectCcdiFileUploadRecordVo">
select id, project_id, lsfx_project_id, log_id, file_name, file_size,
file_status, enterprise_names, account_nos, error_message,
upload_time, upload_user
from ccdi_file_upload_record
</sql>
<!-- 批量插入 -->
<insert id="insertBatch" parameterType="java.util.List">
insert into ccdi_file_upload_record (
project_id, lsfx_project_id, file_name, file_size, file_status,
upload_time, upload_user
) values
<foreach collection="list" item="item" separator=",">
(
#{item.projectId}, #{item.lsfxProjectId}, #{item.fileName},
#{item.fileSize}, #{item.fileStatus}, #{item.uploadTime},
#{item.uploadUser}
)
</foreach>
</insert>
<!-- 统计各状态文件数量 -->
<select id="countByStatus" resultType="java.util.Map">
select file_status as `status`, count(*) as count
from ccdi_file_upload_record
where project_id = #{projectId}
group by file_status
</select>
</mapper>
```
**Step 3: 编译验证**
```bash
cd ccdi-project
mvn clean compile
```
Expected: BUILD SUCCESS
**Step 4: 提交**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiFileUploadRecordMapper.java
git add ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml
git commit -m "feat: 添加文件上传记录Mapper接口和XML映射"
```
---
## Task 4: DTO 和 VO 类
**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiFileUploadQueryDTO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java`
**Step 1: 创建查询 DTO**
创建文件 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiFileUploadQueryDTO.java`:
```java
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 文件上传记录查询 DTO
*
* @author ruoyi
* @date 2026-03-05
*/
@Data
public class CcdiFileUploadQueryDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 项目ID */
private Long projectId;
/** 文件状态 */
private String fileStatus;
/** 文件名称(模糊查询) */
private String fileName;
/** 上传人 */
private String uploadUser;
}
```
**Step 2: 创建统计 VO**
创建文件 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java`:
```java
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 文件上传统计 VO
*
* @author ruoyi
* @date 2026-03-05
*/
@Data
public class CcdiFileUploadStatisticsVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 上传中数量 */
private Long uploading;
/** 解析中数量 */
private Long parsing;
/** 解析成功数量 */
private Long parsedSuccess;
/** 解析失败数量 */
private Long parsedFailed;
/** 总数量 */
private Long total;
}
```
**Step 3: 编译验证**
```bash
cd ccdi-project
mvn clean compile
```
Expected: BUILD SUCCESS
**Step 4: 提交**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiFileUploadQueryDTO.java
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java
git commit -m "feat: 添加文件上传查询DTO和统计VO"
```
---
## Task 5: 线程池配置
**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/config/AsyncThreadPoolConfig.java`
**Step 1: 创建线程池配置类**
创建文件 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/config/AsyncThreadPoolConfig.java`:
```java
package com.ruoyi.ccdi.project.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 异步线程池配置
*
* @author ruoyi
* @date 2026-03-05
*/
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {
/**
* 文件上传专用线程池
* 容量100个线程
* 拒绝策略AbortPolicy直接拒绝由调度线程捕获并重试
*/
@Bean("fileUploadExecutor")
public Executor fileUploadExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(100);
// 最大线程数
executor.setMaxPoolSize(100);
// 队列容量设为0不使用队列直接走拒绝策略
executor.setQueueCapacity(0);
// 线程名称前缀
executor.setThreadNamePrefix("file-upload-");
// 拒绝策略AbortPolicy抛出 RejectedExecutionException
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
// 线程空闲时间(秒)
executor.setKeepAliveSeconds(60);
// 等待所有任务完成后再关闭
executor.setWaitForTasksToCompleteOnShutdown(true);
// 最长等待时间
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}
```
**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/config/AsyncThreadPoolConfig.java
git commit -m "feat: 添加异步线程池配置"
```
---
## 子计划1完成检查清单
- [ ] 数据库表创建成功
- [ ] 实体类编译通过
- [ ] Mapper接口和XML映射正确
- [ ] DTO和VO类创建完成
- [ ] 线程池配置完成
- [ ] 所有代码已提交到git
**下一步:** 执行子计划2 - Service层核心实现

View File

@@ -0,0 +1,510 @@
# 项目异步文件上传功能 - 子计划2Service层核心实现
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 实现文件上传的核心业务逻辑,包括批量上传、异步处理、状态更新
**Architecture:** 双层异步架构(调度线程 + 文件处理线程池),先插入记录后异步处理
**Tech Stack:** Spring @Async, CompletableFuture, MyBatis Plus
---
## Task 1: Service 接口
**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java`
**Step 1: 创建 Service 接口**
创建文件 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java`:
```java
package com.ruoyi.ccdi.project.service;
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 org.springframework.web.multipart.MultipartFile;
/**
* 文件上传服务接口
*
* @author ruoyi
* @date 2026-03-05
*/
public interface ICcdiFileUploadService {
/**
* 批量上传文件
*
* @param projectId 项目ID
* @param files 文件数组
* @param username 上传人
* @return 批次ID
*/
String batchUploadFiles(Long projectId, MultipartFile[] files, String username);
/**
* 查询上传记录列表
*
* @param page 分页参数
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiFileUploadRecord> selectPage(Page<CcdiFileUploadRecord> page,
CcdiFileUploadQueryDTO queryDTO);
/**
* 统计各状态文件数量
*
* @param projectId 项目ID
* @return 统计结果
*/
CcdiFileUploadStatisticsVO countByStatus(Long projectId);
/**
* 根据ID查询记录详情
*
* @param id 记录ID
* @return 记录详情
*/
CcdiFileUploadRecord getById(Long id);
}
```
**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/service/ICcdiFileUploadService.java
git commit -m "feat: 添加文件上传服务接口"
```
---
## Task 2: Service 实现 - Part 1: 基础CRUD方法
**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
**Step 1: 创建 Service 实现类**
创建文件 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`:
```java
package com.ruoyi.ccdi.project.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.mapper.CcdiFileUploadRecordMapper;
import com.ruoyi.ccdi.project.service.ICcdiFileUploadService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
/**
* 文件上传服务实现
*
* @author ruoyi
* @date 2026-03-05
*/
@Slf4j
@Service
public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
@Resource
private CcdiFileUploadRecordMapper recordMapper;
@Override
public Page<CcdiFileUploadRecord> selectPage(Page<CcdiFileUploadRecord> page,
CcdiFileUploadQueryDTO queryDTO) {
LambdaQueryWrapper<CcdiFileUploadRecord> queryWrapper = new LambdaQueryWrapper<>();
// 项目ID
if (queryDTO.getProjectId() != null) {
queryWrapper.eq(CcdiFileUploadRecord::getProjectId, queryDTO.getProjectId());
}
// 文件状态
if (StringUtils.hasText(queryDTO.getFileStatus())) {
queryWrapper.eq(CcdiFileUploadRecord::getFileStatus, queryDTO.getFileStatus());
}
// 文件名称(模糊查询)
if (StringUtils.hasText(queryDTO.getFileName())) {
queryWrapper.like(CcdiFileUploadRecord::getFileName, queryDTO.getFileName());
}
// 上传人
if (StringUtils.hasText(queryDTO.getUploadUser())) {
queryWrapper.eq(CcdiFileUploadRecord::getUploadUser, queryDTO.getUploadUser());
}
// 按上传时间倒序
queryWrapper.orderByDesc(CcdiFileUploadRecord::getUploadTime);
return recordMapper.selectPage(page, queryWrapper);
}
@Override
public CcdiFileUploadStatisticsVO countByStatus(Long projectId) {
// 查询统计数据
List<Map<String, Object>> statusCounts = recordMapper.countByStatus(projectId);
// 组装 VO
CcdiFileUploadStatisticsVO vo = new CcdiFileUploadStatisticsVO();
vo.setUploading(0L);
vo.setParsing(0L);
vo.setParsedSuccess(0L);
vo.setParsedFailed(0L);
long total = 0L;
for (Map<String, Object> item : statusCounts) {
String status = (String) item.get("status");
Long count = ((Number) item.get("count")).longValue();
total += count;
switch (status) {
case "uploading" -> vo.setUploading(count);
case "parsing" -> vo.setParsing(count);
case "parsed_success" -> vo.setParsedSuccess(count);
case "parsed_failed" -> vo.setParsedFailed(count);
}
}
vo.setTotal(total);
return vo;
}
@Override
public CcdiFileUploadRecord getById(Long id) {
return recordMapper.selectById(id);
}
// batchUploadFiles 方法将在下一步实现
@Override
public String batchUploadFiles(Long projectId, MultipartFile[] files, String username) {
// TODO: 将在下一步实现
throw new UnsupportedOperationException("Method not implemented yet");
}
}
```
**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/service/impl/CcdiFileUploadServiceImpl.java
git commit -m "feat: 添加文件上传服务实现基础CRUD方法"
```
---
## Task 3: Service 实现 - Part 2: 批量上传主方法
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
**Step 1: 实现批量上传主方法**
`CcdiFileUploadServiceImpl.java` 中添加以下代码(替换原来的 TODO
```java
@Resource
@org.springframework.beans.factory.annotation.Qualifier("fileUploadExecutor")
private java.util.concurrent.Executor fileUploadExecutor;
@Override
public String batchUploadFiles(Long projectId, MultipartFile[] files, String username) {
log.info("【文件上传】开始批量上传: projectId={}, 文件数量={}, username={}",
projectId, files.length, username);
// 1. 生成批次ID
String batchId = java.util.UUID.randomUUID().toString().replace("-", "");
// 2. 获取项目的 lsfxProjectId
// TODO: 需要注入 CcdiProjectMapper 并查询项目信息
// Integer lsfxProjectId = project.getLsfxProjectId();
Integer lsfxProjectId = 1; // 临时硬编码,稍后修复
// 3. 批量插入文件记录status=uploading
List<CcdiFileUploadRecord> records = new java.util.ArrayList<>();
java.util.Date now = new java.util.Date();
for (MultipartFile file : files) {
CcdiFileUploadRecord record = new CcdiFileUploadRecord();
record.setProjectId(projectId);
record.setLsfxProjectId(lsfxProjectId);
record.setFileName(file.getOriginalFilename());
record.setFileSize(file.getSize());
record.setFileStatus("uploading");
record.setUploadTime(now);
record.setUploadUser(username);
records.add(record);
}
recordMapper.insertBatch(records);
log.info("【文件上传】批量插入记录成功: 数量={}", records.size());
// 4. 异步启动调度线程提交任务
final Integer finalLsfxProjectId = lsfxProjectId;
java.util.concurrent.CompletableFuture.runAsync(() -> {
submitTasksAsync(projectId, finalLsfxProjectId, files, records, batchId);
});
log.info("【文件上传】批量上传任务已提交: batchId={}", batchId);
return batchId;
}
/**
* 调度线程:循环提交任务到线程池
* 支持等待30秒重试机制
*/
private void submitTasksAsync(Long projectId, Integer lsfxProjectId,
MultipartFile[] files,
List<CcdiFileUploadRecord> records,
String batchId) {
log.info("【文件上传】调度线程启动: projectId={}, batchId={}", projectId, batchId);
// 循环提交任务
for (int i = 0; i < files.length; i++) {
MultipartFile file = files[i];
CcdiFileUploadRecord record = records.get(i);
boolean submitted = false;
int retryCount = 0;
while (!submitted && retryCount < 2) {
try {
// 尝试提交异步任务
java.util.concurrent.CompletableFuture.runAsync(
() -> processFileAsync(projectId, lsfxProjectId, file,
record.getId(), batchId, record),
fileUploadExecutor
);
submitted = true;
log.info("【文件上传】任务提交成功: fileName={}, recordId={}",
file.getOriginalFilename(), record.getId());
} catch (java.util.concurrent.RejectedExecutionException e) {
retryCount++;
if (retryCount == 1) {
log.warn("【文件上传】线程池已满等待30秒后重试: fileName={}",
file.getOriginalFilename());
try {
Thread.sleep(30000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
log.error("【文件上传】等待被中断: fileName={}", file.getOriginalFilename());
updateRecordStatus(record.getId(), "parsed_failed", "任务提交被中断");
break;
}
} else {
log.error("【文件上传】重试失败,放弃任务: fileName={}", file.getOriginalFilename());
updateRecordStatus(record.getId(), "parsed_failed", "系统繁忙,请稍后重试");
}
}
}
}
log.info("【文件上传】调度线程完成: projectId={}, batchId={}", projectId, batchId);
}
/**
* 更新记录状态(辅助方法)
*/
private void updateRecordStatus(Long recordId, String status, String errorMessage) {
CcdiFileUploadRecord record = new CcdiFileUploadRecord();
record.setId(recordId);
record.setFileStatus(status);
record.setErrorMessage(errorMessage);
recordMapper.updateById(record);
}
/**
* 异步处理单个文件的完整流程
* TODO: 下一步实现完整逻辑
*/
private void processFileAsync(Long projectId, Integer lsfxProjectId, MultipartFile file,
Long recordId, String batchId, CcdiFileUploadRecord record) {
// TODO: 将在下一步实现
log.info("【文件上传】开始处理文件: fileName={}", file.getOriginalFilename());
}
```
**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/service/impl/CcdiFileUploadServiceImpl.java
git commit -m "feat: 实现批量上传主方法和调度线程"
```
---
## Task 4: Service 实现 - Part 3: 异步处理单个文件
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
**Step 1: 实现异步处理单个文件的完整流程**
`CcdiFileUploadServiceImpl.java` 中,替换 `processFileAsync` 方法:
```java
/**
* 异步处理单个文件的完整流程
* 包含:上传 → 轮询解析状态 → 获取结果 → 保存流水数据
*/
@org.springframework.scheduling.annotation.Async("fileUploadExecutor")
public void processFileAsync(Long projectId, Integer lsfxProjectId, MultipartFile file,
Long recordId, String batchId, CcdiFileUploadRecord record) {
log.info("【文件上传】开始处理文件: fileName={}, recordId={}",
file.getOriginalFilename(), recordId);
try {
// 步骤1状态已是uploading记录已存在
// 步骤2上传文件到流水分析平台
log.info("【文件上传】步骤2: 上传文件到流水分析平台");
// TODO: 调用 lsfxClient.uploadFile()
// UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file);
// Integer logId = uploadResponse.getData().getLogId();
// 临时模拟 logId
Integer logId = (int) (System.currentTimeMillis() % 1000000);
// 步骤3更新状态为 parsing
log.info("【文件上传】步骤3: 更新状态为解析中, logId={}", logId);
record.setLogId(logId);
record.setFileStatus("parsing");
recordMapper.updateById(record);
// 步骤4轮询解析状态最多300次间隔2秒
log.info("【文件上传】步骤4: 开始轮询解析状态");
// TODO: 实现真实的轮询逻辑
// boolean parsingComplete = waitForParsingComplete(lsfxProjectId, logId.toString());
boolean parsingComplete = true; // 临时模拟
if (!parsingComplete) {
throw new RuntimeException("解析超时超过10分钟请检查文件格式是否正确");
}
// 步骤5获取文件上传状态
log.info("【文件上传】步骤5: 获取文件上传状态");
// TODO: 调用 lsfxClient.getFileUploadStatus()
// GetFileUploadStatusResponse statusResponse = lsfxClient.getFileUploadStatus(...);
// 步骤6判断解析结果
// TODO: 实现真实的判断逻辑
boolean parseSuccess = true; // 临时模拟
if (parseSuccess) {
// 解析成功
log.info("【文件上传】步骤6: 解析成功,保存主体信息");
record.setFileStatus("parsed_success");
record.setEnterpriseNames("测试主体1,测试主体2");
record.setAccountNos("622xxx,623xxx");
recordMapper.updateById(record);
// 步骤7获取流水数据并保存
log.info("【文件上传】步骤7: 获取流水数据");
// TODO: 实现 fetchAndSaveBankStatements
// fetchAndSaveBankStatements(projectId, lsfxProjectId, logId, totalCount);
} else {
// 解析失败
log.warn("【文件上传】步骤6: 解析失败");
record.setFileStatus("parsed_failed");
record.setErrorMessage("解析失败:文件格式错误");
recordMapper.updateById(record);
}
log.info("【文件上传】处理完成: fileName={}", file.getOriginalFilename());
} catch (Exception e) {
log.error("【文件上传】处理失败: fileName={}", file.getOriginalFilename(), e);
updateRecordStatus(recordId, "parsed_failed", e.getMessage());
}
}
/**
* 轮询解析状态
* TODO: 实现真实逻辑
*/
private boolean waitForParsingComplete(Integer groupId, String logId) {
// TODO: 调用 lsfxClient.checkParseStatus() 轮询
return true;
}
/**
* 获取并保存流水数据
* TODO: 实现真实逻辑
*/
private void fetchAndSaveBankStatements(Long projectId, Integer groupId,
Integer logId, int totalCount) {
// TODO: 调用 lsfxClient.getBankStatement() 获取流水
// TODO: 批量插入到 ccdi_bank_statement
}
```
**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/service/impl/CcdiFileUploadServiceImpl.java
git commit -m "feat: 实现异步处理单个文件的完整流程"
```
---
## 子计划2完成检查清单
- [ ] Service接口创建完成
- [ ] 基础CRUD方法实现并测试通过
- [ ] 批量上传主方法实现完成
- [ ] 调度线程和重试机制实现
- [ ] 异步处理单个文件流程实现
- [ ] 所有代码已提交到git
**下一步:** 执行子计划3 - Controller和API文档

View File

@@ -0,0 +1,477 @@
# 项目异步文件上传功能 - 子计划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 文档可访问
---
**所有子计划执行完成!**

File diff suppressed because it is too large Load Diff