Layui文件上传及页面回显、数据库绑定
1:文件上传分析
文件上传作为一个独立功能,不能和业务产生过于直接的关系,得单独设计表和代码
2:数据表分析
首先需要一个表保存文件的基本信息,如:源文件名,服务器文件路径,文件类型等。
还需要一个表用来关联业务数据,但是不同业务的id也许有重复的,所以不能用业务id关联,得单独定义一个字段
以下实现是基于Layui2.5.6和springboot2.3.8实现的
3:数据表创建
首先是保存文件基本信息的表:file_upload
CREATE TABLE `file_upload` ( `id` bigint NOT NULL AUTO_INCREMENT, `file_source_name` varchar(128) DEFAULT NULL COMMENT '源文件名', `file_new_name` varchar(128) DEFAULT NULL COMMENT '新文件名', `file_url` varchar(512) DEFAULT NULL COMMENT '文件全路径', `file_type` varchar(32) DEFAULT NULL COMMENT '文件类型', `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '上传时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=73 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文件上传记录表';
保存业务文件关联关系表:file_service_map
CREATE TABLE `file_service_map` ( `id` bigint NOT NULL AUTO_INCREMENT, `file_id` bigint NOT NULL COMMENT '文件id', `service_id` varchar(128) NOT NULL COMMENT '业务id', `service_type` varchar(64) NOT NULL COMMENT '业务类型', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文件业务中间表';
4:页面设计
页面的话,就不需要自己写,用layui现成的上传组件,但是有些地方还是得调整一下或者开拓一下
页面上传思路:
使用layui上传文件,文件在服务器保存后返回文件id,页面把id隐藏的保存起来,然后提交业务数据的时候,把文件的id带到服务器,然后生成关联id,保存业务表,保存中间关系表
页面回显思路:
使用业务表数据查询接口,查询数据,同时根据查询出来的文件关联id去查询中间表,拿到中间表的数据。在业务表的回显vo中,增加一个List,用来装文件列表信息。页面根据这个信息去渲染或者生成html段落即可实现文件的列表回显
5:页面实现
html页面中加入一段容器,尽量不要把上传组件的html代码写到这里面,封装到js里进行统一管理
<!--文件上传容器--> <div class="layui-form-item" id="uploadElementId"> </div>
js代码,下面几个方法是通用的,所以可以单独放一个js里。其中预览文件只对图片做了预览
//支持的图片格式 var imgTypes = ['.bmp', '.jpg', '.png', '.tif', '.gif', '.pcx', '.tga', '.exif', '.fpx', '.svg', '.psd', '.cdr', '.pcd', '.dxf', '.ufo', '.eps', 'ai', '.raw', '.WMF', '.webp', '.avif', '.apng']; layui.define(['layer', 'upload'], function () { let $ = layui.$; let upload = layui.upload; //文件id集合 var fileIds = []; window.initUpload = function (elementId) { //判断容器是否存在,如果不存在再渲染 if ($("#" + elementId).children().length > 0) { return; } $("#" + elementId).append( ` <div class="layui-input-block layui-col-md16"> <!--这个ids是每次上传文件后,js会重新赋值这个id,格式是逗号分开的文件id--> <input type="hidden" placeholder="文件id" id="fileIds" name="fileIds"/> <!--文件预览的图片容器--> <div id="load-img"> </div> <div class="layui-upload-drag" title="支持拖拽上传" id="upload" style="width: 80%"> <i class="layui-icon"></i> <p>点击上传文件,或将文件拖拽到此处</p> </div> <!--上传后列表展示,但是如果业务表不提交,这个id也就无法保存到数据库,也就无法关联文件--> <div class="layui-upload-list"> <table class="layui-table"> <thead> <tr> <th>文件名</th> <th>操作</th> <th hidden="true">文件id</th> </tr> </thead> <tbody id="fileUploadList"> </tbody> </table> </div> </div> ` ) //构建文件上传 upload.render({ elem: '#upload' , url: '/upload' , accept: 'file' , multiple: true , done: function (res, index, upload) { //上传后的回调 if (res.code == 0) { //提示成功 layer.msg(res.msg, {icon: 1}); //把全局变量修改 fileIds.push(res.data.id); //赋值给页面的input参数 $("#fileIds").val(fileIds); //渲染页面数据(关键方法) uploadElementRend(res.data, "fileUploadList"); } else { layer.msg(res.msg, {icon: 2}); } } }) } }) /** * 下载文件 * @param fileId */ function downFile(fileId) { window.open("/upload/download?fileId=" + fileId); } /** * 重命名 * @param fileId 文件id * @param fileName 文件现在的名字 */ function updateFileName(fileId, fileName) { fileName = fileName.substr(0, fileName.lastIndexOf(".")) layer.prompt({ formType: 0, title: '请输入名称', btn: ['确定', '取消'], btnAlign: 'c', value: fileName }, function (value, index) { $.ajax({ type: "get", url: "/upload/updateFileName", cache: false, data: { "fileId": fileId, "fileName": value }, success: function (result) { if (result.code === 0 || result.code === 202) { $("#" + fileId + "FileName").text(result.data); layer.msg(result.msg, {icon: 1}); } else { layer.msg(result.msg, {icon: 2}); } }, error: function (err) { layer.msg(err.responseText ? err.responseText : '无权限或操作失败!', {icon: 2}); } }); layer.close(index); }); } /** * 打开图片 */ function openPhoto(url, fileName) { let type = fileName.substr(fileName.lastIndexOf("."), fileName.length); if (imgTypes.includes(type)) { parent.layer.open({ type: 1, title: fileName, skin: 'layui-layer-rim', area: ['75%', '85%'], shadeClose: true, end: function (index, layero) { return false; }, content: '<div style="text-align:center;"><img src="/file/' + url + '" /></div>' }); } else { layer.msg('暂不支持预览该文件!', {icon: 7}); } } /** * 删除文件 * @param fileId */ function deleteFile(fileId) { layer.confirm('"确定要删除该文件吗?"', {icon: 3, title: '警示'}, function (index) { $.ajax({ type: "get", url: "/upload/deleteFile?fileId=" + fileId, async: false, success: function (data) { if (data.code === 202 || data.code === 0) { layer.msg(data.msg, {icon: 1}); //删除页面上的文件列表 $("#" + fileId).remove(); //删除图片相关的元素 $("." + fileId).remove(); } else { layer.msg(data.msg, {icon: 2}); } }, error: function (err) { layer.msg(err.responseText ? err.responseText : '无权限或操作失败!', {icon: 2}); } }) }) } /** * 详情或修改时的文件回显,与下面方法其实是一样的,只是这里是个集合 * @param d 数据 * @param elementId id */ function rendEditFile(d, elementId) { if (d != null) { //业务数据中的文件列表信息 let msgList = d.fileMsgList; if (msgList) { for (let i = 0; i < msgList.length; i++) { uploadElementRend(msgList[i]); } } } } /** * 上传文件的回显 如果要自定义实现,则把下面代码复制一份进行操作 * @param d 上传的返回值 */ function uploadElementRend(d) { let fileCss = ""; //如果是图片类型,就直接展示 if (imgTypes.includes(d.fileType)) { //如果是图片 支持预览的情况下 让文字变颜色 fileCss = "color: green;cursor: pointer;"; //渲染图片的id是封装死的,其他页面尽量不要修改 $("#load-img").append( `<img class="layui-upload-img ` + d.id + `" onclick="openPhoto('` + d.fileUrl + `','` + d.fileSourceName + `')" src="/file/` + d.fileUrl + `" width="100"/>` ) } //构建删除按钮 这一段是跳转时候用来判断跳转类型的,如果是详情的话,就不要删除按钮 let h = ""; let type = getUrlParam('lookType'); if ("detail" != type) { h = `<a class="layui-btn layui-btn-danger layui-btn-xs" onclick=deleteFile(` + d.id + `)>删除</a>`; h += `<a class="layui-btn layui-btn-normal layui-btn-xs" onclick=updateFileName('` + d.id + `','` + d.fileSourceName + `')>重命名</a>`; } //添加内容和按钮 文件列表的id是封装死的,尽量不要修改 $("#fileUploadList").append( ` <tr id="` + d.id + `"> <td style="` + fileCss + `" id="` + d.id + `FileName" onclick="openPhoto('` + d.fileUrl + `','` + d.fileSourceName + `')">` + d.fileSourceName + `</td> <td> <a class="layui-btn layui-btn-normal layui-btn-xs" onclick=downFile(` + d.id + `)>下载</a> ` + h + ` </td> <td hidden="true">` + d.id + `</td> </tr> ` ) } /** * 用于封装整个js核心方法,上述其他方法均为实现,而业务操作不需要关注实现,只需要调用下面这两个方法即可 * @type */ var fileUploadFactory = { //初始化文件上传组件 initUpload: function (elementId) { initUpload(elementId); lookType(); }, //文件上传后的ui渲染 rendEditFile: function (d, elementId) { //初始化一下,如果容器存在,它不会再次初始化 initUpload(elementId); //根据数据d,渲染列表信息 rendEditFile(d, elementId); } }
文件回显,是需要查询到数据后,调用上面js文件末尾的函数:
fileUploadFactory (数据,容器id);
html页面调用上面的js
文件上传的创建页需要调用的js,主要是:fileUploadFactory.initUpload("uploadElementId");
layui.use(['layer', 'form', 'laydate', 'upload'], function () { let $ = layui.$; let form = layui.form; let laydate = layui.laydate; laydate.render({ elem: '#proTime' }); // 提交事件 form.on('submit(save-problem)', function (data) { let field = data.field; save(field); return false; }); //初始文件上传容器 fileUploadFactory.initUpload("uploadElementId"); //保存业务数据 function save(problem) { $.ajax({ type: "post", url: "/problem/save", data: JSON.stringify(problem), contentType: 'application/json', success: function (data) { if (data.code === 202 || data.code === 0) { layer.msg(data.msg, {icon: 1}); } else { layer.msg(data.msg, {icon: 2}); } closePage(); }, error: function (err) { layer.msg(err.responseText ? err.responseText : '无权限或操作失败!', {icon: 2}); } }) } function closePage() { setTimeout(function () { let index = parent.layer.getFrameIndex(window.name); parent.layer.close(index); }, 1000) } })
文件上传的详情页需要调用的js,主要是:fileUploadFactory.rendEditFile(d, "uploadElementId");
layui.use(['layer', 'form', 'laydate', 'upload'], function () { let $ = layui.$; let form = layui.form; let laydate = layui.laydate; laydate.render({ elem: '#proTime' }); // 修改事件 form.on('submit(update-problem)', function (data) { let field = data.field; field.id = getUrlParam('problemId'); update(field); return false; }); //查数据 let d = edit(); //渲染页面数据 rendEdit(d);
//渲染文件列表 fileUploadFactory.rendEditFile(d, "uploadElementId"); //判断是否需要禁用页面元素 lookType(); function update(role) { $.ajax({ type: "post", url: "/problem/update", data: JSON.stringify(role), contentType: 'application/json', success: function (data) { if (data.code === 202 || data.code === 0) { layer.msg(data.msg, {icon: 1}); } else { layer.msg(data.msg, {icon: 2}); } closePage(); }, error: function (err) { layer.msg(err.responseText ? err.responseText : '无权限或操作失败!', {icon: 2}); } }) } function closePage() { setTimeout(function () { let index = parent.layer.getFrameIndex(window.name); parent.layer.close(index); }, 1000) } }) function edit() { let d = {}; $.ajax({ type: "get", url: "/problem/detail?id=" + getUrlParam('problemId'), contentType: 'application/json', async: false, success: function (data) { d = data; } }) return d; }
6:后端代码分析
上传文件需要动态配置服务器的路径,不能写死。保存的文件路径只能保存设置的基本路径后面的路径,不能把服务器前面的获取的路径也保存下来。拿到文件以后,不能直接保存,需要重新命名文件名,这是防止不同的业务,但是有相同的文件。
保存时为了后期方便管理,要根据文件类型放在各自类型的文件下,否则时间长了就会很乱也不好处理
》接口配置思路:
首先需要几个基本的接口
上传、下载、删除
还需要几个给业务的接口,方便业务操作的。
绑定业务文件关系、业务数据中文件列表的填充、解除关联并删除文件
》同时还需要提供一个实体类,用来限定文件上传的参数和定义一些方法
import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; import java.util.List; /** * 上传文件的服务接收和入参都需要继承这个类,用于统一管理字段名 **/ @Data public class BaseUploadMapEntity { /** * 业务表文件关联id字段名 */ private String serviceId; /** * 文件id集合 */ @TableField(exist = false) private String fileIds; /** * 关联的文件集合 */ @TableField(exist = false) private List<UploadEntity> fileMsgList; }
后端controller
@RestController @RequestMapping("/upload") public class UploadController { private final UploadService uploadService; public UploadController(UploadService uploadService) { this.uploadService = uploadService; } /** * 上传文件 * * @param file 文件 * @return 上传详细信息 */ @PostMapping public LayuiPageInfo upload(MultipartFile file) { UploadEntity upload = uploadService.upload(file); if (upload == null) { return LayuiPageFactory.createErrInfo("文件上传失败!", -1); } return LayuiPageFactory.createLayuiPageInfo(upload); } /** * 文件下载 * * @param fileId 文件id * @param response 响应 */ @PreAuthorize("hasAnyAuthority('/upload/download')") @GetMapping("/download") public void download(Integer fileId, HttpServletResponse response) { uploadService.download(fileId, response); } /** * 删除文件 * * @param fileId 文件id * @return 删除结果 */ @PreAuthorize("hasAnyAuthority('/upload/deleteFile')") @GetMapping("/deleteFile") public LayuiPageInfo deleteFile(Integer fileId, HttpServletResponse response) { return uploadService.deleteFile(fileId); } /** * 修改文件名 * @param fileId 文件id * @param fileName 新文件名,建议不写后缀 * @return */ @PreAuthorize("hasAnyAuthority('/upload/updateFileName')") @GetMapping("/updateFileName") public LayuiPageInfo updateFileName(Integer fileId, @RequestParam(required = true) String fileName) { return uploadService.updateFileName(fileId, fileName); } /** * 清理没有关联的文件,释放系统磁盘空间 * @return 清理结果 */ @PreAuthorize("hasAnyAuthority('/upload/clearFile')") @GetMapping("/clearFile") public LayuiPageInfo clearFile(){ return uploadService.clearFile(); } }
service接口
public interface UploadService { /** * 上传文件 * * @param file 文件 * @return 上传详细信息 */ UploadEntity upload(MultipartFile file); /** * 文件下载 * * @param fileId 文件id * @param response 响应 */ void download(Integer fileId, HttpServletResponse response); /** * 删除文件 * * @param fileId 文件id * @return 删除结果 */ LayuiPageInfo deleteFile(Integer fileId); /** * 修改文件名 * * @param fileId 文件id * @param fileName 新文件名 * @return 修改结果 */ LayuiPageInfo updateFileName(Integer fileId, String fileName); /** * 清理没有关联的文件,释放系统磁盘空间 * * @return 清理结果 */ LayuiPageInfo clearFile(); //-------------------------------下面三个方法是业务操作的方法--------------------------------------- /** * 绑定文件业务 * * @param uploadMapEntity 文件业务基本类 * @param type 业务类型 */ void saveOrUpdateFile(BaseUploadMapEntity uploadMapEntity, UploadServiceType type); /** * 文件详情的数据填充 * * @param uploadMapEntity 文件业务基本类 */ void fileDetail(BaseUploadMapEntity uploadMapEntity); /** * 解除关联并删除文件 * * @param uploadMapEntity 文件业务基本类 * @return 删除结果 */ void deleteFile(BaseUploadMapEntity uploadMapEntity); }
实现类
import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.web.multipart.MultipartFile; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.net.URLEncoder; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; /** * @author 文件上传实现*/ @Service @Slf4j @DS("userdb") public class UploadServiceImpl implements UploadService { private final UploadFileMapperService fileMapperService; private final FileServiceMapMapperService serviceMapMapperService; public UploadServiceImpl(UploadFileMapperService fileMapperService, FileServiceMapMapperService serviceMapMapperService) { this.fileMapperService = fileMapperService; this.serviceMapMapperService = serviceMapMapperService; } /** * 配置文件配置的服务器保存路径 */ @Value("${application.fileUploadPath}") private String basePath; @Override public UploadEntity upload(MultipartFile file) { File toFile = null; if (file.equals("") || file.getSize() <= 0) { return null; } else { /*获取文件原名称*/ String originalFilename = file.getOriginalFilename(); /*获取文件格式*/ String fileFormat = originalFilename.substring(originalFilename.lastIndexOf(".")); String uuid = UUID.randomUUID().toString().trim().replaceAll("-", ""); String currentPath = System.getProperty("user.dir"); //避免命名重复导致文件覆盖 String fileNewName = uuid + fileFormat; //保存路径中根据类型不同,保存在不同的文件夹 String fileTypePath = fileFormat.substring(1, fileFormat.length()) + "/" + fileNewName; toFile = new File(currentPath + basePath + fileTypePath); String absolutePath = null; try { absolutePath = toFile.getCanonicalPath(); /*判断路径中的文件夹是否存在,如果不存在,先创建文件夹*/ String dirPath = absolutePath.substring(0, absolutePath.lastIndexOf(File.separator)); File dir = new File(dirPath); if (!dir.exists()) { dir.mkdirs(); } InputStream ins = file.getInputStream(); //写文件 saveToServiceFile(ins, toFile); ins.close(); //保存文件信息 UploadEntity entity = new UploadEntity(); entity.setFileSourceName(originalFilename); entity.setFileNewName(fileNewName); entity.setFileUrl(fileTypePath); entity.setFileType(fileFormat); fileMapperService.save(entity); } catch (IOException e) { e.printStackTrace(); } return fileMapperService.getOne(new LambdaQueryWrapper<UploadEntity>() .eq(UploadEntity::getFileNewName, fileNewName)); } } public UploadEntity queryUploadMsgByFileId(Integer fileId) { return fileMapperService.getById(fileId); } /** * 根据业务关联id查询文件详情 * * @param serviceId 业务关联id * @return 文件列表详情 */ public List<UploadEntity> queryUploadMsgByServiceId(String serviceId) { List<FileServiceMap> list = serviceMapMapperService.list(new LambdaQueryWrapper<FileServiceMap>() .eq(FileServiceMap::getServiceId, serviceId)); if (!CollectionUtils.isEmpty(list)) { List<Integer> fileIds = list.stream().map(f -> f.getFileId()).collect(Collectors.toList()); return fileMapperService.list(new LambdaQueryWrapper<UploadEntity>() .in(UploadEntity::getId, fileIds)); } return Collections.emptyList(); } /** * 构建文件关联的map对象 * * @param serviceId 业务关联id * @param fileIds 文件id集合 * @param type 业务类型 * @return 构建的集合 */ public List<FileServiceMap> buildFileServiceMap(String serviceId, String fileIds, UploadServiceType type) { return Arrays.asList(fileIds.split(",")).stream().map(f -> { FileServiceMap map = new FileServiceMap(); map.setServiceId(serviceId); map.setFileId(Integer.valueOf(f)); map.setServiceType(type.name()); return map; }).collect(Collectors.toList()); } /** * 文件下载 * * @param fileId 文件id * @param response 响应 */ @Override public void download(Integer fileId, HttpServletResponse response) { try { UploadEntity upload = queryUploadMsgByFileId(fileId); if (upload == null) { return; } String fileUrl = upload.getFileUrl(); if (!StringUtils.isEmpty(fileUrl)) { String currentPath = System.getProperty("user.dir"); File uploadFile = new File(currentPath + basePath + fileUrl); ServletOutputStream outputStream = response.getOutputStream(); response.addHeader("Content-Disposition", "attachment;filename =" + URLEncoder.encode(upload.getFileSourceName(), "UTF-8")); response.setContentType("application/octet-stream"); byte[] bytes = FileUtil.readBytes(uploadFile); outputStream.write(bytes); outputStream.flush(); outputStream.close(); } } catch (Exception e) { e.printStackTrace(); log.error("文件下载异常!详细信息:{}", e.getMessage()); } } /** * 文件删除 * * @param fileId 文件id * @return */ @Override @Transactional(rollbackFor = {Exception.class}) public LayuiPageInfo deleteFile(Integer fileId) { UploadEntity upload = queryUploadMsgByFileId(fileId); if (upload == null) { return LayuiPageFactory.createErrInfo("文件不存在!", -1); } //删除业务关联关系 boolean b = serviceMapMapperService.remove(new LambdaQueryWrapper<FileServiceMap>() .eq(FileServiceMap::getFileId, fileId)); boolean removeFileMsg = fileMapperService.removeById(fileId); if (b || removeFileMsg) { String currentPath = System.getProperty("user.dir"); File file = new File(currentPath + basePath + upload.getFileUrl()); if (file.exists()) { boolean delete = file.delete(); log.warn("删除文件:" + upload.getFileSourceName() + (delete ? "(成功)" : "(失败)")); return delete ? LayuiPageFactory.createMsgInfo("文件删除成功!") : LayuiPageFactory.createErrInfo("文件删除失败!", -1); } } return LayuiPageFactory.createErrInfo("文件关联关系解除失败!", -1); } /** * 保存业务文件关联表 * * @param serviceId * @param fileIds * @param serviceType */ public void saveFileServiceMap(String serviceId, String fileIds, UploadServiceType serviceType) { if (fileIds != null && !"".equals(fileIds)) { //构建文件关系集合 List<FileServiceMap> map = buildFileServiceMap(serviceId, fileIds, serviceType); boolean b = serviceMapMapperService.saveBatch(map); log.info("[" + serviceType.getDesc() + "]业务保存文件" + (b ? "成功!" : "失败!")); } } /** * 生成业务关联id * * @return */ public String generateServiceId() { return UUID.randomUUID().toString().trim().replaceAll("-", ""); } /** * 修改文件名 * * @param fileId 文件id * @param fileName 新文件名 * @return */ @Override public LayuiPageInfo updateFileName(Integer fileId, String fileName) { if (StringUtils.isEmpty(fileName) || "".equals(fileName.trim())) { return LayuiPageFactory.createErrInfo("文件名不能为空!", -1); } //1:根据id查询文件信息 UploadEntity fileEntity = fileMapperService.getById(fileId); if (fileEntity == null) { return LayuiPageFactory.createErrInfo("文件不存在!", -1); } //2:判断新文件名是否有相同后缀 if (fileName.endsWith(fileEntity.getFileType())) { fileEntity.setFileSourceName(fileName); } else { int typeFlag = fileName.indexOf("."); if (typeFlag > 0) { //切割一下,防止页面传错 fileName = fileName.substring(0, typeFlag); } fileEntity.setFileSourceName(fileName + fileEntity.getFileType()); } boolean b = fileMapperService.updateById(fileEntity); LayuiPageInfo msgInfo = LayuiPageFactory.createMsgInfo("文件修改成功!"); msgInfo.setData(fileName + fileEntity.getFileType()); return b ? msgInfo : LayuiPageFactory.createErrInfo("文件修改失败!", -1); } @Override public LayuiPageInfo clearFile() { log.warn("开始清理服务多余文件!"); //1:查询中间表和文件表差集 List<UploadEntity> uploadEntities = fileMapperService.queryFileNotInMap(); //2:根据查询出来的文件信息,把文件都删掉,需要排错,有些数据存在 但文件可能已经没有了 if (!CollectionUtils.isEmpty(uploadEntities)) { uploadEntities.forEach(f -> deleteFile(f.getId())); return LayuiPageFactory.createMsgInfo("本次共清理" + uploadEntities.size() + "个文件!"); } return LayuiPageFactory.createMsgInfo("没有多余文件清理!"); } /** * 写文件 * * @param ins * @param file */ private static void saveToServiceFile(InputStream ins, File file) { OutputStream os = null; try { os = new FileOutputStream(file); int bytesRead = 0; byte[] buffer = new byte[8192]; while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) { os.write(buffer, 0, bytesRead); } os.close(); ins.close(); } catch (Exception e) { e.printStackTrace(); } } /** * 渲染查询时文件列表详情数据 * * @param uploadMapEntity 文件业务基本类 * @param type */ @Override public void saveOrUpdateFile(BaseUploadMapEntity uploadMapEntity, UploadServiceType type) { //生成id if (StringUtils.isEmpty(uploadMapEntity.getServiceId()) && (uploadMapEntity.getFileIds() != null && !"".equals(uploadMapEntity.getFileIds()))) { uploadMapEntity.setServiceId(generateServiceId()); } //保存中间表信息 saveFileServiceMap(uploadMapEntity.getServiceId(), uploadMapEntity.getFileIds(), type); } /** * 渲染查询时文件列表详情数据 * * @param uploadMapEntity 文件业务基本类 */ @Override public void fileDetail(BaseUploadMapEntity uploadMapEntity) { //设置文件详情集合 uploadMapEntity.setFileMsgList(queryUploadMsgByServiceId(uploadMapEntity.getServiceId())); } /** * @param uploadMapEntity 文件业务基本类 */ @Override public void deleteFile(BaseUploadMapEntity uploadMapEntity) { List<UploadEntity> entityList = queryUploadMsgByServiceId(uploadMapEntity.getServiceId()); if (!CollectionUtils.isEmpty(entityList)) { entityList.forEach(f -> deleteFile(f.getId())); } } }
使用的时候,让业务实体类去继承这个类,就可以方便和规范的设置数据。具体看后面的业务实现代码
里面的两个service对应的是两个数据库表的service,是mybatisPlus做的。就不在贴代码
js中预览图片的路径是/file开头的,没有完全的服务器路径,也不能写全服务器路径。所以后端需要写一个配置文件映射一下地址:把/file开头的请求,映射到指定的文件路径下,这样就页面就可以通过请求直接看到图片了
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${application.fileUrl}") private String fileUrl; @Value("${application.fileUploadPath}") private String fileUploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //将匹配上/file/**虚拟路径的url映射到文件上传到服务器的目录, registry.addResourceHandler("/" + fileUrl + "/**") .addResourceLocations("file:" + System.getProperty("user.dir") + fileUploadPath); WebMvcConfigurer.super.addResourceHandlers(registry); } }
这段代码表示如:localhost:8080/file/xxx.png
在后端会映射到:本地盘/指定的fileUploadPath/xxx.png
到此,文件上传一系列接口都完善了,然后就是业务关联
7:业务上传处理思路
页面提交时会携带一个fileIds字段,这个字段是用逗号分隔的文件id,后端拿到后,直接调用文件服务的保存方法,方法里面已经赋值serviId了。
实体类变化
import com.baomidou.mybatisplus.annotation.TableName; import com.test.trigger.upload.entity.BaseUploadMapEntity; import lombok.Data; import java.util.Date; /** * 业务实体(正常情况下,对接数据库的类是不能作为返回页面的,这里没有封装vo)*/ @Data @TableName("problem") public class ProblemEntity extends BaseUploadMapEntity { private int id; /** * 环境 */ private String env; /** * 服务名 */ private String serviceName; /** * 标题 */ private String title; /** * 问题类型 */ private String type; /** * 问题 */ private String problem; /** * 处理方式 */ private String handleMethod; /** * 处理负责人 */ private String handleHead; /** * 问题发生时间 */ private String proTime; /** * 状态 */ private int status; private Date createDate; public void create() { setCreateDate(new Date()); } }
上面关键的地方在于继承了文件上传的基本类。理论上返回的数据应该要封装vo或者其他的非数据库实体的对象,这里为了方便就都写一块
保存时写法
@Override @Transactional(rollbackFor = {Exception.class}) public LayuiPageInfo save(ProblemEntity problem) { problem.create(); //生成业务关联id 并保存。内部根据是否存在关联id而选择是否需要赋值 uploadService.saveOrUpdateFile(problem, UploadServiceType.PROBLEM_SERVICE_TYPE); //保存业务数据 boolean b = problemMapperService.save(problem); return b ? LayuiPageFactory.createMsgInfo("保存成功!") : LayuiPageFactory.createErrInfo("保存失败!", -1); }
页面需要文件关联详情的,也就是详情和修改的时候,list集合的时候,一般不用查
@Override public ProblemEntity detail(int id) { //查业务数据 ProblemEntity entity = problemMapperService.getById(id); //查文件详情集合并赋值 uploadService.fileDetail(entity); return entity; } @Override @Transactional(rollbackFor = {Exception.class}) public LayuiPageInfo update(ProblemEntity problem) {//生成业务关联id 并保存。内部根据是否存在关联id而选择是否需要赋值 uploadService.saveOrUpdateFile(problem, UploadServiceType.PROBLEM_SERVICE_TYPE); boolean b = problemMapperService.updateById(problem); return b ? LayuiPageFactory.createMsgInfo("修改成功!") : LayuiPageFactory.createErrInfo("修改失败!", -1); } @Override @Transactional(rollbackFor = {Exception.class, RuntimeException.class}) public LayuiPageInfo delete(String id) { //1:查询中间表文件,删除文件 ProblemEntity problemEntity = problemMapperService.getById(id); if (problemEntity != null) { uploadService.deleteFile(problemEntity); //2:删除问题数据 boolean b = problemMapperService.removeById(id); return b ? LayuiPageFactory.createMsgInfo("删除成功!") : LayuiPageFactory.createErrInfo("删除失败!", -1); } return LayuiPageFactory.createErrInfo("文件不存在!", -1); }
最后的效果
查看
修改
点击放大
服务器的文件就是这样分部的
文件详情表
文件业务关联表
业务表字段