【文件上传管理系统】实战详解 SpringBoot + Vue.js
一、介绍
这是一个基于 Spring Boot + Vue.js 的完整文件上传管理系统,提供了文件的上传、管理、查看、删除等功能。系统采用前后端分离架构,具有良好的用户界面和完善的功能体验。
二、核心功能
- 多文件上传: 支持同时上传多个文件,支持拖拽上传
- 文件管理: 查看文件列表、文件信息、文件大小等
- 文件操作: 文件预览、文件删除、文件搜索
- 用户体验: 现代化界面设计、响应式布局、操作反馈
三、效果展示
1. 整体页面效果

2. 上传文件功能
(1)选择图片,文件或视频

(2)文件上传到暂存区

(3)取消上传功能

(4)上传成功效果图

3. 文件列表管理
(1)搜索功能

(2)查看功能


(3)删除功能


四、技术架构
1. 后端技术栈
|
技术 |
版本 |
用途 |
|
Spring Boot |
3.x |
应用框架 |
|
Spring MVC |
内置 |
Web 框架 |
|
MyBatis |
3.x |
ORM 框架 |
|
MySQL |
8.x |
数据库 |
|
Swagger |
3.x |
API 文档 |
|
Lombok |
1.18.x |
代码简化 |
2. 前端技术栈
|
技术 |
版本 |
用途 |
|
Vue.js |
3.x |
前端框架 |
|
JavaScript |
ES6+ |
脚本语言 |
|
CSS |
3 |
样式表 |
|
HTML |
5 |
页面结构 |
五、代码深度解析
1. 后端代码部分
(1)启动类 - UploadApplication.java
@SpringBootApplication
@MapperScan("com.breeze.upload.mapper") // 指定MyBatis映射器扫描路径
public class UploadApplication implements WebMvcConfigurer {
public static void main(String[] args) {
// 启动Spring Boot应用
SpringApplication.run(UploadApplication.class, args);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 配置上传文件的静态资源访问路径
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:" + System.getProperty("user.dir") + "/uploads/");
}
}
- @SpringBootApplication: 组合注解,包含@Configuration、@EnableAutoConfiguration、@ComponentScan
- @MapperScan: 告诉 MyBatis 扫描指定包下的 Mapper 接口
- main方法: 应用程序入口点
- addResourceHandlers: 配置静态资源访问,让前端可以直接访问上传的文件
(2)实体类 - File.java
@Data // Lombok注解,自动生成getter、setter、toString等方法
public class File {
private Long id; // 文件ID
private String originalName; // 原始文件名
private String fileName; // 处理后的文件名(唯一)
private String fileType; // 文件类型(MIME类型)
private Long fileSize; // 文件大小(字节)
private String filePath; // 文件存储路径
private LocalDateTime uploadTime; // 上传时间
private String fileUrl; // 文件访问URL
}
(3)数据访问层 - FileMapper.java
@Mapper // 标记为MyBatis映射器接口
public interface FileMapper {
// 插入文件信息
int insert(File file);
// 根据id查询文件信息
File findById(Long id);
// 查询所有文件信息
List<File> findAll();
// 根据id删除文件信息
int deleteById(Long id);
}
- @Mapper: 告诉 MyBatis 这是一个映射器接口
- 定义了基本的 CRUD 操作方法
- 方法名遵循 MyBatis 的命名规范,便于 XML 配置
(4)数据访问 XML 配置 - FileMapper.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接口 -->
<mapper namespace="com.breeze.upload.mapper.FileMapper">
<!-- 结果映射,将数据库字段映射到实体类属性 -->
<resultMap id="FileResultMap" type="com.breeze.upload.entity.File">
<id property="id" column="id"/>
<result property="originalName" column="original_name"/>
<result property="fileName" column="file_name"/>
<result property="fileType" column="file_type"/>
<result property="fileSize" column="file_size"/>
<result property="filePath" column="file_path"/>
<result property="uploadTime" column="upload_time"/>
<result property="fileUrl" column="file_url"/>
</resultMap>
<!-- 插入文件记录 -->
<insert id="insert" parameterType="com.breeze.upload.entity.File"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO files(original_name, file_name, file_type, file_size, file_path, upload_time)
VALUES(#{originalName}, #{fileName}, #{fileType}, #{fileSize}, #{filePath}, #{uploadTime})
</insert>
<!-- 根据ID查询文件 -->
<select id="findById" parameterType="java.lang.Long" resultMap="FileResultMap">
SELECT * FROM files WHERE id = #{id} LIMIT 1
</select>
<!-- 查询所有文件 -->
<select id="findAll" resultMap="FileResultMap">
SELECT * FROM files
</select>
<!-- 根据ID删除文件 -->
<delete id="deleteById" parameterType="java.lang.Long">
DELETE FROM files WHERE id = #{id}
</delete>
</mapper>
(5)服务层 - FileService.java
@Service // 标记为服务层组件
public class FileService {
@Autowired
private FileMapper fileMapper;
// 从配置文件中读取文件上传目录
@Value("${file.upload-dir:uploads}")
private String uploadDir;
/**
* 获取文件上传目录
*/
public String getUploadDir() {
return uploadDir;
}
/**
* 获取所有文件
*/
public List<File> getAllFiles() {
return fileMapper.findAll();
}
/**
* 根据ID获取文件
*/
public File getFileById(Long id) {
return fileMapper.findById(id);
}
/**
* 保存文件
*/
public File saveFile(MultipartFile multipartFile) throws IOException {
// 创建文件存储目录(如果不存在)
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 生成唯一文件名
String originalFilename = multipartFile.getOriginalFilename();
String fileExtension = getFileExtension(originalFilename);
String uniqueFilename = UUID.randomUUID().toString() + "." + fileExtension;
// 保存文件到磁盘
Path filePath = uploadPath.resolve(uniqueFilename);
Files.copy(multipartFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
// 生成文件访问URL
String fileUrl = "/" + uniqueFilename;
// 保存文件信息到数据库
File file = new File();
file.setOriginalName(originalFilename);
file.setFileName(uniqueFilename);
file.setFileType(multipartFile.getContentType());
file.setFileSize(multipartFile.getSize());
file.setFilePath(filePath.toString());
file.setUploadTime(LocalDateTime.now());
file.setFileUrl(fileUrl);
fileMapper.insert(file);
return file;
}
/**
* 删除文件
*/
public boolean deleteFile(Long id) {
// 先查询文件信息
File file = fileMapper.findById(id);
if (file == null) {
return false;
}
// 删除磁盘上的文件
try {
Path filePath = Paths.get(file.getFilePath());
Files.deleteIfExists(filePath);
} catch (IOException e) {
e.printStackTrace();
}
// 删除数据库记录
int result = fileMapper.deleteById(id);
return result > 0;
}
/**
* 获取文件扩展名
*/
private String getFileExtension(String fileName) {
if (fileName == null || fileName.lastIndexOf(".") == -1) {
return "";
}
return fileName.substring(fileName.lastIndexOf(".") + 1);
}
}
- @Service: 标记为 Spring 服务层组件
- @Value: 从配置文件中注入文件上传目录
- saveFile: 核心方法,包含文件保存的完整逻辑
- 创建目录(如果不存在)
- 生成唯一文件名(使用 UUID 避免重名)
- 保存文件到磁盘
- 保存文件信息到数据库
- deleteFile: 先删除磁盘文件,再删除数据库记录
(6)控制器 - FileController.java
/**
* @author 邂逅星河浪漫
* @date 2025/09/20
*/
@RestController
@RequestMapping("/api/files")
@CrossOrigin
@Tag(name = "文件上传接口",description = "文件上传,搜索,查看接口")
public class FileController {
@Autowired
private FileService fileService;
@Operation(summary = "上传文件",
description = "上传多个文件",
tags = {"文件上传"})
@PostMapping("/upload")
public ResponseEntity<?> uploadFiles(@RequestParam(value = "files", required = false) MultipartFile[] files) {
try {
// 检查是否有文件被上传
if (files == null || files.length == 0) {
return ResponseEntity.badRequest()
.body("没有文件被上传,请确保选择了文件");
}
List<File> uploadedFiles = new ArrayList<>();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
File uploadedFile = fileService.saveFile(file);
uploadedFiles.add(uploadedFile);
}
}
// 检查是否有有效文件被处理
if (uploadedFiles.isEmpty()) {
return ResponseEntity.badRequest()
.body("没有有效的文件被处理,请确保选择的文件不为空");
}
return ResponseEntity.ok(uploadedFiles);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("文件上传失败: " + e.getMessage());
}
}
/**
* @return
*/
@GetMapping
public ResponseEntity<List<File>> getAllFiles() {
List<File> files = fileService.getAllFiles();
return ResponseEntity.ok(files); // 返回所有文件
}
/**
* @param id
* @return
*/
@GetMapping("/{id}")
public ResponseEntity<File> getFileById(@PathVariable Long id) {
File file = fileService.getFileById(id);
if (file != null) {
return ResponseEntity.ok(file); // 文件存在
} else {
return ResponseEntity.notFound().build(); // 文件不存在
}
}
/**
* @param id
* @return
*/
@DeleteMapping("/{id}")
public ResponseEntity<String> deleteFile(@PathVariable Long id) {
boolean deleted = fileService.deleteFile(id); // 调用删除文件方法
if (deleted) {
return ResponseEntity.ok("文件删除成功");
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("文件不存在或删除失败");
}
}
/**
* @param filename
* @return
*/
@GetMapping("/view/{filename}")
public ResponseEntity<Resource> viewFile(@PathVariable String filename) {
try {
// 获取文件存储目录
String uploadDir = fileService.getUploadDir();
// 构建文件路径
Path filePath = Paths.get(uploadDir).resolve(filename); // 文件路径
Resource resource = new UrlResource(filePath.toUri()); // 创建资源对象
// 检查文件是否存在且可读
if (resource.exists() || resource.isReadable()) {
return ResponseEntity.ok() // 返回文件内容
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"")
.body(resource);
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); // 文件不存在
}
} catch (MalformedURLException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); // 文件路径错误
}
}
}
- @RestController: 组合@Controller和@ResponseBody
- @RequestMapping("/api/files"): 设置基础 URL 路径
- @CrossOrigin: 允许跨域请求,解决前后端分离的跨域问题
- @Operation: Swagger 注解,用于 API 文档
- 每个方法对应一个 HTTP 请求处理:
- uploadFiles: 处理文件上传
- getAllFiles: 获取所有文件列表
- getFileById: 根据 ID 获取单个文件
- deleteFile: 删除文件
- viewFile: 查看文件内容
(7)配置文件 - application.yaml
spring:
application:
name: upload
# 数据库配置
datasource:
url: jdbc:mysql://localhost:3306/file_upload?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username:
password:
driver-class-name: com.mysql.cj.jdbc.Driver
# 文件上传配置
servlet:
multipart:
max-file-size: 100MB # 单个文件最大
max-request-size: 100MB # 请求最大
# 静态资源配置
web:
resources:
static-locations: file:uploads/
# MyBatis配置
mybatis:
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名
mapper-locations: classpath:mapper/*.xml # mapper文件位置
# 文件存储目录
file:
upload-dir: uploads
# springdoc swagger配置
springdoc:
api-docs:
enabled: true # api-docs是否启用
path: /v3/api-docs # api-docs访问路径
swagger-ui:
enabled: true # swagger-ui是否启用
path: /swagger-ui.html # swagger-ui访问路径
packages-to-scan: com.breeze.upload.controller # 扫描的包,自行更改!路径要写对!
paths-to-match: /api/** # 匹配的接口路径,自行更改
- spring.datasource: 数据库连接配置
- spring.servlet.multipart: 文件上传大小限制
- mybatis.configuration.map-underscore-to-camel-case: 开启驼峰命名映射
- file.upload-dir: 文件存储目录配置
- springdoc: Swagger 文档配置
2. 前端代码部分
(1)核心数据存储(响应式状态定义)
<script setup>
import { ref, onMounted } from 'vue'
// 1. 文件列表核心数据(后端返回的文件信息存储)
const files = ref([]) // 存储所有文件列表
const loading = ref(false) // 列表加载状态
const searchTerm = ref('') // 搜索关键词(关联后端搜索逻辑)
// 2. 文件上传核心数据
const fileInput = ref(null) // 关联文件选择输入框
const selectedFiles = ref([]) // 存储待上传的本地文件
const uploading = ref(false) // 上传状态
const uploadMessage = ref('') // 上传结果消息
const uploadMessageType = ref('') // 消息类型(success/error)
const uploadController = ref(null) // 上传中断控制器
// 3. 文件预览核心数据(关联后端预览接口)
const previewModal = ref(false) // 预览模态框状态
const previewUrl = ref('') // 后端返回的文件预览地址
const currentFile = ref(null) // 当前预览的文件信息
(2)与后端交互的核心接口调用
a. 获取文件列表(GET /api/files)
// 从后端获取文件列表,支持前端临时过滤(待后端搜索接口完善后可替换)
const fetchFiles = async () => {
try {
loading.value = true
let url = '/api/files' // 后端文件列表接口
// 调用后端接口
const response = await fetch(url)
if (response.ok) {
let result = await response.json() // 接收后端返回的文件列表数据
// 前端临时过滤(若后端有搜索接口,可替换为带参数的GET请求)
if (searchTerm.value.trim()) {
const keyword = searchTerm.value.toLowerCase()
result = result.filter(file =>
file.originalName && file.originalName.toLowerCase().includes(keyword)
)
}
files.value = result // 将后端数据存入响应式状态,用于页面渲染
} else {
console.error('获取文件列表失败:', response.status)
}
} catch (error) {
console.error('获取文件列表出错(网络/后端异常):', error)
} finally {
loading.value = false
}
}
// 组件挂载时自动调用,初始化文件列表
onMounted(() => {
fetchFiles()
})
b. 文件上传(POST /api/files/upload)
// 向后端上传文件(支持多文件)
const uploadFiles = async () => {
if (selectedFiles.value.length === 0) {
showMessage('请选择文件', 'error')
return
}
uploading.value = true
uploadMessage.value = ''
try {
const formData = new FormData()
// 构建表单数据(与后端接口参数"files"对应)
selectedFiles.value.forEach(file => {
formData.append('files', file)
})
// 创建上传中断控制器(支持取消上传)
uploadController.value = new AbortController()
const signal = uploadController.value.signal
// 调用后端上传接口
const response = await fetch('/api/files/upload', {
method: 'POST',
body: formData,
signal: signal // 绑定中断信号
})
if (response.ok) {
await response.json() // 接收后端上传成功后的文件信息(可用于更新列表)
showMessage('文件上传成功', 'success')
selectedFiles.value = [] // 清空待上传列表
fetchFiles() // 重新获取列表,同步后端数据
} else {
const errorText = await response.text() // 接收后端错误信息
showMessage(`上传失败: ${errorText}`, 'error')
}
} catch (error) {
if (error.name === 'AbortError') {
showMessage('上传已取消', 'error')
} else {
showMessage(`上传出错(网络/后端异常): ${error.message}`, 'error')
}
} finally {
uploading.value = false
uploadController.value = null
}
}
// 取消上传(调用AbortController中断请求)
const cancelUpload = () => {
if (uploading.value && uploadController.value) {
uploadController.value.abort() // 通知后端中断上传
uploading.value = false
showMessage('上传已取消', 'error')
}
selectedFiles.value = []
if (fileInput.value) fileInput.value.value = '' // 重置文件输入框
}
c. 文件删除(DELETE /api/files/{id})
// 调用后端删除接口,删除指定ID的文件
const deleteFile = async (fileId) => {
if (!confirm('确定要删除这个文件吗?')) return
try {
// 调用后端删除接口(路径带文件ID)
const response = await fetch(`/api/files/${fileId}`, {
method: 'DELETE'
})
if (response.ok) {
const message = await response.text() // 接收后端删除成功消息
showMessage(message, 'success')
fetchFiles() // 重新获取列表,同步后端删除后的数据
} else if (response.status === 404) {
showMessage('文件不存在(后端返回404)', 'error')
} else {
const errorText = await response.text() // 接收后端删除错误信息
showMessage(`删除失败: ${errorText}`, 'error')
}
} catch (error) {
console.error('删除文件出错(网络/后端异常):', error)
showMessage(`删除出错: ${error.message}`, 'error')
}
}
d. 文件预览(GET /api/files/view/{filename})
// 调用后端预览接口,获取文件预览地址
const viewFile = (file) => {
currentFile.value = file // 存储当前预览文件信息
// 拼接后端预览接口地址(路径带后端存储的文件名)
previewUrl.value = `/api/files/view/${file.fileName}`
previewModal.value = true // 打开预览模态框
}
// 关闭预览(清空预览数据)
const closePreview = () => {
previewModal.value = false
previewUrl.value = ''
currentFile.value = null
}
六、 相关技术知识
1. Spring Boot 核心概念
依赖注入 (Dependency Injection)
Spring Boot 使用依赖注入来管理组件之间的关系:
|
@RestController public class FileController { @Autowired // 依赖注入 private FileService fileService; } |
控制反转 (Inversion of Control)
Spring 容器负责创建和管理对象的生命周期,开发者只需要定义组件,不需要关心对象的创建过程。
自动配置 (Auto-configuration)
Spring Boot 根据类路径上的依赖自动配置应用程序:
|
@SpringBootApplication // 启用自动配置 public class UploadApplication { // ... } |
2. 文件上传原理
MultipartFile 接口
Spring 提供的MultipartFile接口用于处理文件上传:
|
@PostMapping("/upload") public ResponseEntity<?> uploadFiles(@RequestParam("files") MultipartFile[] files) { // 处理文件上传 } |
文件存储策略
- 本地文件系统存储: 简单直接,适合小型应用
- 数据库存储: 将文件二进制数据存储在数据库中
- 云存储: AWS S3、阿里云 OSS 等
- 分布式文件系统: HDFS、FastDFS 等
安全考虑
- 限制文件大小和类型
- 生成唯一文件名避免冲突
- 存储文件到 Web 应用外部目录
- 对上传文件进行安全检查
3. 前后端通信
RESTful API 设计
遵循 RESTful 风格设计 API:
- GET: 获取资源
- POST: 创建资源
- PUT: 更新资源
- DELETE: 删除资源
HTTP 状态码使用
|
// 成功 ResponseEntity.ok() // 200 OK // 客户端错误 ResponseEntity.badRequest() // 400 Bad Request ResponseEntity.notFound() // 404 Not Found // 服务器错误 ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) // 500 Internal Server Error |
CORS 配置
处理跨域请求:
|
@CrossOrigin // 允许所有来源的跨域请求 // 或更细粒度的配置 @CrossOrigin(origins = "http://localhost:8081", maxAge = 3600) |
更多推荐



所有评论(0)