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

前后端联合实现多个文件上传

 1、前端 Vue3

CommonApplyBasicInfoForm.vue

<script setup lang="ts" name="CommonApplyBasicInfoForm">
......
// 文件输入实例对象
const fileInputRef = ref<HTMLInputElement | null>(null);
// 选择文件列表
const selectedFiles = ref<FileList | null>(null);
// 通过 FormData 对象实现文件上传
let formData = new FormData();// 选择文件,防抖
const onUploadClick = debounce(() => {// 模拟点击元素if (fileInputRef.value) {// 重置以允许重复选择相同文件fileInputRef.value.value = "";fileInputRef.value.click();}},1000,{ leading: true, trailing: true, maxWait: 1000 }
);// 点击【选择文件】触发,实现多个文件上传
const handleUpload = async (e: Event) => {// 获取文件对象列表const input = e.target as HTMLInputElement;selectedFiles.value = input.files;// 判断文件列表是否为空if (!selectedFiles.value?.length) {ElMessage.warning("请选择至少一个文件");return;}try {// 清空 FormData 表单数据的内容:重新赋值,创建新实例,旧数据被丢弃(完全清空),需要使用 let 声明对象,不能使用 const 声明对象formData = new FormData();// 添加多个文件到 FormDatafor (const file of selectedFiles.value) {formData.append("uploadFiles", file);}// 受理编号长度超过8位if (applyBasicInfo.value.outerApplyId.length > 8) {// 添加主键编号到 FormDataformData.append("key", applyBasicInfo.value.outerApplyId);// 文件数量不超过5份if (selectedFiles.value.length < 5) {// 将文件存储位置添加到 formData 对象中,如:受理基础信息、ApplyBasicInfo、ApplyBasicFile,后端会将文件存储到数据库的 ApplyBasicFile 表中formData.append("storage", "受理基础信息");}// 文件数量不超过6份else if (selectedFiles.value.length < 6) {// 将文件存储位置添加到 formData 对象中// 如:受理基础信息/、ApplyBasicInfo/、ApplyBasicFile/,后端会将文件存储到本地磁盘默认目录下formData.append("storage", "受理基础信息/");}// 文件数量超过6份else {// 将文件存储位置添加到 formData 对象中// 如:受理基础信息/xxx、ApplyBasicInfo/xxx、ApplyBasicFile/xxx,后端会将文件存储到本地磁盘默认目录下的 xxx 目录下formData.append("storage", "受理基础信息/ApplyBasicFile");}// 发送请求,上传多个文件到后端服务器await uploadFilesService(formData);}// 受理编号长度不超过8位else {// 添加受理编号到 FormDataformData.append("outerApplyId", applyBasicInfo.value.outerApplyId);// 发送请求,上传多个文件到后端服务器await applyBasicInfoUploadFilesService(formData);}ElMessage.success("文件上传成功!");// 获取已上传的文件列表await fetchUploadedFiles(applyBasicInfo.value.outerApplyId);} catch (error) {ElMessage.error("文件上传失败!");}
};
......
</script><template>
......<span>选择文件</span><!-- 文件输入元素,不显示,通过点击【选择文件】执行 onUploadClick,模拟点击该元素,从而触发 handleUpload 事件 --><input ref="fileInputRef" type="file" multiple style="display: none" @change="handleUpload" />
......
</template>

applyBasicInfo.ts

import request from "@/utils/request";/*** 上传多个文件,使用 post 发送请求,发送的数据只有:请求体数据(表单数据 formData)* @param formData 表单数据,包含的数据有:多个文件数据(uploadFiles)和 受理编号(outerApplyId) {@link FormData}*/
export const applyBasicInfoUploadFilesService = (formData: FormData) => {return request.post("/applyBasicInfo/uploadFiles", formData, {// 上传文件,需设置 headers 信息,将"Content-Type"设置为"multipart/form-data"headers: {"Content-Type": "multipart/form-data"}});
};

2、后端 Spring boot + Mybatis

控制层:ApplyBasicInfoController.java
package com.weiyu.controller;import com.weiyu.pojo.*;
import com.weiyu.service.ApplyBasicInfoService;
import com.weiyu.service.FileDownloadService;
import com.weiyu.service.FileUploadService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import java.util.List;/*** 受理信息 Controller*/
@Slf4j
@RestController
@RequestMapping("/applyBasicInfo")
public class ApplyBasicInfoController {@Autowiredprivate ApplyBasicInfoService applyBasicInfoService;@Autowiredprivate FileUploadService fileUploadService;/*** 上传多个文件** @param outerApplyId 受理编号* @param uploadFiles  多个文件 {@link List}&lt;{@link MultipartFile}&gt;* @return 统一响应结果 {@link Result}&lt;*&gt;*/@PostMapping("/uploadFiles")public Result<?> uploadFiles(@RequestParam("outerApplyId") String outerApplyId,@RequestParam("uploadFiles") List<MultipartFile> uploadFiles) {log.info("【受理基础信息】,上传多个文件,/applyBasicInfo/uploadFiles," +"outerApplyId = {},uploadFiles = {}", outerApplyId, uploadFiles);if (uploadFiles == null || uploadFiles.isEmpty()) {return Result.error("请选择至少一个文件");}try {for (MultipartFile uploadFile: uploadFiles) {fileUploadService.uploadFile(outerApplyId, "受理基础信息", uploadFile);}return Result.success("多个文件上传成功!");} catch (Exception e) {return Result.error("多个文件上传失败:" + e.getMessage());}}
}
服务层接口实现:FileUploadServiceImpl .java
package com.weiyu.service.impl;import com.weiyu.mapper.FileUploadMapper;
import com.weiyu.pojo.FileData;
import com.weiyu.service.FileUploadService;
import com.weiyu.utils.FileSaveUtils;
import com.weiyu.utils.PublicUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;import java.util.List;/*** 文件上传 Service 接口实现*/
@Service
public class FileUploadServiceImpl implements FileUploadService {@Autowiredprivate FileUploadMapper fileUploadMapper;@Autowiredprivate FileSaveUtils fileSaveUtils;/*** 文件上传** @param key        主键* @param storage    文件存储位置(数据库表名 或 文件系统路径 或 实体名称 或 业务名称)* @param uploadFile 上传文件*/@Overridepublic void uploadFile(String key, String storage, MultipartFile uploadFile) {try {// storage 包含路径分隔符 / ,上传文件到本地磁盘中// 如:storage = 质量体系文件/、QualityFile/、ControledFileMain/,上传文件到本地磁盘默认目录下// 如:storage = 质量体系文件/xxx、QualityFile/xxx、ControledFileMain/xxx,上传文件到本地磁盘默认目录中的 xxx 子目录下if (storage.contains("/")) {// 分割处理 storage 的内容List<String> list = List.of(storage.split("/"));// 获取表名String tableName = PublicUtils.mapStorageToTableName(list.get(0));// 文件相对路径名称String fileRelativePathName;// 保存文件到本地磁盘(默认目录)if (list.size() == 1) {// 使用文件存储工具类,将文件保存到本地磁盘fileRelativePathName = fileSaveUtils.saveFileToLocalDisk(uploadFile);}// 保存文件到本地磁盘(默认目录的子目录 xxx)else {fileRelativePathName = fileSaveUtils.saveFileToLocalDisk(uploadFile, list.get(1));}// 通过更新方式,将文件名称保存到数据库if ("ControledFileMain".equalsIgnoreCase(tableName)) {if (key.length() <= 5) {// 使用文件存储工具类,将文件名称保存到数据库fileSaveUtils.saveFileNameToDatabase(fileRelativePathName, tableName, key);} else {// 使用 Mapper,将文件名称保存到数据库fileUploadMapper.saveFileName(key, tableName, fileRelativePathName);}}}// storage 不包含路径分隔符 / ,上传文件到数据库中 或 上传文件到本地磁盘中// 如:storage = 质量体系文件、QualityFile、ControledFileMainelse {// 获取表名String tableName = PublicUtils.mapStorageToTableName(storage);// 文件大小超过3MB,上传文件到本地磁盘中if (uploadFile.getSize() > 1024 * 1024 * 3) {// 使用文件存储工具类,将文件保存到本地磁盘String fileRelativePathName = fileSaveUtils.saveFileToLocalDisk(uploadFile);// 通过更新方式,将文件名称保存到数据库if ("ControledFileMain".equalsIgnoreCase(tableName)) {if (key.length() <= 5) {// 使用文件存储工具类,通过更新方式,将文件名称保存到数据库fileSaveUtils.saveFileNameToDatabase(fileRelativePathName, tableName, key);} else {// 使用 Mapper,通过更新方式,将文件名称保存到数据库fileUploadMapper.saveFileName(key, tableName, fileRelativePathName);}}}// 文件大小不超过3MB,上传文件到数据库中else {FileData fileData = new FileData();fileData.setFileName(uploadFile.getOriginalFilename());fileData.setFileContent(uploadFile.getBytes());// 通过新增方式,将文件增加到数据库if ("ApplyBasicFile".equalsIgnoreCase(tableName)) {// 新增文件到数据库if (key.length() > 8) {// 使用 使用文件存储工具类,通过新增方式,将文件增加到数据库fileSaveUtils.addFileToDatabase(key, tableName, uploadFile);} else {// 使用 Mapper,通过新增方式,将文件增加到数据库fileUploadMapper.addFile(key, tableName, fileData);}}// 通过更新方式,将文件保存到数据库else {// 保存文件到数据库if (key.length() <= 5) {// 使用文件存储工具类,通过更新方式,将文件保存到数据库fileSaveUtils.saveFileToDatabase(uploadFile, tableName, key);} else {// 使用 Mapper,通过更式方式,将文件保存到数据库fileUploadMapper.saveFile(key, tableName, fileData);}}}}} catch (Exception e) {// 抛出运行异常throw new RuntimeException(e.getMessage());}}
}
数据表结构
数据传输对象 DTO:FileData.java
package com.weiyu.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** 文件数据*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FileData {private String fileName;private byte[] fileContent;
}

持久层 Mapper:FileUploadMapper.java

package com.weiyu.mapper;import com.weiyu.pojo.FileData;
import org.apache.ibatis.annotations.Mapper;/*** 文件上传 Mapper*/
@Mapper
public interface FileUploadMapper {/*** 保存文件到数据库** @param key       主键* @param tableName 表名* @param fileData  文件数据*/void saveFile(String key, String tableName, FileData fileData);/*** 保存文件名称到数据库** @param key       主键* @param tableName 表名* @param fileName  文件名称*/void saveFileName(String key, String tableName, String fileName);/*** 增加文件到数据库** @param key       主键* @param tableName 表名* @param fileData  文件数据*/void addFile(String key, String tableName, FileData fileData);
}

持久层数据库sql更新:FileUploadMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.weiyu.mapper.FileUploadMapper"><!--mssql--><!-- 保存文件数据到数据库 --><update id="saveFile"><choose><when test="tableName == 'ControledFileMain'">update ControledFileMain setcfm_ContentFileName = #{fileData.fileName}, cfm_Content = #{fileData.fileContent}, cfm_ContentIsNull = 0where Cfm_BigType = '3' and Cfm_ID = #{key}</when></choose></update><!-- 保存文件名称到数据库 --><update id="saveFileName"><choose><when test="tableName == 'ControledFileMain'">update ControledFileMain setcfm_ContentFileName = #{fileName}, cfm_Content = null, cfm_ContentIsNull = 0where Cfm_BigType = '3' and Cfm_ID = #{key}</when></choose></update><!-- 增加文件数据到数据库 --><insert id="addFile"><choose><when test="tableName == 'ApplyBasicFile'">insert into ApplyBasicFile (ApplyBasicFile_MasterID, ApplyBasicFile_FileName, ApplyBasicFile_File)values (#{key}, #{fileData.fileName}, #{fileData.fileContent})</when></choose></insert>
</mapper>

持久层工具类:FileSaveUtils.java

package com.weiyu.utils;import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;import javax.sql.DataSource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.UUID;/*** 文件存储工具类*/
@Component // 通过 @Component 注解,将该工具类交给 ICO 容器管理,使用的时候不需要 new,直接 @Autowired 注入即可
@Slf4j
public class FileSaveUtils {// 获取文件目录@Getter@Value("${upload.localhost.savePath}") // 从配置文件中获取 upload.localhost.savePath 的配置值private String savePath;@Autowiredprivate DataSource dataSource;/*** 增加文件到数据库** @param key       主键* @param tableName 表名* @param file      文件流* @throws SQLException 异常,SQL类的操作需要抛出异常*/public void addFileToDatabase(String key, String tableName, MultipartFile file) throws SQLException, IOException {log.info("【文件存储工具类】,增加文件到数据库,fileNo = {},tableName = {},file = {}", key, tableName, file);// 参数校验if (file == null || file.isEmpty()) {throw new IllegalArgumentException("文件不能为空");}// 使用白名单校验if (!"ApplyBasicFile".equalsIgnoreCase(tableName)) {throw new IllegalArgumentException("不支持的表名: " + tableName);}if (key == null || key.trim().isEmpty()) {throw new IllegalArgumentException("文件编号不能为空");}String sql = "";if ("ApplyBasicFile".equalsIgnoreCase(tableName)) {// 编写 sqlsql = " insert into ApplyBasicFile (ApplyBasicFile_MasterID, ApplyBasicFile_FileName, ApplyBasicFile_File)" +" values (?, ?, ? )";}// 使用 try-with-Resources 自动管理资源try (// 通过数据源,获取连接Connection connection = dataSource.getConnection();// 装载 sql,PreparedStatement 是 Java JDBC API 中的一个重要接口,用于执行预编译的SQL语句。它比普通的Statement更高效、更安全,特别是在需要多次执行相似SQL语句时。PreparedStatement ps = connection.prepareStatement(sql);// 读取文件流InputStream inputStream = file.getInputStream()) {if ("ApplyBasicFile".equalsIgnoreCase(tableName)) {// 设置参数ps.setString(1, key);ps.setString(2, file.getOriginalFilename());if (key.length() <= 5) {ps.setBlob(3, inputStream);} else {ps.setBinaryStream(3, inputStream);}}// 执行 sql,执行更新,返回受影响的行数ps.executeUpdate();} catch (SQLException | IOException e) {// ❌ 禁止使用 printStackTrace 在控制台输出异常的详细堆栈跟踪信息// e.printStackTrace();// ✅ 规范日志记录:使用日志框架记录完整异常堆栈(参数 e 包含异常的详细堆栈跟踪信息)log.error("异常错误 {}", e.getMessage(), e); // 记录错误消息和详细堆栈跟踪信息// 重新抛出异常throw e;}// 无需 finally 块,资源会自动关闭}
}

3、应用效果

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

相关文章:

  • FastAPI 教程:构建高性能异步 API 服务
  • 石化设备健康管理平台:工业智能化转型的关键使能技术​
  • std::thread详解
  • Spring Boot单体项目整合Nacos
  • C++17 折叠表达式(Fold Expressions)详解
  • ConcurrentHashMap在扩容的过程中又有新的数据写入是怎么处理的
  • 《Bishop PRML》10.1 (3) 理解VAE reconstruction loss
  • Redis 中的 Bitmap 与 Bitfield 及 Java 操作实践
  • python如何下载svg图片
  • 【Proteus仿真】数码管控制系列仿真——单个数码管控制/多数码管控制
  • leetcode 260 只出现一次的数字III
  • 你的数据是如何被保护的?
  • Linux系统的进程管理
  • vue3+vite+ts 发布npm 组件包
  • 查看所有装在c盘软件的方法
  • [知识点记录]SQLite 数据库和MySQL 数据库有什么区别?
  • DuckDB 内嵌分析:ABP 的「本地 OL盘快照」
  • 福彩双色球第2025100期号码推荐
  • 福彩双色球第2025100期数据统计
  • 吴恩达机器学习作业十一:异常检测
  • Docker 容器(二)
  • 机器视觉学习-day15-图像轮廓特征查找
  • Wi-Fi技术——OSI模型
  • 深度学习量化双雄:PTQ 与 QAT 的技术剖析与实战
  • 开源协作白板 – 轻量级多用户实时协作白板系统 – 支持多用户绘图、文字编辑、图片处理
  • globals() 小技巧
  • C++ 模板全览:从“非特化”到“全特化 / 偏特化”的完整原理与区别
  • Prometheus之启用--web.enable-remote-write-receiver
  • 基于muduo库的图床云共享存储项目(三)
  • 前端常见安全问题 + 防御方法 + 面试回答