ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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

前后端分离档案管理系统这套东西,我实际做下来最大的感受是:技术栈看似“普通”,但真的把一个完整项目从零到部署跑通,中间踩的坑比想象中多得多。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 的携带和存储会带来一堆需要额外处理的问题。我用的是 JWT(JSON 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 Result<String> 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 和 pageSize,Mapper 里用 LIMIT 做分页:

@Override public PageResult<ArchivesInfoVO> pageList(ArchivesQueryDTO query) { int total = archivesInfoMapper.countByCondition(query); int pageNum = query.getPageNum(); int pageSize = query.getPageSize(); int offset = (pageNum - 1) * pageSize; query.setOffset(offset); List<ArchivesInfoVO> 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 本地快速启动步骤

在本地跑起来这个项目,操作路径很清晰,按顺序执行就不会出岔子:

  1. 先确认环境:JDK 8 或 11、MySQL 8.x、Node 14 以上。
  2. 创建数据库,执行sql/init.sql脚本,自动创建表结构和初始数据。
  3. 修改后端配置文件application.yml中的数据库连接信息和文件存储路径。
  4. 在 IDEA 里直接运行 SpringBoot 启动类,后端就起来了,访问http://localhost:8080/api/ping能返回数据则正常。
  5. 前端项目目录下执行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.active=prod > logs/run.log 2>&1 & # 前端:打包后把 dist 目录放到服务器 npm run build

Nginx 配置里做三层处理:一是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 连接字符串中增加allowPublicKeyRetrieval=true&useSSL=false

其中一个我实际踩过的最隐蔽的坑是“后端接口在本地正常、部署到服务器之后偶发 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

这算是经验之谈,也是项目交付中最容易被忽略、却最值得做的“不显眼”工作。如果你也在跑类似的系统项目,希望这篇记录能帮你少走几步弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进