ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot + Vue + MyBatis 档案管理系统开发实战与踩坑指南

SpringBoot + Vue + MyBatis 档案管理系统开发实战与踩坑指南 做这类档案管理系统属于典型的企业级内部系统也是毕业设计里的常客了。SpringBoot Vue MySQL MyBatis 这套组合说它是当前 Java 全栈开发的主流配置一点不夸张。这篇文章我会直接从实操角度拆解讲讲这套系统的设计思路、数据库怎么建模、前后端重点代码怎么写、联调部署时容易在哪里翻车尽量把每一步背后的考虑也讲清楚方便你照着复现或者改造。1. 这是一个什么项目为什么要这么做1.1 档案管理系统的真实业务场景先搞清楚一个问题档案管理系统到底管的是什么。很多人一上来就建表写 CRUD结果做着做着发现系统根本不符合实际场景。档案管理本质上解决的是两个问题怎么把散落的纸质档案数字化、结构化地存起来以及怎么让人快速找到自己需要的那一份文件。典型的使用对象是公司的行政档案室、高校的学籍档案科、银行保险等金融机构的客户资料库。日常操作不外乎这几类档案员做入库登记录入档案编号、名称、类型、密级、存放位置、归档日期。管理员做分类维护档案类型不是无限级的通常就是一个树形分类比如人事档案下面分入职材料离职材料。普通用户做检索借阅按关键词查档案列表提交借阅申请查看审批状态。系统管理员做用户和权限管理不同角色可见范围不一样涉密档案不是谁都能看的。这个系统最容易被忽视的一点是档案和文件的关系是一卷多件。一个案卷比如一个人的人事档案里可能包含多份文件身份证复印件、学历证明、劳动合同所以数据库设计时不能只建一张档案表必须考虑主表 子表或者 JSON 扩展字段这是和普通增删改查项目最大的区别。1.2 为什么选 SpringBoot Vue MyBatis 这套组合这个选型放在今天依然是最稳妥的没有之一。我接触过很多类似项目有的当年用的是 Struts2 JSP有的用 SSM 手写配置现在是 SpringBoot Vue 时代了理由很实在SpringBoot 把配置简化到了极致。不需要再写一堆 XML 装配 Bean内嵌 Tomcat 让部署变成一个可执行 JAR。对毕设或者小团队内部系统来说这点太重要省下的时间全可以花在业务逻辑上。Vue 是当前前端最容易上手的框架之一。配合 Element UI 这类组件库一个表格页、表单页、弹窗确认前后端分离的交互模式下写起来非常顺手。相比 JSP 时代的模板渲染Vue 的响应式数据绑定让页面状态的维护轻松一个量级。MyBatis 最大的优势是 SQL 可控。档案检索这种场景查询条件经常是档案名称模糊 类型精确 时间范围 密级约束这种组合条件MyBatis 的动态 SQL 能根据条件拼接语句不用在 Java 代码里搞一堆的 if 判断拼字符串也容易优化 SQL。MySQL 免费、资料多、生态成熟。不管学校机房还是公司内网都能装遇到问题随便一搜就能找到答案。一句话总结这套组合就是为了中小型管理系统量身定做的几万条数据量下性能完全够用开发效率高学习曲线平缓网上资料密度极高。2. 整体架构设计与数据库建模2.1 前后端分离架构怎么分层既然选了 Vue 做前端自然就走前后端分离的路子。这意味着后端只负责出接口前端只负责渲染页面和交互两者通过 JSON 通信。实际项目里我会把后端代码分成下面几层每个包职责很明确controller接收请求、参数校验、调用 service、封装返回值。只做调度不写业务。service业务逻辑层。比如档案借阅流程里判断库存 - 创建借阅记录 - 修改档案状态这种事务操作都在这层。mapper数据访问层也就是 MyBatis 的接口。SQL 写在 XML 文件里一个方法对应一条 SQL。entity / dto实体类对应表字段DTO 用于接收查询参数和响应数据避免把实体直接暴露给前端。这一点容易被新手忽略但做接口规范化时很重要。config放 WebMvc 配置、跨域过滤、拦截器等系统级配置。返回结构建议统一不要一会返回 Map一会直接丢 List。我常用的是ResultT通用类包含 code、message、data 三个字段。前端 Axios 拦截器里统一处理 code等于全站的错误处理入口只需要写一遍。2.2 数据库表设计档案分类、元数据与文件存储档案系统的成败一半看表设计。我按实际经验把核心表列出来大家可以参考着裁剪archive_type档案分类表字段类型说明idbigint主键namevarchar(50)分类名称parent_idbigint父分类 id顶级为 0order_numint排序号create_timedatetime创建时间archive档案主表字段类型说明idbigint档案 idarchive_novarchar(50)档案编号全局唯一titlevarchar(200)档案标题type_idbigint所属分类secret_leveltinyint密级1 公开 2 内部 3 机密statustinyint状态1 在库 2 借出 3 销毁storage_locationvarchar(100)存放位置如A区-3排-2节descriptiontext备注描述create_bybigint创建人 idcreate_timedatetime归档时间archive_file档案文件表一卷多件字段类型说明idbigint文件 idarchive_idbigint所属档案file_namevarchar(200)原始文件名file_pathvarchar(500)存储相对路径file_sizebigint字节数file_typevarchar(20)扩展名如 pdf/jpgupload_timedatetime上传时间用户、角色这几张表就是常规设计不做赘述。这里重点提醒几个细节archive_no建议在程序里生成比如类型编号 日期 流水号的格式。不要靠数据库自增因为档案编号有实际业务含义需要具备一定的可读性。file_path存的是相对路径而不是完整 URL。因为将来系统部署可能换机器、换端口完整 URL 写进数据库会让迁移变得极其痛苦。密级字段不要用 varchar 存中文。用 tinyint 存数字查询排序都方便显示层再映射成机密字样。常用查询字段如type_id、status、create_time建议建索引。数据量超过十万以后全表扫描会明显拖慢检索速度。3. 后端核心实现登录鉴权、档案CRUD与文件上传3.1 登录与权限控制的两种主流做法这类管理系统最常见的鉴权方案有两种Session 和 Token实际项目中我推荐从这两个里二选一。Session 方案适合单机部署实现最简单登录成功把用户信息放进 Session拦截器里判断session.getAttribute(loginUser)是否为 null。优点是后端完全控制安全缺点是不方便横向扩展并且跨域时配置较繁琐。Token 方案适合前后端分离部署在跨域环境下登录成功后用 JWT 签发一个 token前端存到localStorage每次请求放进Authorization请求头后端写一个过滤器或拦截器校验解析。需要注意的坑是 token 有效期设置——太短会导致用户体验极差频繁要求重新登录太长又有安全风险。我一般给管理系统设 2 到 8 小时同时登录一次下发两个 tokenaccessToken2 小时 refreshToken7 天刷新接口只校验 refreshToken。毕设阶段做简单点可以只用一个 token但有效期务必要设置。角色权限这块不要一开始就把 RBAC 做得特别重。通用做法是三张表用户表、角色表、用户角色关联表。如果需求里有菜单级别的可见性控制再加一张权限表用 role 直接关联菜单列表前端登录后通过/user/info接口拿角色和菜单动态渲染菜单栏。很多系统实际只分管理员和普通用户两种角色用 SQL 查询时加个if条件就完成数据级控制不需要过度设计。3.2 MyBatis 动态SQL实现多条件档案检索档案检索是系统的核心功能也是动态 SQL 最典型的应用场景。比如前端传来的查询条件可能是这样的searchForm { keyword: 张, typeId: 3, secretLevel: 2, status: 1, beginTime: 2024-01-01, endTime: 2024-12-31 }对应 MyBatis Mapper XML 里的写法select idselectArchivePage parameterTypemap resultTypecom.demo.entity.Archive select a.*, t.name as typeName from archive a left join archive_type t on a.type_id t.id where if testkeyword ! null and keyword ! and a.title like concat(%, #{keyword}, %) /if if testtypeId ! null and a.type_id #{typeId} /if if testsecretLevel ! null and a.secret_level #{secretLevel} /if if teststatus ! null and a.status #{status} /if if testbeginTime ! null and beginTime ! and a.create_time gt; concat(#{beginTime}, 00:00:00) /if if testendTime ! null and endTime ! and a.create_time lt; concat(#{endTime}, 23:59:59) /if /where order by a.create_time desc /select这里有两个必须注意的点。第一where标签会自动去掉第一个多余的and手写 SQL 搭配where 11的做法也能跑但可读性差不建议。第二模糊查询务必使用like concat(%, #{keyword}, %)这种参数占位方式不要直接like %${keyword}%。$是字符串拼接存在 SQL 注入风险一个引号就能让你的登录接口被绕过还可能导致拖库。另外前端传时间范围往往带T如2024-01-01T00:00:00后端接收后最好统一格式化避免日期字符串直接存库。分页插件我习惯用 PageHelper用法很简单PageHelper.startPage(pageNum, pageSize); ListArchive list archiveMapper.selectArchivePage(queryMap); PageInfoArchive pageInfo new PageInfo(list);只要执行查询前调用startPagePageHelper 会自动拦截下一条 SQL 拼上 limit同时查出总条数。注意一个坑startPage之后必须紧跟一条查询语句中间不能穿插其他数据库操作否则分页会失效或者作用到错误的 SQL 上。3.3 案卷文件上传与存储路径规划档案文件上传是这类系统的另一个硬需求。我把流程拆分一下前端用 Element UI 的el-upload设置action指向后端/file/upload接口上传成功之后返回文件的 id 和路径再把路径作为参数随档案表单一起提交。这样做的好处是文件上传和档案保存解耦即使档案信息填写有误文件也已经保存好了不用重复上传。后端 MultipartFile 接收文件存储规则我很建议按日期分目录String uploadDir D:/archive/upload/ new SimpleDateFormat(yyyy/MM).format(new Date()); File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFilename UUID.randomUUID().toString().replace(-, ) ext; file.transferTo(new File(dir, newFilename));文件名务必用 UUID 重命名这个命令我不能更严肃地强调——直接用用户原始文件名很容易造成文件覆盖和内网中文名乱码问题。如果这个系统将来要做在线预览建议只接收 pdf、jpg、png 这几种格式并做大小限制比如最大 20MB。文件下载用 SpringBoot 自带的ResponseEntityResource实现最省事GetMapping(/file/download) public ResponseEntityResource download(RequestParam String filePath) throws IOException { File file new File(realBasePath, filePath); Resource resource new FileSystemResource(file); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, attachment; filename\ URLEncoder.encode(file.getName(), UTF-8) \) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(resource); }如果文件是 PDF 且在本机查看Content-Disposition设为inline可以让浏览器直接打开预览。名字编码那行是我踩过坑的地方不URLEncoder.encode的话中文文件名会直接乱码。4. 前端 Vue 项目实战4.1 Vue CLI 工程搭建与 Element UI 布局前端工程我建议用vue create直接生成选 Vue 2 Vue Router 配 Element UI或者 Vue 3 Element Plus看你的环境基础。如果毕设使用导师或者老师机器上的环境大概率是 Java 思路用 Vue 2 反而兼容性更好。项目结构调整成常规模式src/router路由表配置页面路径和路由守卫。src/api按模块拆分接口请求文件比如archive.js里集中放getList、save、delete、upload所有档案相关的接口调用。src/views页面组件按业务模块建目录。src/utils/request.jsAxios 实例封装统一设置 baseURL 和拦截器。管理系统后台布局通常是左侧菜单 顶栏 内容区的经典结构。Element UI 的el-container组件可以快速组合菜单通过路由数据动态生成el-menu设置router属性后点击菜单项会自动根据 index 跳转对应路由少写很多跳转逻辑。4.2 Axios 封装与路由守卫不要在每个页面里直接调 axios这是前端工程化最基本的要求。我通常会在request.js里做这些事import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { // 提示错误信息 return Promise.reject(res) } return res }, error { // 401 跳转登录页 return Promise.reject(error) } ) export default service设置baseURL: /api后本地开发时候通过 devServer 的 proxy 把请求代理到后端端口生产环境部署时前端静态资源由 Nginx 托管同一路径下的/api反代到 Java 服务。这样前端环境里既有写死的后端地址后续迁移更灵活。Vue 项目的vue.config.js里这样配devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } }路由守卫的逻辑是前端权限控制的第一道闸门。后端接口有鉴权前端也要保证未登录用户访问不到内部页面router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else { if (!token) { next(/login) } else { next() } } })4.3 档案列表页与上传组件的细节处理档案列表页是整个系统前端的核心页面一般包含搜索区、表格区、分页区。搜索区用el-form内联布局关键字、类型下拉、密级下拉、时间范围点击查询按钮后this.searchForm.pageNum 1再拉列表。这里有一个容易犯的小错误修改查询条件后页码不清零导致数据不是在第二页就是空页面体验很糟糕。表格操作列里上传文件这个功能建议用el-upload放在弹窗里。注意设置limit后on-exceed要给出提示否则超出限制时用户完全不知道发生了什么。文件上传中进度条可以用on-progress事件来更新percentage数据实时反馈上传状态这个细节做好了整个系统的使用质感会明显提升。新增编辑档案时表单校验用el-form的rules属性去完成。比如编号必填、标题必填、密级必选。校验规则除了required: true之外还可以加正则校验编号格式。弹窗关闭前记得调用resetFields()清空表单状态否则下次打开上次填的数据还在多少人在这个坑里被坑过。5. 部署联调与常见问题排查5.1 本地开发环境配置要点环境配置部分我从 Java 端到前端按顺序说一遍。后端方面application.yml里数据库连接信息用环境变量占位不要把真实密码写死在配置文件里。比如spring.datasource.password: ${DB_PASSWORD}。至少做到本地开发和服务器使用不同的 profile。MySQL 字符集要在建库时指定不然后面中文乱码会非常闹心CREATE DATABASE archive_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;一定要用 utf8mb4不要用 utf8。档案里可能包含生僻字或者特殊符号utf8 的坑就是存不了一些四字节字符存储直接报错。同时数据库连接串加上参数useUnicodetruecharacterEncodingutf8mb4或用characterEncodingutf-8。前端方面创建工程时候选择npm或yarn然后npm install安装依赖。如果网络不稳定遇到依赖下载失败可以配置淘宝镜像源再重新安装。Vue 项目启动命令是npm run serve在浏览器访问本地地址。联调时最容易出现的问题就是跨域。如果不用 devServer proxy而是直接访问后端地址浏览器会拦截非同源的请求。Nginx 部署时推荐用反向代理解决server { listen 80; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意proxy_pass后面有斜杠和没有斜杠的区别很大不带斜杠是保留完整路径带斜杠会把/api前缀去掉。后端接口路径没带/api前缀时这里就必须带着斜杠去做路径重写。5.2 高频踩坑记录与解决方案速查表我把实操中整理的问题清单直接列出来读者可以对应排查表现原因解决方案页面请求 404但 controller 确实存在前后端路径不匹配或字段名不对先看浏览器 Network 里实际请求 URL比对控制层RequestMapping接口返回 500日志提示 SQL 列不存在实体字段/数据库字段映射失败检查TableField或结果为 Map 时的字段别名POST 请求中文乱码前后端编码不一致前端用 UTF-8后端配置 CharacterEncodingFilter数据库连接串加编码参数上传文件超过限制报错默认限制 1MBspring.servlet.multipart.max-file-size20MBmax-request-size 同时调大分页查询总条数不对startPage 和查询之间插入了其他 SQL保证startPage紧贴 mapper 查询方法Session 登录后第二次请求又提示未登录前后端分离跨域没携带 Cookie方案一用 token方案二开启跨域时withCredentials: true日期参数绑定失败前端传字符串后端 Java 类型不匹配用DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss)或在全局配置日期转换器Vue 打包后刷新页面 404前端是 history 路由服务器没有 fallbackNginx 配置try_files $uri $uri/ /index.html;表格数据量太大加载卡顿没有分页或全量加载后端必须分页前端表格开启 virtual 渲染大数据量时还有一个非常隐蔽的坑前端 Axios 设置的responseType: blob下载文件时导出接口报错返回的 JSON 会被当作文件下载下来打开是乱码。解决方案是下载接口返回前判断response.data.type是否为application/json是则提示后端错误信息而不是下载。部署层面的经验总结下来就是后端mvn clean package打成 jarnohup java -jar xx.jar log.txt 21 启动前端npm run build生成 dist 目录交给 Nginx 托管数据库脚本提交到项目里和源码一起管理不要只留在开发机里。这样换机器、交毕设材料时候一套流程下来非常顺畅。我个人实际操作中的体会是这类系统功能的逻辑难度普遍不高真正的价值体现在各种分的边界情况处理上。边界条件、用户反馈、文件异常、安全校验这些才是决定一个系统能不能真正交付使用的关键所在。比如搜索关键字为空格时后端要把空白过滤掉比如档案被借出后不允许再次借阅再比如删除档案时要先把它下面的文件记录一并清理否则数据库会有孤儿数据。把这些细节补齐后这个项目就不仅是一个作业而是真正具备上线条件的内部工具了。最后再分享一个小技巧开发过程中直接写一个data_init.sql脚本把测试用的分类数据、管理员账号、演示用的几份档案全部准备齐全。每次重置数据库后一键导入调试效率能提升好几倍。回头去看我当时给这个档案系统搭的最小角色体系只有管理员和档案员两个角色数据层面用create_by字段区分权限范围整个权限逻辑压缩到两个if判断却完美覆盖了实际需求。系统设计这件事很多时候做减法比做加法更难也更有价值。
返回列表