当前位置: 首页 > news >正文

【文件上传管理系统】实战详解 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);}@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {// 配置上传文件的静态资源访问路径registry.addResourceHandler("/uploads/**").addResourceLocations("file:" + System.getProperty("user.dir") + "/uploads/");}
}
  1. @SpringBootApplication: 组合注解,包含@Configuration、@EnableAutoConfiguration、@ComponentScan
  2. @MapperScan: 告诉 MyBatis 扫描指定包下的 Mapper 接口
  3. main方法: 应用程序入口点
  4. addResourceHandlers: 配置静态资源访问,让前端可以直接访问上传的文件
(2)实体类 - File.java
@Data  // Lombok注解,自动生成getter、setter、toString等方法
public class File {private Long id;  // 文件IDprivate 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);
}
  1. @Mapper: 告诉 MyBatis 这是一个映射器接口
  2. 定义了基本的 CRUD 操作方法
  3. 方法名遵循 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 {@Autowiredprivate 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);// 生成文件访问URLString 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);}
}
  1. @Service: 标记为 Spring 服务层组件
  2. @Value: 从配置文件中注入文件上传目录
  3. saveFile: 核心方法,包含文件保存的完整逻辑
    1. 创建目录(如果不存在)
    2. 生成唯一文件名(使用 UUID 避免重名)
    3. 保存文件到磁盘
    4. 保存文件信息到数据库
  4. deleteFile: 先删除磁盘文件,再删除数据库记录
(6)控制器 - FileController.java

/*** @author 邂逅星河浪漫* @date 2025/09/20*/
@RestController
@RequestMapping("/api/files")
@CrossOrigin
@Tag(name = "文件上传接口",description = "文件上传,搜索,查看接口")
public class FileController {@Autowiredprivate 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*/@GetMappingpublic 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); // 文件路径错误}}
}
  1. @RestController: 组合@Controller和@ResponseBody
  2. @RequestMapping("/api/files"): 设置基础 URL 路径
  3. @CrossOrigin: 允许跨域请求,解决前后端分离的跨域问题
  4. @Operation: Swagger 注解,用于 API 文档
  5. 每个方法对应一个 HTTP 请求处理:
    1. uploadFiles: 处理文件上传
    2. getAllFiles: 获取所有文件列表
    3. getFileById: 根据 ID 获取单个文件
    4. deleteFile: 删除文件
    5. viewFile: 查看文件内容
(7)配置文件 - application.yaml
spring:application:name: upload# 数据库配置datasource:url: jdbc:mysql://localhost:3306/file_upload?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8username: 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/**  # 匹配的接口路径,自行更改
  1. spring.datasource: 数据库连接配置
  2. spring.servlet.multipart: 文件上传大小限制
  3. mybatis.configuration.map-underscore-to-camel-case: 开启驼峰命名映射
  4. file.upload-dir: 文件存储目录配置
  5. 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 = truelet 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 = trueuploadMessage.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 = falseuploadController.value = null}
}// 取消上传(调用AbortController中断请求)
const cancelUpload = () => {if (uploading.value && uploadController.value) {uploadController.value.abort() // 通知后端中断上传uploading.value = falseshowMessage('上传已取消', 'error')}selectedFiles.value = []if (fileInput.value) fileInput.value.value = '' // 重置文件输入框
}

c. 文件删除(DELETE /api/files/{id})

// 调用后端删除接口,删除指定ID的文件
const deleteFile = async (fileId) => {if (!confirm('确定要删除这个文件吗?')) returntry {// 调用后端删除接口(路径带文件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 = falsepreviewUrl.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) {

    // 处理文件上传

}

文件存储策略

  1. 本地文件系统存储: 简单直接,适合小型应用
  2. 数据库存储: 将文件二进制数据存储在数据库中
  3. 云存储: AWS S3、阿里云 OSS 等
  4. 分布式文件系统: HDFS、FastDFS 等

安全考虑

  1. 限制文件大小和类型
  2. 生成唯一文件名避免冲突
  3. 存储文件到 Web 应用外部目录
  4. 对上传文件进行安全检查

3. 前后端通信

RESTful API 设计

遵循 RESTful 风格设计 API:

  1. GET: 获取资源
  2. POST: 创建资源
  3. PUT: 更新资源
  4. 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)

http://www.dtcms.com/a/393864.html

相关文章:

  • 软考中级习题与解答——第八章_计算机网络(3)
  • 【每日一问】PFC电路有什么作用?
  • 智能制造设备健康管理案例:AIoT技术驱动的工业设备智能运维革命​
  • Rd-03_V2 雷达模块【上手使用指南】
  • PD 分离推理架构详解
  • 重庆蓝金领科技培训评价如何
  • 【TS3】搭建本地开发环境
  • MR、AR、VR:技术浪潮下安卓应用的未来走向
  • React搭建应用
  • NVIDIA Dynamo 推理框架
  • 校园网即点即连——校园网自动登录的思路流程
  • C# 设计模式|单例模式全攻略:从基础到高级实现与防御
  • SQL 字符串函数高频考点:LIKE 和 SUBSTRING 的区别
  • 法律文档智能分析系统:NLP+法律知识库的技术实现方案
  • Flutter_学习记录_实现商品详情页Tab点击跳转对应锚点的demo
  • 【大语言模型】作为可微分搜索索引的Transformer记忆体
  • NLP---自然语言处理
  • 多条件查询中的日期交互指南:从前端到后端的顺畅协作
  • 系分论文《论人工智能在网络安全态势感知系统中的分析与设计》
  • 【Kubernetes】(六)Service
  • Coze源码分析-资源库-删除工作流-后端源码-核心技术与总结
  • vue Ai 流试回答实现打字效果
  • 【架构】面向对象六大设计原则
  • ✅ 基于OpenCV与HyperLPR的车牌识别系统 PyQt5可视化 支持多种输入 深度学习毕业设计
  • 深度学习在计算机视觉中的最新进展:范式转变与前沿探索
  • 本地免费使用网页表格控件websheet
  • Spring Boot集成MQTT与单片机通信
  • 【Axios 】web异步请求
  • FreeRTOS实战指南 — 6 临界段保护
  • 关于CFS队列pick_next_task_fair选取下一个任务的分析