一、介绍

这是一个基于 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/");
    }
}
  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;  // 文件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);
}
  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 {
    
    @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);
    }
}
  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 {

    @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); // 文件路径错误
        }
    }
}
  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%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/**  # 匹配的接口路径,自行更改
  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 = 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) {

    // 处理文件上传

}

文件存储策略

  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)

更多推荐