首页 / 资讯中心 / 文章详情

前后端分离档案管理系统实战:SpringBoot+Vue+MyBatis全栈踩坑记录

前后端分离档案管理系统实战:SpringBoot+Vue+MyBatis全栈踩坑记录 ★ FEATURED ARTICLE
前后端分离档案管理系统这套东西我实际做下来最大的感受是技术栈看似“普通”但真的把一个完整项目从零到部署跑通中间踩的坑比想象中多得多。SpringBoot Vue MyBatis MySQL这四个词单拎出来哪个都不稀奇合在一起要做出一个能真正交付、能上线、能在服务器上稳定跑的档案管理系统涉及到的细节足够写好几篇长文。这篇就把我完整做完这个模拟项目X之后的经验整理出来包括技术选型为什么这么定、权限模型怎么设计、前后端是怎么联调的、上传文件有哪些隐藏问题以及最后部署上线时的各种坑。适合同样在做课程设计、毕业设计、或者公司内部想快速搭一套带权限管理的后台系统的开发者参考也适合刚学完 SpringBoot 和 Vue 想找个完整项目练手的朋友对照着看。1. 内容整体设计与思路拆解1.1 这套技术组合为什么被反复使用很多人在选型时会纠结是不是要用微服务是不是要上 Redis是不是要搞得“高大上”一点我的建议很直接做档案管理系统这种业务明确、权限要求清晰、并发量不夸张的系统SpringBoot Vue MyBatis MySQL 就是足够稳的搭配。SpringBoot 解决了后端配置地狱的问题。以前用 Spring 写个项目要配一堆 XML现在一个spring-boot-starter-web拉进来内嵌 Tomcat打成一个 jar 包就能跑这在部署阶段省了非常多事。Vue 则是当前国内前端生态里最成熟的选择之一配合 Element UI 这类组件库做后台管理界面几乎是开箱即用。MyBatis 的半自动化 SQL 控制力很强对于档案管理中常见的多表联查、条件动态拼接比全自动 ORM 更直观可控。MySQL 更不用多说了稳定、免费、文档多无论你是在自己电脑上跑还是部署到服务器遇到问题能搜到大量解决方案这对新手来说特别重要。这套组合之所以反复出现在各类项目里本质上是因为它把“开发效率”和“可控程度”平衡得很好。你不用为了一个单体应用强行上微服务那一套也不用为了几个 RESTful 接口引入太重的前端架构。用生活化类比来说这套技术栈就像装修里的大白墙加原木地板——不惊艳但百搭、耐造、出了问题好补救而且你总能在各个平台找到已经装过一遍的人的记录。1.2 前后端分离的边界应该怎么划既然标题挂了“前后端分离”那这个分离就不只是“前端写一套页面后端写一套接口”这么简单。划分边界最关键的是明确谁负责什么后端只负责业务逻辑、数据持久化、权限校验输出 JSON。前端只负责页面渲染、用户交互、请求后端接口并处理响应。会话状态通过 Token 机制维持不用后端 session。前端静态资源由 Nginx 托管后端接口走独立路径。我见过不少所谓“前后端分离”的项目实际还是后端 Controller 里返回一个页面路径或者把前端打包出来的文件直接丢进后端静态目录。这样做工作量和真正的前后端分离差不多但耦合度上升了——前端改一个静态资源都要重启后端服务这个别扭感会在后期迭代时被无限放大。真正分离之后前端开发只需要 mock 接口就能独立干活后端开发也不用关心页面长什么样测试也能分别对两端做验证。我在做这个项目时坚持的原则是后端接口一律返回统一响应结构业务状态码加数据加消息前端根据状态码统一处理错误。这样写出来的代码两端各看各的约定文档联调时几乎不会因为“返回格式不统一”这类问题反复扯皮。1.3 档案管理系统的核心需求拆解如果只是把常见的增删改查套个壳那这个项目做完不会有太大价值。档案管理系统相比普通后台管理系统有几个特征值得注意第一档案数据一旦录入是需要长期留存的所以回收站、逻辑删除、恢复机制不是可选项而是必需品。第二档案涉及密级和部门归属权限控制必须能细分到角色和操作级别不能只做一个“是管理员还是普通用户”的粗粒度判断。第三档案往往需要附带扫描件、图片等材料文件上传和路径管理要提前设计否则后期数据一多就是一场灾难。综合这些需求我把整个系统拆成了这几个核心模块用户登录与认证、用户与部门管理、角色与权限管理、档案类别管理、档案信息管理、档案借阅/归还记录、文件上传与下载、操作日志、回收站。每个模块往下还能继续拆但整体脉络清楚了写代码就只是时间问题。2. 核心功能模块拆解与实操要点2.1 登录认证与 Token 设计思路前后端分离场景下传统的 Session 方案天然不占优势因为跨域时 Cookie 的携带和存储会带来一堆需要额外处理的问题。我用的是 JWTJSON Web Token方案用户登录成功后后端生成一个包含用户ID、用户名、角色信息、过期时间的 Token 返回给前端前端把它存到 localStorage 或 sessionStorage之后每次请求在请求头里带上Authorization: Bearer token后端用拦截器统一校验 Token校验通过就把用户信息塞进 ThreadLocal后续逻辑直接取用。Token 的有效期我设置成 2 小时前端在封装 Axios 的时候通过响应拦截器判断状态码如果返回 401就清掉本地 Token 并跳回登录页。没有做刷新机制原因很简单这个系统的用户量级和并发量并不强刷新 Token 带来的复杂度大于收益。如果你的项目需要长期免登可以考虑加一个 refresh_token 双 Token 方案但那种情况建议直接上 Spring Security Redis而不是自己手写一套。2.2 权限模型RBAC 不只三张表档案管理系统的权限我最开始也想得很简单用户表、角色表、权限表三张表关联一下不就行了等到真做的时候才发现用户和角色之间、角色和权限之间都要有中间表而且权限本身的类型至少得分成两类一类是菜单权限决定你登录后能看到哪些侧边栏另一类是操作权限决定你能不能在页面上点击“新增”、“删除”、“导出”这些按钮。RBAC 这块我在前端也做了配合后端在用户登录后把角色和权限编码列表一起返回前端拿到权限列表后在路由守卫里判断是否能进入某个页面在菜单渲染时过滤掉没有权限的项在按钮操作上通过自定义指令v-permission控制显隐。后端则在 Controller 层做最终的鉴权兜底避免前端绕过按钮直接调接口。这里要重点说明一个安全常识前端控制按钮显隐只是用户体验层面的事真正的安全边界必须由后端把死。我用一个全局拦截器校验 Token 有效性再加一个基于注解的权限校验切面在需要权限的接口方法上标RequiresPermission(system:file:delete)类似的权限码切面执行时比对当前用户权限列表。这套方案比在每个方法里手写代码判断要优雅得多也更容易维护。2.3 档案信息的增删改查与分页查询设计档案表的核心字段自不必多说重点在于查询条件的组合。用户在前端可能同时按档案编号、档案标题、类别、创建时间范围、密级状态来筛选后端如果用固定 SQL 写死查询条件那每多一个筛选条件就要改一次 Mapper非常痛苦。MyBatis 的好处此时就体现出来了用动态 SQL 的where和if标签就能实现条件自动拼接没有传入的条件不会进入 SQL完美匹配这种多条件组合查询场景。分页这里我必须提一个踩坑经验很多人分页用的 PageHelper但 PageHelper 的动态拼接逻辑在某些复杂 SQL 场景下会行为诡异比如在join查询或多表查询时可能统计 count 不准。我在这个项目里没有引入额外的分页插件而是自己写了一个 PageResult 结构体手动通过 LIMIT 控制分页然后在 Service 层分别查一次总条数和当前页的数据列表。代码量多一点但逻辑完全在自己的控制范围内排查问题非常直接。2.4 文件上传的细节问题清单档案管理系统几乎必然要支持附件上传这里面的坑不是一般的多。上传我使用的是MultipartFile后端借助文件存储工具把文件保存到服务器指定目录同时生成一个新的文件名用 UUID 加原文件后缀写入数据库。这里的核心原则是数据库永远不要存原始文件名而是存一个不可预测的新文件名原始文件名单独存一个字段用于展示。为什么这么做因为用户上传的文件名可能包含中文、特殊字符直接用它做路径存储不但会出现编码问题还可能出现路径穿越等安全风险。另外一个容易踩的坑是上传文件大小限制。SpringBoot 默认的上传大小上限是 1MB而真实的档案扫描件动辄几十MB。在 SpringBoot 的配置中需要把spring.servlet.multipart.max-file-size调大同时还要调整max-request-size否则遇到多文件上传时会报“the request was rejected because its size exceeds the configured maximum”这类错误。除此之外Nginx 层如果做了反向代理还要同步调整client_max_body_size这个配置很隐蔽我曾经在本地环境上传一切正常、部署到服务器后一传大文件就 413查了很久才发现是 Nginx 的默认限制。2.5 回收站、逻辑删除与恢复机制在档案管理这种领域物理删除是不被鼓励的。我在所有核心业务表上都加了一个deleted字段查询时默认过滤掉deleted 1的记录。删除操作执行时实际上是把deleted置为 1同时记录删除时间回收站页面专门查询这些已删除的记录恢复操作就是把deleted改回 0。这样设计之后误删恢复变得非常轻松而且还能完整保留操作痕迹。回收站的清理功能采用的是预约清理——系统提供一个接口管理员可以手动清理超过 30 天的已删除记录再配合定时任务自动清理旧数据防止表数据无限膨胀。这个看似简单的逻辑在实际交付中是很加分的因为它直接体现了系统对“数据安全”的理解而不是只会写增删改查的 demo。3. 实操过程与核心环节实现3.1 数据库设计四张核心表起步数据库表的设计我建议从简出发先把最核心的表建好再逐步扩展。以下是这个项目里最核心的几张表结构可以直接参考表名核心字段用途说明sys_userid, username, password, real_name, dept_id, role_id, status, create_time用户表密码存 BCrypt 加密后的密文sys_roleid, role_name, role_code, description角色表角色编码用于后端鉴权archives_infoid, archive_no, title, category_id, level, status, file_path, deleted, create_time档案信息主表file_path 存服务器相对路径archives_borrowid, archive_id, user_id, borrow_time, return_time, status档案借阅记录表密码加密这里多说一句绝不允许明文存储密码也不允许使用 MD5、SHA1 这类简单散列。我使用的是 BCrypt 算法它对同一个密码每次生成的密文都不一样还能内置加盐逻辑即便数据库泄露被反查的难度也高得多。用户在登录注册时根本不需要关心加密算法但作为开发者必须把这些安全底线守住。表结构建好后我在 SQL 初始化脚本里预设了两个角色超级管理员和普通用户以及一个默认管理员账号这样应用启动完成后不需要手动去数据库插数据就能直接登录系统这个细节在交付时体验感会好很多。3.2 后端核心代码解析后端我是严格按照 Controller、Service、Mapper 三层结构组织的。Controller 只负责接收请求、调用 Service、封装返回值Service 层写业务逻辑和事务控制Mapper 层通过 MyBatis 的接口和 XML 或注解映射 SQL。以下是文件上传接口的一个核心代码片段结合上文提到的文件名处理和路径存储PostMapping(/upload) RequiresPermission(archives:file:upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(上传文件不能为空); } String originalFilename file.getOriginalFilename(); String ext ; int dotIndex originalFilename.lastIndexOf(.); if (dotIndex 0) { ext originalFilename.substring(dotIndex); } // 使用 UUID 作为新文件名避免路径穿越和重名覆盖 String newFileName UUID.randomUUID().toString().replace(-, ) ext; String datePath LocalDate.now().toString().replace(-, /); String relativePath upload/ datePath / newFileName; File dir new File(uploadDir / datePath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(uploadDir / relativePath)); // 数据库中只需存储相对路径配合虚拟映射即可访问 return Result.success(relativePath); }这段代码有几个关键点需要理解日期分目录存储可以有效防止单目录文件数量过大UUID 文件名避免中文乱码返回相对路径而不是绝对路径是因为部署在不同的操作系统和服务器路径都可能不同相对路径配合配置文件里的上传路径前缀才是可移植的做法。在开发环境我用本机的一个目录生产环境部署时把这个目录配置到数据盘即可这样系统和数据就分离开以后系统升级不会丢文件。分页查询的代码同样值得仔细看。我在 Service 里先通过 Mapper 的countByCondition查总数再通过selectPageList查列表参数里传入 pageNum 和 pageSizeMapper 里用 LIMIT 做分页Override public PageResultArchivesInfoVO pageList(ArchivesQueryDTO query) { int total archivesInfoMapper.countByCondition(query); int pageNum query.getPageNum(); int pageSize query.getPageSize(); int offset (pageNum - 1) * pageSize; query.setOffset(offset); ListArchivesInfoVO list archivesInfoMapper.selectPageList(query); return PageResult.of(total, list); }这个写法是 PageHelper 之外的稳妥替代方案代码一目了然不需要理解插件背后的字节码增强逻辑排查起来非常友好。3.3 前端核心实现Vue Element UI前端的工程结构我按 Vue CLI 标准脚手架创建目录上做了分层router 目录放路由配置和守卫逻辑store 目录放全局状态当前用户信息、Token、权限列表等api 目录统一放接口请求函数utils 目录挂 Axios 实例和工具函数views 目录按页面模块组织。Axios 封装是我在前端部分最强调的环节。每个请求都需要自动在 header 里带 Token每个响应都要统一处理业务状态码、HTTP 状态码并在请求出错时统一弹提示。以下是请求封装的核心逻辑简化版import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_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) { Message.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } Message.error(error.response ? error.response.data.msg : 网络异常) return Promise.reject(error) } )这段代码把三个常见问题一次性解决了一是未登录或 Token 过期时直接跳回登录页二是业务异常时前端不用每个页面都写一堆 if else而是一行代码统弹出消息三是所有请求都带上了认证信息不用在每次调用接口时手动传 Token。路由守卫部分我在router.beforeEach里做了三件事判断是否放行登录页判断用户是否已登录有没有 Token根据后端返回的权限列表检查当前路由的 meta 权限码没有权限直接跳到 403 页面。菜单渲染则根据路由表过滤出有权限的项实现用户登录后只看到自己能看到的侧边栏。3.4 前后端联调的实践心得联调阶段最容易出的问题反而不是接口本身而是各种环境配置的小事。跨域就是其中一个经典问题。开发环境下我用 Vue CLI 自带的代理功能解决跨域在vue.config.js里配置/api代理到后端的localhost:8080后端接口统一前缀为/api。这样可以避免前端单独去配允许跨域的代码因为开发时打开浏览器的域名是localhost:8081前端端口而请求目标是localhost:8080后端端口如果不代理几乎必然出现跨域被拦截。生产环境则不需要后端配置跨域因为 Nginx 把前端静态资源和/api请求都代理到了同一个域名下同域请求不存在跨域问题。这个差异我在不少项目里看到有人搞混开发环境配了代理生产部署时又想当然地在后端配了CrossOrigin或全局 CORS 配置结果把接口暴露得彻彻底底这是安全意识不强的表现。正确做法是生产环境由 Nginx 接管后端不额外放开跨域限制。4. 部署上线与运维排错实战4.1 本地快速启动步骤在本地跑起来这个项目操作路径很清晰按顺序执行就不会出岔子先确认环境JDK 8 或 11、MySQL 8.x、Node 14 以上。创建数据库执行sql/init.sql脚本自动创建表结构和初始数据。修改后端配置文件application.yml中的数据库连接信息和文件存储路径。在 IDEA 里直接运行 SpringBoot 启动类后端就起来了访问http://localhost:8080/api/ping能返回数据则正常。前端项目目录下执行npm install安装依赖完成后再执行npm run serve浏览器访问http://localhost:8081使用初始化脚本里的管理员账号登录。这里我要专门提醒两个问题第一npm install如果安装过慢建议用国内镜像源能节省大量等待时间‘设置完毕后重新执行即可’第二JDK 版本不要随意用太高SpringBoot 2.x 默认最高支持到 JDK 8 或 11如果环境装的是 JDK 17可能出现一些隐性的兼容问题推荐java -version确认一下。4.2 服务器部署jar Nginx 分离方案服务器部署时后端我用 jar 方式运行前端打包后的 dist 目录由 Nginx 托管配置思路如下# 后端打包并后台启动 mvn clean package -DskipTests nohup java -jar archives-system.jar --spring.profiles.activeprod logs/run.log 21 # 前端打包后把 dist 目录放到服务器 npm run buildNginx 配置里做三层处理一是location /指向前端 dist 目录处理页面路由时需要配置try_files $uri $uri/ /index.html否则刷新二级页面会 404二是location /api/反向代理到后端的 8080 端口三是client_max_body_size根据上传需求调大。以下是一个可参考的简化配置server { listen 80; server_name your-domain.com; client_max_body_size 100m; root /var/www/archives/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意这里的proxy_pass后面没有带/api部分因为我配置里后端接口没有统一加server.servlet.context-path/api如果在后端上下文配置过代理时要反过来去掉前缀我项目里把context-path配置为/api然后proxy_pass http://127.0.0.1:8080;这样路径会原样传到后端。如果发现部署后前端请求接口全部 404八九成就是这里的路径处理没对齐先去检查接口真实路径再去核对 Nginx 转发规则。4.3 线上环境常见安全隐患部署到服务器之后安全问题会从“纸面上的规范”变成“真刀真枪的考验”。以下几个点我是专门检查过并且建议你一定不要跳过的第一MySQL 数据库账号一定要新建专用账号不要用 root 直接连应用。最小权限原则在这里体现得很直接给应用一个只能操作指定数据库的账号即使被注入攻击者能造成的破坏也要小很多。第二后端 jar 包的运行用户不要用 root创建一个普通用户去跑服务防止接口漏洞被利用后拿到 root 权限。第三文件上传目录权限设置为应用账号可读写即可不要赋予 777 权限。我见到过一些部署教程教人一把梭数据库 root 裸奔、后端 root 拖起来跑、上传目录 777。这样部署确实最省事但等于把整个服务器的钥匙都挂在门口。企业里的档案系统数据是最敏感的资产这些红线碰都不能碰。4.4 常见问题与排查技巧实录问题现象可能原因排查与解决前端页面能打开但接口全部 404Nginx 代理路径和后端 context-path 不一致打开浏览器 F12看实际请求路径再核对 Nginxproxy_pass是否吃掉或保留了前缀上传大文件报 413 Request Entity Too LargeNginx 的client_max_body_size默认只有 1m在 server 块中增加配置并 reload Nginx上传文件保存失败报 IOException 或目录不存在应用账号没有上传目录的写权限确认目录所有者是运行 jar 的账号执行chown -R appuser:appuser /data/upload登录后很快被踢下线Token 有效期太短或前后端时间不同步检查 Token 过期时间和服务器时间必要时用date -R对比时区页面刷新后 404前端路由为 history 模式但 Nginx 没有 try_files 配置按上文 Nginx 配置增加 try_files数据库 MySQL 8 连接报“Public Key Retrieval is not allowed”MySQL 8 的认证插件问题在 JDBC 连接字符串中增加allowPublicKeyRetrievaltrueuseSSLfalse其中一个我实际踩过的最隐蔽的坑是“后端接口在本地正常、部署到服务器之后偶发 500具体报错是数据库连接超时”。排查到后面发现是 MySQL 连接池默认配置的 wait_timeout 是 8 小时应用空闲一整夜后连接失效第二天第一次请求直接报错。办法是给数据源配置连接池的参数让 HikariCP 的max-lifetime小于 MySQL 的wait_timeout并且开启连接有效性检测。像这类问题不在配置层面提前预防线上迟早要踩。5. 交付与二开源码结构和扩展方向5.1 源码目录结构说明完整的源码要能让人拿过去就能看、就能改清晰的目录结构是第一步。后端的包结构我按功能分包而不是按技术层分包com.demo.archive ├── config # 配置类拦截器、跨域、文件映射等 ├── controller # 接口层接收参数、返回结果 ├── service # 业务层事务控制、逻辑编排 ├── mapper # MyBatis 数据访问层 ├── entity # 数据库实体类 ├── dto # 请求参数封装避免实体类直接暴露 ├── vo # 响应结果封装按需裁剪字段 ├── common # 通用类如统一返回结果、异常处理、常量 └── util # 工具类如 JWT 工具、文件处理工具前端的目录结构也按模块划分src ├── api # 后台接口的请求函数封装 ├── assets # 静态资源 ├── components # 通用组件 ├── layout # 后台整体布局包含侧边栏、导航栏 ├── router # 路由配置与守卫 ├── store # Vuex 状态管理 ├── styles # 全局样式 └── views # 页面组件按模块分文件夹这套结构不是最优的架构但它一定是最容易让别人理解的架构。对于学习型项目来说可读性远比优雅性重要。5.2 进阶扩展的两个方向做完整套系统后如果想让这个项目更有竞争力有两个方向我认为值得深入。第一个方向是消息提醒档案借阅到期后系统自动发送到期提醒。这个扩展需要引入定时任务和消息推送机制逻辑不算复杂但在真实场景中很受欢迎。第二个方向是数据可视化在首页增加档案总量、季度借阅量、档案分类占比等统计图表前端配合 ECharts 做可视化大屏。这两个方向都需要后端增加统计查询接口、前端增加图表组件工作量适中但提升的完成度是非常明显的。我个人在实际操作中的体会是一个项目能不能真正“收口”拼的不是用了多新的技术而是细节有没有打磨到位——权限按钮有没有真实控制住、文件上传路径有没有规划好、部署之后能不能稳定跑一个月不报错。这些地方做扎实了代码本身的水平高低反而不是最重要的。整套系统做完之后我最大的感受是如果你的目标不是造一款颠覆性的平台而是扎扎实实解决一个场景内的所有业务问题SpringBoot Vue 这套组合已经绰绰有余了。最后再分享一个小技巧部署完成后在服务器上用一个简单的定时任务每天凌晨备份 MySQL 数据。数据备份这件事日常看起来毫无存在感但真到了需要恢复数据的那一天你会感谢当初这个不到五分钟就能写好的脚本。运行指令也很简单一行 cron 加上 mysqldump 就能完成0 2 * * * mysqldump -u archive_app -pArchivePass123 archives_db /data/backup/archives_$(date \%Y\%m\%d).sql 2/dev/null这算是经验之谈也是项目交付中最容易被忽略、却最值得做的“不显眼”工作。如果你也在跑类似的系统项目希望这篇记录能帮你少走几步弯路。
阅读完成 · 觉得有帮助?
咨询建站