一、创建子模块 file-service

二、接入依赖
<dependencies>
<!--1. 通用服务依赖-->
<dependency>
<groupId>com.xxxxx</groupId>
<artifactId>common-service</artifactId>
<version>1.0.0</version>
</dependency>
<!--2. web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--3. lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--4. validation依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--5. 数据库驱动依赖-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!--6. mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!--7 poi依赖-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<!--8. 阿里云oss依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<!--9. Nacos配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--10. Nacos服务注册-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--11. bootstrap依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--12 easyExcel依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.4</version>
</dependency>
</dependencies>
三、表结构设计

三张表分别管三件事:
files:记「这是谁的文件」。谁上传的、原文件名、OSS 上的位置、上传状态。列表、下载、鉴权、删文件都靠它。
file_table_mappings:记「这个文件生成了哪几张 MySQL 表」。一个 Excel 可能有多个 Sheet,每个 Sheet 会建成一张独立表。预览切 Sheet、复原、删除、AI 选表,都先通过 fileId 查这张映射。
field_mappings:记「Excel 表头怎么对应数据库列」。例如「姓名」→ col_xingming。预览要显示中文表头,AI 要把用户说的列名翻成 SQL 字段,都靠这张表。
一句话:files 管文件本身,file_table_mappings 管文件到表,field_mappings 管表头到字段。
四、上传文件功能的实现
接口定义

定义 Controller 接口
java
/**
* 文件服务控制器
*/
@RestController
@RequestMapping("/files")
public class FilesController {
/**
* 单个文件上传接口
*/
@PostMapping("/upload/single")
@LogOperation("文件上传")
public Result<FileUploadResponse> uploadFile(
@RequestParam("file")MultipartFile file,
@RequestHeader("Authorization")String authorization,
@Valid FileUploadRequest request
) {
}
}
补充 dto 的 FileUploadResponse 类 与 FileUploadRequest 类
java
/**
* 文件上传的响应结果
*/
@Data
@Builder
public class FileUploadResponse {
/**
* 文件ID
*/
private Long fileId;
/**
* 原始文件名
*/
private String fileName;
/**
* 文件大小(字节)
*/
private Long fileSize;
/**
* 文件访问路径
*/
private String fileUrl;
/**
* oss中的key
*/
private String ossKey;
/**
* 上传的状态
*/
private Integer uploadStatus;
}
java
/**
* 文件上传请求参数
*/
@Data
@Builder
public class FileUploadRequest {
/**
* 文件目录
*/
private String category;
}
定义操作文件的接口
java
/**
* 操作文件的接口
*/
public interface FilesService {
FileUploadResponse upload(MultipartFile file, FileUploadRequest request, Long userId);
}
定义操作文件的实现类
java
/**
* 操作文件接口的实现类
*/
@Service
@Slf4j
public class FileServiceImpl implements FilesService {
@Override
public FileUploadResponse upload(MultipartFile file, FileUploadRequest request, Long userId) {
// 1. 校验文件(文件类型、文件大小)
// 2. 上传文件到阿里云OSS
// 3. 创建文件记录
// 4. 把excel转化成mysql的表
// 5. 构建响应
return null;
}
}
实现文件校验工具类(写在 common-service 模块下)
java
/**
* 文件校验的工具类
*/
@Component
public class FileValidationUtil {
// 文件扩展名支持列表
private static final List<String> SUPPORT_FILE_EXTENSIONS = Arrays.asList(
".xls", ".xlsx"
);
// 文件类型的支持列表
private static final List<String> SUPPORT_FILE_TYPE = Arrays.asList(
"application/vnd.ms-excel", // .xls
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" // .xlsx
);
public static String getContentType(String fileName) {
String extension = fileName.substring(fileName.lastIndexOf("."));
switch (extension) {
case ".xls":
return "application/vnd.ms-excel";
case ".xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
default:
return "";
}
}
// 获取文件扩展名
private static String getFileExtension(String fileName) {
if (fileName == null || !fileName.contains(".")) {
return "";
}
return fileName.substring(fileName.lastIndexOf("."));
}
// 校验文件格式是否支持
public static boolean validateFileFormat(MultipartFile file) {
if (file == null || file.isEmpty()) {
return false;
}
// 检查文件扩展名
String extension = getFileExtension(file.getOriginalFilename());
if (!SUPPORT_FILE_EXTENSIONS.contains(extension)) {
return false;
}
// 检查文件类型
String contentType = file.getContentType();
if (!SUPPORT_FILE_TYPE.contains(contentType)) {
return false;
}
return true;
}
/**
* 判断文件大小
* @param file 文件
* @return 是否超过50MB
*/
public static boolean ifOutOfLarge(MultipartFile file) {
return file.getSize() <= 50 * 1024 * 1024;
}
}
完善 FileServiceImpl 校验文件功能
java
// 1. 校验文件(文件类型、文件大小)
if (!FileValidationUtil.validateFileFormat(file)) {
throw new IllegalArgumentException("只能处理excel文件");
}
if (!FileValidationUtil.ifOutOfLarge(file)) {
throw new IllegalArgumentException("文件大小不能超过50MB");
}
接入 OSS
注册、登录阿里云
创建 AccessKey

开通 oss 服务

开通阿里云OSS免费试用

创建 bucket


创建 OssService 与 OssServiceImpl
java
/**
* 阿里云oss服务接口
*/
public interface OssService {
/**
* 上传文件到指定路径
* @param file 文件本身
* @param filePath 文件路径
* @return 可以访问的URL
*/
String uploadFile(MultipartFile file, String filePath);
}
java
/**
* oss服务的实现类
*/
@Service
@Slf4j
@ConditionalOnBean(OssConfig.class)
public class OssServiceImpl implements OssService {
@Override
public String uploadFile(MultipartFile file, String filePath) {
// 1.获取完整的文件流
try {
InputStream inputStream = file.getInputStream();
String objectKey = createKey(filePath, file.getOriginalFilename());
PutObjectRequest putObjectRequest = new PutObjectRequest(
);
// 2. 调用oss的客户端直接上传代码
// 3. 返回上传后的url地址
return "";
}
}
PutObjectRequest 需要 有 BuckName 配置相关的东西,所以这里创建一个 OssConfig 来记录阿里云配置
java
/**
* 阿里云配置
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "aliyun.oss")
@ConditionalOnProperty(prefix = "aliyun.oss", name = "access-key-id")
public class OssConfig {
/**
* 访问断点
*/
private String endPoint;
/**
* 秘钥ID
*/
private String accessKeyId;
/**
* 秘钥
*/
private String accessKeySecret;
/**
* 存储空间
*/
private String bucketName;
/**
* 最大文件限制(字节)
*/
private Long maxFileSize;
@Bean
public OSS ossClient() {
OSS oSSClient = new OSSClientBuilder().build(
this.endPoint,
this.accessKeyId,
this.accessKeySecret
);
return oSSClient;
}
}
@ConfigurationProperties(prefix = "aliyun.oss"):将配置文件中以 aliyun.oss 为前缀的属性,自动绑定到当前类的字段上。如 Spring 会自动把 aliyun.oss.access-key-id 的值赋给 accessKeyId 字段,aliyun.oss.endpoint 赋给 endpoint,以此类推。注意 :字段名要符合驼峰命名 规则(access-key-id → accessKeyId),Spring 会自动转换。
@ConditionalOnProperty(prefix = "aliyun.oss", name = "access-key-id"):条件注解,只有当配置文件中存在 aliyun.oss.access-key-id 这个属性时,这个 Bean 才会被创建。告诉 Spring ------"如果配置文件里有 OSS 的 AccessKey,才加载这个配置类;如果没有,就不加载。"
完善OssServiceImpl
java
/**
* oss服务的实现类
*/
@Service
@Slf4j
@ConditionalOnBean(OssConfig.class)
public class OssServiceImpl implements OssService {
@Autowired
private OssConfig ossConfig;
@Autowired
private OSS ossClient;
@Override
public String uploadFile(MultipartFile file, String filePath) {
// 1.获取完整的文件流
try {
InputStream inputStream = file.getInputStream();
String objectKey = createKey(filePath, file.getOriginalFilename());
PutObjectRequest putObjectRequest = new PutObjectRequest(
ossConfig.getBucketName(),
objectKey,
inputStream
);
// 2. 调用oss的客户端直接上传代码
PutObjectResult putObjectResult = ossClient.putObject(putObjectRequest);
// 3. 返回上传后的url地址
return getUrl(objectKey);
} catch (IOException e) {
log.error("上传oss失败{}", file.getOriginalFilename());
throw new RuntimeException(e);
}
}
private String createKey(String filePath, String fileName) {
if (filePath == null || filePath.trim().isEmpty()) {
return "uploads/" +fileName;
} else {
return filePath + fileName;
}
}
private String getUrl(String objectKey) {
return String.format("https://%s.%s/%s",
ossConfig.getBucketName(),
ossConfig.getEndPoint(),
objectKey
);
}
}
PutObjectRequest 构造方法
java
PutObjectRequest putObjectRequest = new PutObjectRequest(
ossConfig.getBucketName(), // 参数1:bucket名称
objectKey, // 参数2:对象键(文件在OSS中的完整路径)
inputStream // 参数3:文件输入流
);
完善 FileServiceImpl 步骤二上传 文件到阿里云OSS
java
// 2. 上传文件到阿里云OSS
String fileUrl = ossService.uploadFile(file, request.getCategory());
Excel 转换成 mysql 的表
建表
创建 ExcelToTableService 与 ExcelToTableServiceImpl
java
/**
* 用于做excel到mysql表转换的服务接口
*/
public interface ExcelToTableService {
/**
* 把已有的excel文件转换成mysql表
*/
List<String> convertExcelToTable(MultipartFile file, Long fileId);
}
java
/**
* excel转换mysql表的实现类
*/
@Slf4j
@Service
public class ExcelToTableServiceImpl implements ExcelToTableService {
@Override
public List<String> convertExcelToTable(MultipartFile file, Long fileId) {
// 1. 先获取excel的数据
// 2. 获取sheet的名称
// 3. 根据文件信息做实际table的转换
return "";
}
}
获取 sheet 个数方法,判断是否有数据(在ExcelToTableServiceImpl 中实现)
java
private int getSheetCount(MultipartFile file) {
byte[] fileBytes = getFileBytes(file);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(fileBytes);
ExcelReader excelReader = EasyExcel.read(byteArrayInputStream).build();
List<ReadSheet> sheets = excelReader.excelExecutor().sheetList();
return sheets.size();
}
private byte[] getFileBytes(MultipartFile file) {
try {
return file.getBytes();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
完善 convertExcelToTable 方法
java
// 1. 先获取excel的数据
int sheetCount = getSheetCount(file);
if (sheetCount == 0) {
throw new IllegalArgumentException("excel是个空文件");
}
获取sheet名称的实现(在ExcelToTableServiceImpl 中实现)
由于 一个文件有多个 sheet, 对应多张表,sheet名是生成表名的一部分
java
public List<String> getSheetNames(MultipartFile file) {
byte[] fileBytes = getFileBytes(file);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(fileBytes);
ExcelReader excelReader = EasyExcel.read(byteArrayInputStream).build();
List<ReadSheet> sheets = excelReader.excelExecutor().sheetList();
List<String> names = new ArrayList<>();
for (ReadSheet readSheet :sheets) {
names.add(readSheet.getSheetName());
}
return names;
}
完善 convertExcelTTable 方法
java
// 2. 获取sheet的名称
List<String> sheetNames = getSheetNames(file);
实现 convertExcelToTable 方法
java
private List<String> convertExcelToTable(MultipartFile file, Long fileId, List<String> sheetNames) {
// 1. 先生成表名
// 2. 创建表
// 3. 获取file的数据
// 4. 把file的数据转换成插入的sql
// 5. 执行sql
}
写分布式锁(写在 common-service 里)
在生成随机表名的时候,需要考虑并发性。需要使用分布式锁保证表名的全局唯一
实现 DistributeLockService 与 DistributeLockServiceImpl
java
/**
* 分布式锁服务
*/
public interface DistributeLockService {
boolean tryLock(String lockKey, String lockValue, long expireTime);
boolean releaseLock(String lockKey, String lockValue);
}
expireTime 为了防止程序挂了无法释放锁
java
/**
* 分布式锁的实现类
*/
@Service
@Slf4j
public class DistributeLockServiceImpl implements DistributeLockService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "lock_prefix:";
private final DefaultRedisScript<Long> redisScript;
private static final String SCRIPT_TEXT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
public DistributeLockServiceImpl() {
redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(SCRIPT_TEXT);
redisScript.setResultType(Long.class);
}
@Override
public boolean tryLock(String lockKey, String lockValue, long expireTime) {
// 1. 补全完整的key
String fullLockKey = LOCK_PREFIX + lockKey;
// 2. 把value写入redis
Boolean result = redisTemplate.opsForValue().setIfAbsent(
fullLockKey,
lockValue,
expireTime,
TimeUnit.SECONDS
);
if (result) {
log.info("成功获取分布式锁{}:{}", fullLockKey, lockValue);
} else {
log.error("获取分布式锁{}失败, 被别的请求站上了", lockKey);
}
return result;
}
@Override
public boolean releaseLock(String lockKey, String lockValue) {
// 1. 补全完整的key
String fullLockKey = LOCK_PREFIX + lockKey;
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList(fullLockKey),
lockValue
);
boolean released = result != null && result > 0;
if (released) {
log.info("成功释放分布式锁{}", lockKey);
} else {
log.error("释放分布式锁失败{}", lockKey);
}
return released;
}
}
fullLockKey 标识「锁哪把资源」,lockValue 标识「谁持有这把锁」。
Lua 脚本:
释放锁必须同时满足:
- 校验:当前 Redis 里的 value 还是我的
lockValue - 删除:校验通过才
DEL - 原子性:两步必须一气呵成,中间不能被别的命令插入

java
public DistributeLockServiceImpl() {
redisScript = new DefaultRedisScript<>(); // 1. 创建脚本对象
redisScript.setScriptText(SCRIPT_TEXT); // 2. 设置 Lua 脚本内容
redisScript.setResultType(Long.class); // 3. 设置返回值类型
}
这是一个构造函数,在 DistributeLockServiceImpl 类被 Spring 创建 Bean 实例时执行,用于初始化 Lua 脚本对象。
java
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList(fullLockKey),
lockValue
);
上述代码执行 Lua 脚本的核心调用,将 Lua 脚本发送给 Redis 执行并返回结果。

完善ExcelToTableServiceImpl -- 使用分布式锁生成表名
java
private String createTableName() {
// 1. 设置一个重试次数
int maxRetries = 5;
int retryCount = 1;
// 2. 设置lockValue
String lockValue = UUID.randomUUID().toString();
// 3. 获取了锁之后再去生成表名, 否则就重复
while (retryCount <= maxRetries) {
String baseName = "excel_table";
String tableName = baseName + String.valueOf(System.currentTimeMillis()).substring(8);
String lockKey = tableName;
boolean lockAcquired = distributeLockService.tryLock(lockKey, lockValue, 60);
if (lockAcquired) {
// 获取分布式锁成功,对表名做个唯一判断
if (tableExists(tableName)) {
distributeLockService.releaseLock(lockKey, lockValue);
} else {
return tableName;
}
}
retryCount++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
throw new RuntimeException("生成唯一表名失败");
}
private boolean tableExists(String tableName) {
String sql = "select count(1) from information_schema.tables where table_schema = DATABASE() and table_name = ?";
return jdbcTemplate.queryForObject(sql, Integer.class, tableName) > 0;
}
实现创建表逻辑
java
/**
* 创建表
* @param tableName 表名
* @param file 文件
* @param fileId 文件ID
* @param sheetIndex sheet所在的索引位置
*/
private void createTableFromExcel(String tableName, MultipartFile file, Long fileId, int sheetIndex) {
// 1. 读取excel的表头
// 2. 根据excel的表头生成table的字段
// 3. 生成sql语句 create table
// 4. 执行sql语句
}
实现读取 excel 表头方法
java
private List<String> readHeader(MultipartFile file, int sheetIndex) throws IOException {
byte[] fileBytes = getFileBytes(file);
List<String> headers = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes)) {
AnalysisEventListener<Map<Integer, String>> headerListener = new AnalysisEventListener<>() {
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
int maxCol = data.keySet().stream().max(Integer::compareTo).orElse(-1);
for (int i = 0; i <= maxCol; i++) {
headers.add(data.getOrDefault(i, ""));
}
// 读取到表头后立即停止解析
throw new ExcelAnalysisStopException("Header row read completed");
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// no-op
}
};
try {
EasyExcel.read(bais, headerListener)
.sheet(sheetIndex)
.headRowNumber(0) // 第一行就是表头
.doRead();
} catch (ExcelAnalysisStopException ignore) {
// 正常用于提前结束读取
}
}
if (headers.isEmpty()) {
throw new IllegalArgumentException("指定 Sheet(索引 " + sheetIndex + ") 没有表头或无法读取表头");
}
return headers;
}
Map<Integer, String> data:这个data就是 EasyExcel 读取到的一行数据,用"列索引→值"的 Map
data.keySet() // 1. 获取所有列的索引 .stream() // 2. 转为流(便于操作) .max(Integer::compareTo) // 3. 找出最大的索引值 .orElse(-1); // 4. 如果没有数据,返回 -1
// 写法1:方法引用(我的代码)
.max(Integer::compareTo)
// 写法2:Lambda 表达式
.max((a, b) -> a.compareTo(b))
// 写法3:完整匿名内部类(Java 8 之前)
.max(new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a.compareTo(b);
}
})
补充 createTableFromExcel 方法中
java
/**
* 创建表
* @param tableName 表名
* @param file 文件
* @param fileId 文件ID
* @param sheetIndex sheet所在的索引位置
*/
private void createTableFromExcel(String tableName, MultipartFile file, Long fileId, int sheetIndex) {
// 1. 读取excel的表头
try {
List<String> headers = readHeader(file, sheetIndex);
if (headers.isEmpty()) {
throw new IllegalArgumentException("sheet没有表头");
}
// 2. 根据excel的表头生成table的字段
// 3. 生成sql语句 create table
// 4. 执行sql语句
} catch(IOException e) {
throw new RuntimeException(e);
}
}
实现 createColumNames 方法,生成表的列名
由于表头可能是中文,我发直接作为mysql 字段,需要 createColumNames 方法对字段做转义
创建 PinyinUtil
java
/**
* 中文转拼音工具类
*/
public class PinyinUtil {
public static String chineseToPinYin(String chinese) {
StringBuilder pinyin = new StringBuilder();
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
// 设置格式
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
format.setVCharType(HanyuPinyinVCharType.WITH_V);
char[] chars = chinese.toCharArray();
for(int i =0; i< chars.length; i++) {
char c = chars[i];
if (Character.toString(c).matches("[\\u4E00-\\u9FA5]+")) {
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c, format);
if (pinyinArray != null && pinyinArray.length > 0) {
pinyin.append(pinyinArray[0]);
if (i < chars.length -1) {
pinyin.append("_");
}
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
throw new RuntimeException(e);
}
} else if (Character.isLetterOrDigit(c)) {
pinyin.append(Character.toLowerCase(c));
if (i < chars.length - 1 && isSpecialChar(chars[i+1])) {
pinyin.append("_");
}
}
else if (c == '_') {
pinyin.append("_");
}
else {
pinyin.append("_");
}
}
return clean(pinyin.toString());
}
private static boolean isSpecialChar(char c) {
return !Character.isLetterOrDigit(c) && c != '_';
}
private static String clean(String str) {
// 第1行:去掉开头和结尾的所有下划线
str = str.replaceAll("^_+|_+$", "");
// 第2行:将中间连续的多个下划线合并为单个
str = str.replaceAll("_+", "_");
return str;
}
}
java
private String createColumnName(String headerName) {
// 1. 删掉首尾空格
String cleanName = headerName.trim();
// 2. 检查是否包含中文
if (!cleanName.matches(".*[\\u4e00-\\u9fa5].*")) { //判断字符串中是否包含至少一个中文字符
// 将所有非字母数字的字符替换为短横线 -
cleanName = cleanName.replaceAll("[^a-zA-Z0-9]", "-");
//判断字符串是否以字母(a-z 或 A-Z)开头
if (!cleanName.matches("[^a-zA-Z].*")) {
cleanName = "col_" +cleanName;
}
} else {
cleanName = PinyinUtil.chineseToPinYin(cleanName);
cleanName = "col_" + cleanName;
}
if (cleanName.length() > 64) {
cleanName = cleanName.substring(0, 64);
// 去掉字符串末尾的所有下划线 _
cleanName = cleanName.replaceAll("_+$", "");
}
if (!cleanName.matches("^[a-zA-Z][a-zA-Z0-9]*$")) {
cleanName = "filed_" + cleanName;
}
return cleanName.trim();
}
补全 createTableFromExcel 创建表方法
java
/**
* 创建表
* @param tableName 表名
* @param file 文件
* @param fileId 文件ID
* @param sheetIndex sheet所在的索引位置
*/
private void createTableFromExcel(String tableName, MultipartFile file, Long fileId, int sheetIndex) {
// 1. 读取excel的表头
try {
List<String> headers = readHeader(file, sheetIndex);
if (headers.isEmpty()) {
throw new IllegalArgumentException("sheet没有表头");
}
// 2. 根据excel的表头生成table的字段
List<String> safeHeaders = new ArrayList<>();
for (String header : headers) {
String safeHeader = createColumnName(header);
safeHeaders.add(safeHeader);
}
System.out.println("字段长度" + safeHeaders.size());
// 3. 生成sql语句 create table
StringBuilder sql = new StringBuilder();
sql.append("create table `").append(tableName).append("` (");
sql.append(" `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',");
for (int i =0; i < safeHeaders.size(); i++) {
String safeHeader = safeHeaders.get(i);
String header = headers.get(i);
sql.append("`").append(safeHeader).append("` TEXT COMMENT '").append(header).append("',");
}
sql.setLength(sql.length() - 1);
sql.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Excel导入表'");
log.info("生成的sql{}", sql);
// 4. 执行sql语句
jdbcTemplate.execute(sql.toString());
// 记录 excel表头 和 mysql表 的映射关系
} catch (IOException e) {
throw new RuntimeException(e);
}
}
实现 excel表头 和 mysql表 的映射关系的记录
Excel 表头不能直接当 MySQL 列名用,建表时必须改成合法字段名,所以要单独存一份「原始表头 ↔ 数据库字段」的对应关系。若不记录映射,之后就无法知道「这个 DB 字段对应用户原来看到的哪一列」。
编写实体类 FileMappingEntity
java
/**
* 字段映射关系表的实体类
*/
@Data
@TableName("field_mappings")
public class FieldMappingEntity {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 文件ID
*/
private Long fileId;
/**
* 表名
*/
private String tableName;
/**
* 动态字段名
*/
private String dbFieldName;
/**
* excel中的表头
*/
private String originalHeader;
/**
* excel表头的顺序
*/
private Integer fieldOrder;
}
创建 FieldMappingMapper
java
/**
* 字段映射关系表mapper
*/
@Mapper
public interface FieldMappingMapper extends BaseMapper<FieldMappingEntity> {
@Insert("<script>" +
"INSERT INTO field_mappings (file_id, table_name, db_field_name, original_header, field_order) " +
"VALUES " +
"<foreach collection='mappings' item='mapping' separator=','> " +
"(#{mapping.fileId}, #{mapping.tableName}, #{mapping.dbFieldName}, #{mapping.originalHeader}, #{mapping.fieldOrder}) " +
"</foreach>" +
"</script>"
)
int batchInsert(@Param("mappings")List<FieldMappingEntity> mappings);
}
继承 BaseMapper<FieldMappingEntity> 后,自动拥有以下方法:
|----------------|------------|
| insert() | 插入单条记录 |
| deleteById() | 根据 ID 删除 |
| updateById() | 根据 ID 更新 |
| selectById() | 根据 ID 查询 |
| selectList() | 条件查询列表 |
| selectPage() | 分页查询 |
| 等等... | 基础 CRUD 操作 |
方法签名 int batchInsert(@Param("mappings") List<FieldMappingEntity> mappings);
| 部分 | 说明 |
|---|---|
int |
返回值:成功插入的记录条数 |
@Param("mappings") |
给参数起别名,在 SQL 中用 #{mappings} 引用 |
List<FieldMappingEntity> |
传入的映射实体列表 |
<script>标签
java
@Insert("<script>" +
"INSERT INTO field_mappings (file_id, table_name, db_field_name, original_header, field_order) " +
"VALUES " +
"<foreach collection='mappings' item='mapping' separator=','> " +
"(#{mapping.fileId}, #{mapping.tableName}, #{mapping.dbFieldName}, #{mapping.originalHeader}, #{mapping.fieldOrder}) " +
"</foreach>" +
"</script>"
)
| 标签 | 作用 |
|---|---|
<script> |
告诉 MyBatis 这是一个动态 SQL,需要解析 |
<foreach> |
循环遍历集合,生成多条 VALUES |
collection='mappings' |
遍历的参数名 |
item='mapping' |
循环中的单个元素变量名 |
separator=',' |
每个元素之间用逗号分隔 |
举例:输入数据
java
List<FieldMappingEntity> mappings = Arrays.asList(
FieldMappingEntity.builder()
.fileId(10000001L)
.tableName("sales_data")
.dbFieldName("sale_date")
.originalHeader("日期")
.fieldOrder(1)
.build(),
FieldMappingEntity.builder()
.fileId(10000001L)
.tableName("sales_data")
.dbFieldName("salesperson")
.originalHeader("销售员")
.fieldOrder(2)
.build()
);
生成的 SQL:
java
INSERT INTO field_mappings (file_id, table_name, db_field_name, original_header, field_order)
VALUES
(10000001, 'sales_data', 'sale_date', '日期', 1),
(10000001, 'sales_data', 'salesperson', '销售员', 2)
创建 FieldMappingService 与 FieldMappingServiceImpl
java
/**
* 保存字段关系的实现类
*/
@Service
@Slf4j
public class FieldMappingServiceImpl implements FieldMappingService {
@Autowired
private FieldMappingMapper fieldMappingMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public int saveMappings(Long fileId, String tableName, List<String> originalHeader, List<String> dbFieldName) {
List<FieldMappingEntity> mappingEntities = new ArrayList<>();
for (int i = 0; i < originalHeader.size(); i++) {
String excelHeader = originalHeader.get(i);
String dbName = dbFieldName.get(i);
FieldMappingEntity fieldMappingEntity = new FieldMappingEntity();
fieldMappingEntity.setFileId(fileId);
fieldMappingEntity.setTableName(tableName);
fieldMappingEntity.setDbFieldName(dbName);
fieldMappingEntity.setOriginalHeader(excelHeader);
fieldMappingEntity.setFieldOrder(i);
mappingEntities.add(fieldMappingEntity);
}
return fieldMappingMapper.batchInsert(mappingEntities);
}
}
补全 createTableFromExcel 中记录映射关系内容
java
// 记录 excel表头 和 mysql表 的映射关系
fieldMappingService.saveMappings(fileId, tableName, headers, safeHeaders);
完善 convertExcelToTable 方法
java
private List<String> convertExcelToTable(MultipartFile file, Long fileId, List<String> sheetNames) {
// 1. 先生成表名
String baseTableName = createTableName();
List<String> tableNames = new ArrayList<>();
// 2. 创建表
for (int i =0; i < sheetNames.size(); i++) {
String tableName = baseTableName + "_sheet" + i;
createTableFromExcel(tableName, file, fileId, i);
// 3. 获取file的数据
// 4. 把file的数据转换成插入的sql
// 5. 执行sql
}
return tableNames;
}
实现数据的插入
创建 insertData 方法实现数据的插入
insertData(tableName, file, sheetIndex)
│
├─① 元数据准备:表头、表结构、列名映射
│
├─② 按文件大小分流读数
│ ├─ > 5MB → EasyExcel 分批流式(每批 1000 行)
EasyExcel 逐行读取
↓
每 1000 行触发一次回调
↓
将 1000 行数据转为 SQL 参数
↓
执行批量插入(1000 条/批)
↓
清空缓冲区,继续读取
│ └─ ≤ 5MB → POI 一次性读入内存
│
└─③ executeBatchInsert:拼多值 INSERT,每批最多 1000 行执行
java
public void insertData(String tableName, MultipartFile file, int sheetIndex) {
// 1. 实现2种读取数据的方式 以文件的大小作为判断的标准
long fileSize = 5 * 1024 * 1024;
try {
// 获取中文表头
List<String> originalHeaders = readHeader(file, sheetIndex);
// 获取表的结构才能实现数据的插入
List<Map<String, Object>> tableStructure = getTableStructure(tableName);
List<String> columns = new ArrayList<>();
for (Map<String, Object> column : tableStructure) {
String columnName = (String) column.get("FieLd");
if (!columnName.equals("id")) {
columns.add(columnName);
}
}
Map<String, String> headerMapping = new HashMap<>();
for (int i = 0; i< originalHeaders.size(); i++) {
String originalHeader = originalHeaders.get(i);
String safeColum = columns.get(i);
headerMapping.put(originalHeader, safeColum);
}
// 2. 大于5MB,使用流式处理
if (file.getSize() > fileSize) {
// 初始化一个计数器
final int[] streamInserted = {0};
final Map<String, String> finalHeaderMapping = headerMapping;
final List<String> finalSafeColumnNames = columns;
final int finalSheetIndex = sheetIndex;
// 流式地获取数据,逐个加入sql语句
processExcelInBatches(file, finalSheetIndex, batchData -> {
List<Object[]> batchArgs = new ArrayList<>();
for (Map<String, Object> row : batchData) {
Object[] args = new Object[finalSafeColumnNames.size()];
for (int i = 0; i <finalSafeColumnNames.size(); i++) {
String safeColumnName = finalSafeColumnNames.get(i);
String originalHeader = null;
for (Map.Entry<String, String> entry : finalHeaderMapping.entrySet()) {
if (entry.getValue().equals(safeColumnName)) {
originalHeader = entry.getKey();
break;
}
}
// 根据原始列名从row中获取对应的数据值
if (originalHeader != null) {
args[i] = row.get(originalHeader);
} else {
args[i] = null;
}
}
batchArgs.add(args);
}
// 每次将当前批次的数据拼成多值 INSERT,单条 SQL 最多 500 行
executeBatchInsert(tableName, finalSafeColumnNames, batchArgs);
streamInserted[0] += batchArgs.size();
});
} else {
// 3. 否则一次性读取数据
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
获取表结构方法的实现
java
// 获取表结构
private List<Map<String, Object>> getTableStructure(String tableName) {
String sql = "describe `" +tableName + "`";
return jdbcTemplate.queryForList(sql);
}
分批处理逻辑
java
processExcelInBatches(file, sheetIndex, batchData -> {
List<Object[]> batchArgs = new ArrayList<>();
for (Map<String, Object> row : batchData) {
// 转换数据...
batchArgs.add(args);
}
executeBatchInsert(tableName, columns, batchArgs);
});
这是一个 Lambda 表达式(匿名函数)-》 参数 -> { 方法体 }
等价于
java
processExcelInBatches(file, sheetIndex, new Consumer<List<Map<String, Object>>>() {
@Override
public void accept(List<Map<String, Object>> batchData) {
List<Object[]> batchArgs = new ArrayList<>();
for (Map<String, Object> row : batchData) {
// 转换数据...
batchArgs.add(args);
}
executeBatchInsert(tableName, columns, batchArgs);
}
});
1. Lambda 被定义 → 还没执行(像一个"待办事项")
2. 传给 processExcelInBatches() 方法
3. 方法内部在合适时机调用 batchProcessor.accept()
4. Lambda 里的代码才真正被执行
DESCRIBE 表名 返回的是元数据结果集,每一行大致是:
| Field | Type | Null | Key | Default | Extra |
|---|---|---|---|---|---|
| id | bigint | NO | PRI | null | auto_increment |
| col_xxx | text | YES | null |
queryForList 会把:
- 每一行 → 一个
Map - 列名 → Map 的 key(
String) - 单元格值 → Map 的 value(
Object,因为类型不一:字符串、数字、null 都有)
所以整体是 List<Map<String, Object>>:一行一个 Map
实现流式数据,逐个加入 sql 语句
java
private void processExcelInBatches(MultipartFile file,
int sheetIndex, Consumer<List<Map<String, Object>>> batchProcessor) {
// 1. 直接把文件转换成字节数组
byte[] fileBytes = getFileBytes(file);
// 2. 使用字节数组输入流创建EasyExcel读取器
// 3. 保存数据
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(fileBytes)) {
EasyExcel.read(byteArrayInputStream, new EasyExcelBatchReadListener(batchProcessor, EXCEL_BATCH_SIZE))
.sheet(sheetIndex)
.headRowNumber(0)
.doRead();
} catch (Exception e) {
throw new RuntimeException("处理大文件失败:"+e.getMessage());
}
}
private static class EasyExcelBatchReadListener extends AnalysisEventListener<Map<Integer, String>> {
private final Consumer<List<Map<String, Object>>> batchProcessor;
private final int batchSize;
private final List<String> headers = new ArrayList<>();
private final List<Map<String, Object>> currentBatch = new ArrayList<>();
private EasyExcelBatchReadListener(Consumer<List<Map<String, Object>>> batchProcessor, int batchSize) {
this.batchProcessor = batchProcessor;
this.batchSize = batchSize;
}
@Override
public void invoke(Map<Integer, String> integerStringMap, AnalysisContext analysisContext) {
if (headers.isEmpty()) {
int maxCol = integerStringMap.keySet().stream().max(Integer::compareTo).orElse(-1);
for (int i = 0; i <= maxCol; i++) {
headers.add(integerStringMap.getOrDefault(i, ""));
}
return;
}
Map<String, Object> rowData = new LinkedHashMap<>();
for (int i =0; i <headers.size(); i++) {
String header = headers.get(i);
String value = integerStringMap.getOrDefault(i, "");
rowData.put(header, value);
}
currentBatch.add(rowData);
if (currentBatch.size() >= batchSize) {
batchProcessor.accept(new ArrayList<>(currentBatch));
currentBatch.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
监听器的 invoke 逻辑:

解释 batchProcessor.accept(new ArrayList<>(currentBatch)):将当前批次的数据传递给外部处理器,外部处理器在 insertData 方法代码中
java
// 流式地获取数据,逐个加入sql语句
processExcelInBatches(file, finalSheetIndex, batchData -> {
List<Object[]> batchArgs = new ArrayList<>();
for (Map<String, Object> row : batchData) {
Object[] args = new Object[finalSafeColumnNames.size()];
for (int i = 0; i <finalSafeColumnNames.size(); i++) {
String safeColumnName = finalSafeColumnNames.get(i);
String originalHeader = null;
for (Map.Entry<String, String> entry : finalHeaderMapping.entrySet()) {
if (entry.getValue().equals(safeColumnName)) {
originalHeader = entry.getKey();
break;
}
}
// 根据原始列名从row中获取对应的数据值
if (originalHeader != null) {
args[i] = row.get(originalHeader);
} else {
args[i] = null;
}
}
batchArgs.add(args);
}
// 每次将当前批次的数据拼成多值 INSERT,单条 SQL 最多 500 行
executeBatchInsert(tableName, finalSafeColumnNames, batchArgs);
streamInserted[0] += batchArgs.size();
});
java
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); // 接收一个参数,无返回值
}
实现批量插入执行器:executeBatchInsert
java
private void executeBatchInsert(String tableName, List<String> columns, List<Object[]> args) {
if (args == null || args.isEmpty()) {
return;
}
int columnCount = columns.size();
for (int start = 0; start < args.size(); start += SQL_BATCH_SIZE) {
int end = Math.min(start + SQL_BATCH_SIZE, args.size());
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("insert into `").append(tableName).append("` (");
for (String name : columns) {
sqlBuilder.append("`").append(name).append("`,");
}
sqlBuilder.setLength(sqlBuilder.length() - 1);
sqlBuilder.append(") values ");
List<Object> paramList = new ArrayList<>();
for (int i = start; i < end; i++) {
Object[] rowValues = args.get(i);
sqlBuilder.append("(");
for (int j = 0; j < columnCount; j++) {
sqlBuilder.append("?,");
paramList.add(rowValues[j]);
}
sqlBuilder.setLength(sqlBuilder.length() - 1);
sqlBuilder.append("),");
}
sqlBuilder.setLength(sqlBuilder.length() - 1); // 去掉最后一个逗号
jdbcTemplate.update(sqlBuilder.toString(), paramList.toArray());
}
}

一次性处理逻辑:
java
public void insertData(String tableName, MultipartFile file, int sheetIndex) {
// 1. 实现2种读取数据的方式 以文件的大小作为判断的标准
long fileSize = 5 * 1024 * 1024;
try {
List<String> originalHeaders = readHeader(file, sheetIndex);
List<Map<String, Object>> tableStructure = getTableStructure(tableName);
List<String> columns = new ArrayList<>();
for (Map<String, Object> column : tableStructure) {
String columnName = (String) column.get("FieLd");
if (!columnName.equals("id")) {
columns.add(columnName);
}
}
Map<String, String> headerMapping = new HashMap<>();
for (int i = 0; i< originalHeaders.size(); i++) {
String originalHeader = originalHeaders.get(i);
String safeColum = columns.get(i);
headerMapping.put(originalHeader, safeColum);
}
// 2. 大于5MB,使用流式处理
if (file.getSize() > fileSize) {
} else {
// 3. 否则一次性读取数据
byte[] fileBytes = getFileBytes(file);
List<Object[]> allArgs = new ArrayList<>();
try(Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(fileBytes))) {
Sheet sheet = workbook.getSheetAt(sheetIndex);
int totalRows = sheet.getLastRowNum();
// 遍历每一行
for (int i = 1; i<= totalRows; i++) {
Row row = sheet.getRow(i);
if (row == null) {
continue;
}
// 读取每行数据到 map
Map<String, Object> rowData = new LinkedHashMap<>();
for (int j = 0; j <originalHeaders.size(); j++) {
Cell cell = row.getCell(j);
rowData.put(originalHeaders.get(j), getCellValue(cell));
}
Object[] args = new Object[columns.size()];
for (int k = 0; k < columns.size(); k++) {
String safeColumName = columns.get(k);
String originHeader = null;
for (Map.Entry<String, String> entry : headerMapping.entrySet()) {
if (entry.getValue().equals(safeColumName)) {
originHeader = entry.getKey();
break;
}
}
if (originHeader != null) {
args[k] = rowData.get(originHeader);
} else {
args[k] = null;
}
}
allArgs.add(args);
}
}
executeBatchInsert(tableName, columns, allArgs);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
提取单元格数据代码
java
// 提取excel单元格值
private String getCellValue(Cell cell) {
return switch (cell.getCellType()) {
case STRING -> cell.getStringCellValue().trim();
case NUMERIC -> {
if (DateUtil.isCellDateFormatted(cell)) {
Date date = cell.getDateCellValue();
SimpleDateFormat sdf = new SimpleDateFormat("YY-MM-dd HH:mm:ss");
yield sdf.format(date);
} else {
double number = cell.getNumericCellValue();
yield String.valueOf(number);
}
}
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
case FORMULA -> String.valueOf(cell.getCellFormula());
default -> "";
};
}
处理过程:

最终 allArgs 收集的是

补全 convertExcelToTable 转换单个sheet方法
java
private List<String> convertExcelToTable(MultipartFile file, Long fileId, List<String> sheetNames) {
// 1. 先生成表名
String baseTableName = createTableName();
List<String> tableNames = new ArrayList<>();
// 2. 创建表
for (int i =0; i < sheetNames.size(); i++) {
String tableName = baseTableName + "_sheet" + i;
createTableFromExcel(tableName, file, fileId, i);
// 3. 获取file的数据
// 4. 把file的数据转换成插入的sql
// 5. 执行sql
insertData(tableName, file, i);
tableNames.add(tableName);
}
return tableNames;
}
补全 convertExcelToTable 转换一个文件,多个 sheet 方法
java
@Override
public List<String> convertExcelToTable(MultipartFile file, Long fileId) {
// 1. 先获取excel的数据
int sheetCount = getSheetCount(file);
if (sheetCount == 0) {
throw new IllegalArgumentException("excel是个空文件");
}
// 2. 获取sheet的名称
List<String> sheetNames = getSheetNames(file);
// 3. 根据文件信息做实际table的转换
return convertExcelToTable(file, fileId, sheetNames);
}
根据已实现的方法,完善 FileServiceImpl 里的 upload 方法
java
/**
* 操作文件接口的实现类
*/
@Service
@Slf4j
public class FileServiceImpl implements FilesService {
@Autowired
private OssService ossService;
@Autowired
private ExcelToTableService excelToTableService;
@Autowired
private FilesMapper filesMapper;
@Autowired
private FileTableMappingService fileTableMappingService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private FieldMappingService fieldMappingService;
@Override
public FileUploadResponse upload(MultipartFile file, FileUploadRequest request, Long userId) {
// 1. 校验文件(文件类型、文件大小)
if (!FileValidationUtil.validateFileFormat(file)) {
throw new IllegalArgumentException("只能处理excel文件");
}
if (!FileValidationUtil.ifOutOfLarge(file)) {
throw new IllegalArgumentException("文件带下不能超过50MB");
}
// 2. 上传文件到阿里云OSS
String fileUrl = ossService.uploadFile(file, request.getCategory());
// 3. 创建文件记录
// 4. 把excel转化成mysql的表
List<String> tableNames = excelToTableService.convertExcelToTable(file, filesEntity.getId());
fileTableMappingService.saveMappings(filesEntity.getId(), tableNames, null);
// 5. 构建响应
FileUploadResponse response = FileUploadResponse.builder()
.fileId(filesEntity.getId())
.fileName(filesEntity.getFileName())
.fileSize(filesEntity.getFileSize())
.fileUrl(fileUrl)
.uploadStatus(1)
.ossKey(filesEntity.getOssKey())
.build();
return response;
}
实现 记录文件原信息 的功能
创建 FilesEntity 实体类
java
/**
* 文件元信息
*/
@Data
@Slf4j
@Builder
@TableName("files")
public class FilesEntity {
/**
* 文件ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户ID
*/
@TableField("user_id")
private Long userId;
/**
* 文件名
*/
@TableField("file_name")
private String fileName;
/**
* 文件上传路径
*/
@TableField("file_path")
private String filePath;
/**
* 文件大小
*/
@TableField("file_size")
private Long fileSize;
/**
* 保存在oss中的标识
*/
@TableField("oss_key")
private String ossKey;
/**
* 文件上传状态
*/
@TableField("upload_status")
private Integer uploadStatus;
}
在 upload 方法中创造 FilesEntity 实例
java
// 3. 创建文件记录
FilesEntity filesEntity = FilesEntity.builder()
.userId(userId)
.fileName(file.getOriginalFilename())
.filePath(request.getCategory() == null ? "upload" : request.getCategory())
.fileSize(file.getSize())
.ossKey(getOssKey(fileUrl))
.build();
filesMapper.insert(filesEntity);
getOssKey(fileUrl) 方法的实现
java
// 根据文件地址获取key
private String getOssKey(String fileUrl) {
String[] parts = fileUrl.split("/");
if (parts.length >= 4) {
StringBuilder key = new StringBuilder();
for (int i = 3; i <parts.length; i++) {
if (i >3) {
key.append("/");
}
key.append(parts[i]);
}
return key.toString();
}
return "";
}
实现 FilesMapper 把 filesEntity 插入数据库
java
/**
* 文件访问数据库的 mapper
*/
@Mapper
public interface FilesMapper extends BaseMapper<FilesEntity> {
}
FileTableMappingService
java
/**
* 文件映射表的服务类
*/
public interface FileTableMappingService {
void saveMappings(Long fileId, List<String> tableNames, List<String> sheetNames);
}
FileTableMappingEntity
java
/**
* 文件sheet和mysql表的映射关系
*/
@Data
@TableName("file_table_mappings")
public class FileTableMappingEntity {
@TableId(type = IdType.AUTO)
private Long id;
private Long fileId;
private String tableName;
private Integer sheetIndex;
private String sheetName;
}
FileTableMappingMapper
java
/**
* 文件与表映射mapper
*/
@Mapper
public interface FileTableMappingMapper extends BaseMapper<FileTableMappingEntity> {
}
FileTableMappingServiceImpl
java
/**
* 文件映射表的服务实现类
*/
@Service
public class FileTableMappingServiceImpl implements FileTableMappingService {
@Autowired
private FileTableMappingMapper fileTableMappingMapper;
@Override
public void saveMappings(Long fileId, List<String> tableNames, List<String> sheetNames) {
for (int i = 0; i <tableNames.size(); i++) {
FileTableMappingEntity fileTableMappingEntity = new FileTableMappingEntity();
fileTableMappingEntity.setFileId(fileId);
fileTableMappingEntity.setTableName(tableNames.get(i));
if (sheetNames != null && sheetNames.get(i) != null) {
fileTableMappingEntity.setSheetName(sheetNames.get(i));
}
fileTableMappingEntity.setSheetIndex(i);
fileTableMappingMapper.insert(fileTableMappingEntity);
}
}
}
完善 Controller 接口
java
/**
* 文件服务控制器
*/
@RestController
@RequestMapping("/files")
public class FilesController {
@Autowired
private FilesService filesService;
@Autowired
private JwtUtil jwtUtil;
/**
* 单个文件上传接口
*/
@PostMapping("/upload/single")
@LogOperation("文件上传")
public Result<FileUploadResponse> uploadFile(
@RequestParam("file")MultipartFile file,
@RequestHeader("Authorization")String authorization,
@Valid FileUploadRequest request
) {
Long userId = jwtUtil.getUserIdByAuthorization(authorization);
if (userId == null) {
return Result.badRequest("无效的令牌");
}
FileUploadResponse response = filesService.upload(file, request, userId);
return Result.success("文件上传成功", response);
}