☰
从零搭建前后端分离的高校办公室行政管理系统:SpringBoot+Vue实战记录
2026/10/22 10:10:13 网站建设 项目流程

把一套前后端分离的高校办公室行政事务管理系统从零搭起来,再完整部署上线,整个过程我前前后后做了不下三回。这套系统用的技术组合很老实:后端SpringBoot+MyBatis+MySQL,前端Vue,整体遵循MVC分层思路。今天这篇就是完整记录,从技术选型到表结构设计,从核心接口到Nginx部署,顺便把我在开发中踩过的坑一起写在里面,适合正在做课设、毕设,或者准备接手类似管理系统项目的同学参考。

如果只挑一句话来说,这套系统的价值在于把“行政事务”从纸质审批和Excel表格里解放出来。公告、会议室预约、用章申请、公务用车、值班安排这些场景,以前靠人工登记,现在全部在线流转,权限清楚、记录可查。对刚接触前后端分离项目的人来说,它又是一个信息量非常合适的样本:不算特别复杂,但MVC分层、RBAC权限、JWT认证、接口对接、前后端打包部署这些关键环节一个不少,照着源码走一遍,基本上能把主流的开发流程打通。

1. 项目定位与整体设计思路

1.1 为什么选择前后端分离架构

我最早接触这类管理系统时,用的是单体模板引擎,页面是服务端渲染,改一个按钮样式都得重启服务,加接口还要同时碰页面代码。后来接手一套前后端分离的代码,才发现两边解耦之后舒服很多:前端只管渲染和交互,后端只管出数据,只要接口约定好,两边可以同时开工。特别是做高校办公室这类内部系统,需求经常变,前端改个表单不用动后端,后端调整审批逻辑也不影响页面,迭代速度快很多。

有人会觉得前后端分离增加了部署成本,确实,算上Nginx反向代理、跨域配置、构建流程,前期要比单体项目麻烦一点。但它换来的是结构清爽和后续扩展的可能性,比如未来要加移动端H5、小程序,后端接口可以直接复用,不需要重新做一套。对于毕设和课设来说,前后端分离这种架构也比较容易在答辩时展开讲,MVC的思想在后端三层里体现得很清楚,前端配合Vue组件化,整个项目一眼就能看出分层关系。

1.2 角色与业务模块怎么切

系统设计之初,我把用户分成三类:系统管理员、办公室工作人员、部门负责人。管理员维护基础数据和用户权限,办公室工作人员负责日常业务录入和发起申请,部门负责人做审批确认。业务模块围绕高校办公室的高频行政场景来设计,包括公告管理、会议室预约、用章申请、公务用车申请、值班排班、文件收发登记。每个模块都离不开一个核心状态:待提交、待审批、已通过、已驳回、已归档。

这个状态字段是整套系统的命脉,设计的时候我统一命名为status,后端用整数表示,前端通过字典翻译成标签。比如用章申请,工作人员填完申请表之后status是0,负责人看到待审批列表,点击通过后变成1,驳回则变成2,归档后变成3。状态统一之后,前端可以复用同一套流程标签组件,后端也只需要约定几个常量,开发效率提升非常明显。

角色主要权限对应模块
管理员用户管理、角色分配、数据字典、系统日志系统管理
工作人员发布公告、发起申请、登记文件、维护日程行政事务
部门负责人审批各类申请、导出报表、查看统计审批中心

还有一个设计重点:审批链路不能写死。项目初期我只做了一层审批,结果需求方反馈有些流程要二级审批,有些只要登记备案不需要审核。后来我把流程类型抽成了一个配置字段,申请记录里保存当前步骤和流程标识,每次流转只处理“当前节点”该做的事。这个调整在数据库层面只是多了两列,却让系统适用范围扩大了不少,会议室预约可以是一级审批,用章申请可以变成两级审批,增加流程时不用改太多代码。

2. 技术选型与项目结构

2.1 后端为什么用SpringBoot+MyBatis

后端选择SpringBoot,看中的是它的低配置启动和内嵌Tomcat,一个jar包就能跑起来,部署时不需要单独装Tomcat。MyBatis则是因为我对SQL有很强的掌控欲,尤其在多表关联、条件动态拼接、分页统计这些场景,写XML比用JPA隐式生成的SQL要直观得多。项目里经常出现“按日期范围、申请类型、当前状态三个条件组合查询”的情况,MyBatis的动态SQL用if标签就能轻松处理,而JPA在这种场景下要么写复杂的Specification,要么回退到原生SQL,反而绕。

MyBatis还有一个优势是方便SQL调优。曾经有一个统计页面加载很慢,我用MyBatis打印出来的SQL去EXPLAIN,发现一张表缺索引,加完索引响应时间从两秒降到几十毫秒。这种排查过程在JPA自动生成SQL时就没有那么直观。如果项目里的查询比较简单,用Spring Data JPA也不是不行,但这套系统里有大量按状态筛选和关联统计需求,MyBatis显然更合适。

2.2 前端为什么用Vue + Element UI

前端选Vue,是因为它的组件化开发和渐进式引入方式很适合后台管理系统。Element UI提供了现成的表格、表单、弹窗、分页、日期选择器,后台页面长得都差不多,用组件拼装比从零写DOM高效太多。我在这套系统里把所有列表页抽象成一套通用模板:顶部搜索区、中间表格区、底部分页区、右侧操作按钮,再加一个表单弹窗。每个业务模块只需要提供字段配置,渲染逻辑基本复用。

请求层用Axios统一封装,我把POST、GET、上传、下载都包了一遍,所有请求自动带上token,后端返回非200状态码时统一弹错误提示。路由守卫也踩过坑,最初只在菜单上做了权限过滤,结果有人直接输入URL就能访问没权限的页面,后来在路由守卫里增加了动态权限判断,没有权限的路径统一跳转到403页面。这一套组合下来,新增一个模块的工作量大概只有后端的三分之一。

2.3 项目目录结构与分层规范

后端采用标准的Controller-Service-Mapper三层结构,额外加一层DTO用于接口参数传递,避免直接用实体类接收前端字段。Controller层只做参数接收和结果包装,Service层处理业务逻辑和事务边界,Mapper层负责SQL读写。这样的好处是职责单一,后续接单元测试也方便,替换实现不会影响上层接口。

office-admin ├── src/main/java/com/office/admin │ ├── controller # 接口层 │ ├── service # 业务层 │ ├── mapper # MyBatis Mapper接口 │ ├── entity # 数据库实体 │ ├── dto # 前端交互对象 │ ├── config # 安全、跨域、MyBatis配置 │ ├── common # 结果封装、常量、异常处理 │ └── utils # JWT、日期等工具类 ├── src/main/resources │ ├── mapper # Mapper XML │ ├── application.yml # 主配置 │ └── application-prod.yml └── sql └── office_admin.sql # 建库建表与初始化数据

前端目录也同样做了分层,src下面按照api、router、store、views、utils、layout这些职责划分。api层集中放接口定义,views每个模块一个文件夹,store只放用户信息和权限菜单这种全局状态,不把所有数据都塞到Vuex里,避免刷新后状态丢失。这个结构看起来简单,但维护起来非常舒服,新成员接手时能很快找到代码位置。

3. 数据库设计与核心业务实现

3.1 数据表设计与关键字段

数据库的命名我统一用sys_开头表示系统管理表,biz_开头表示业务表。系统管理部分是一套经典的RBAC模型:sys_user用户表、sys_role角色表、sys_user_role用户角色关联表、sys_menu菜单表、sys_role_menu角色菜单关联表。业务部分包括biz_notice公告表、biz_meeting_reservation会议室预约表、biz_seal_apply用章申请表、biz_car_apply公务用车申请表、biz_document文件收发登记表、biz_flow_record流程记录表。

每张表里都加了create_by、create_time、update_by、update_time、deleted这几个通用字段。deleted用逻辑删除而不是物理删除,是因为行政记录经常要追溯历史,物理删了数据就彻底没了。逻辑删除用0和1区分,所有查询默认过滤deleted=0,再由MyBatis拦截器统一处理,减少漏写的概率。

以用章申请表为例,核心字段包括申请人ID、申请事由、印章类型、使用日期、文件份数、当前审批节点、状态、备注。审批节点current_node很重要,因为允许二级审批后,同一个表里要记录当前轮到谁审,前端才能正确显示按钮。流程记录表biz_flow_record则把每一次审批动作都保存下来,包括操作人、操作意见、操作结果、时间,这样即使状态被改错,也能从记录表里还原整个完整的历史链。

3.2 登录认证与权限控制这样实现

登录流程用的是JWT,而不是传统Session。原因很简单,前后端分离之后后端接口可能被多个客户端调用,Session在跨域和分布式环境下都不好处理。用户登录成功后,后端用一个密钥把用户ID、用户名、角色信息生成token返回,前端把它存到localStorage,后续每个请求在请求头里带上Authorization字段。后端有个拦截器统一从请求头解析token,解析成功后把用户信息放到ThreadLocal里,Controller和Service随时能拿到当前用户。

这里有一个非常重要的安全细节,密码不能用MD5直接存。MD5虽然快,但撞库太容易了,现在网上随便一查都有海量MD5彩虹表。项目里用BCrypt加密,每次校验的时候用BCrypt的check方法,同一个密码每次加密结果都不一样,数据库泄露了也不容易反向破解。JWT的密钥我放在配置文件里,部署时通过环境变量覆盖,绝对不写死在代码里。

权限校验除了登录拦截,还有一个按钮级权限问题。前端根据角色动态生成菜单,但后端接口也必须校验权限。管理员接口、审批接口、普通查询接口都加了权限标识,比如审批用章申请需要hasPermi('seal:approve')。即使有人绕过前端直接调接口,后端没有权限直接拒绝。这个思路虽然简单,但能挡住大多数越权问题。

3.3 会议室预约接口与前端配合实例

拿会议室预约这个模块说一下前后端怎么配合。前端预约页面包含会议室下拉框、日期、开始时间、结束时间、事由、参与人数,提交时向后端发送POST请求。后端Controller先把DTO校验一遍,再交给Service保存,同时检查这个会议室在当前时间段是否已经被占用。如果冲突,直接抛出业务异常,前端弹窗提示“该时间段已被预约”。

关键接口大致是这样:

@PostMapping("/reservation") public Result add(@RequestBody @Valid MeetingReservationDTO dto) { meetingReservationService.addReservation(dto); return Result.success(); }

Service里第一件事是查重,SQL用重叠条件判断:新预约开始时间小于已有预约结束时间,且新预约结束时间大于已有预约开始时间。这比单纯比较“开始时间是否重复”要准确得多。查重通过后再查一次会议室状态,然后插入预约记录,状态设为待审批。审批接口则校验当前操作人是否有权限,再把状态改成已通过或已驳回,同时写入一条流程记录。

前端这边,Axios封装后的调用大概长这样:

export function createReservation(data) { return request({ url: '/reservation', method: 'post', data }) }

roomOptions从会议室字典接口拉取,提交按钮loading状态防止重复点击,提交成功后刷新列表。列表页的审批按钮根据当前状态动态渲染:状态为待审批时显示通过和驳回,已通过时显示归档,已驳回时不显示可操作按钮。这样的交互逻辑看着不复杂,但真正做好状态和按钮的联动,需要前后端一起约定枚举值和字段名,否则很容易出现类型对不上、状态差一位的bug。

4. 完整部署流程与上线配置

4.1 环境准备与版本建议

部署之前,先把环境列清楚。后端需要JDK1.8或11、Maven3.6以上、MySQL5.7以上,前端需要Node.js 14或16,生产服务器一般还要装Nginx。如果你用的是云服务器,建议先开一台Linux机器,CentOS或者Ubuntu都行,内存至少2G,因为后端jar包加MySQL容易把1G内存吃满。

依赖版本建议备注
JDK1.8 / 11推荐使用供应商的OpenJDK发行版
Maven3.6+打包后端jar
Node.js14 / 16构建前端资源
MySQL5.7 / 8.0数据库使用utf8mb4
Nginx1.18+静态资源与反向代理

数据库初始化我习惯用命令行导入。把项目里的sql/office_admin.sql上传到服务器,然后执行source命令。建库语句里要指定utf8mb4字符集,默认排序规则用utf8mb4_general_ci或者utf8mb4_unicode_ci都可以。初始化数据除了表结构之外,我还会塞一个默认管理员账号和基础菜单,方便部署完成后直接登录验证。

4.2 后端打包与启动

后端在打包之前要改两个配置:数据库连接地址和JWT密钥。本地开发连localhost,服务器上要改成实际数据库地址。如果MySQL和jar包在同一台服务器,用127.0.0.1就行。JWT密钥需要设一个足够复杂的随机字符串,长度为32字节以上,别用默认值上线。application-prod.yml里可以用环境变量引用的方式来配置,例如:

spring: datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: ${DB_USER} password: ${DB_PASSWORD}

打包命令很简单,在项目根目录执行:

mvn clean package -DskipTests

如果Maven下载依赖很慢,可以配置国内镜像,这个属于常规加速手段。打包完成后target目录下会生成office-admin.jar,用nohup命令后台启动:

nohup java -jar office-admin.jar --spring.profiles.active=prod > app.log 2>&1 &

启动之后不要急着测接口,先看日志。日志出现Started Application并且没有报错,再用curl测一下健康检查接口,比如curl http://127.0.0.1:8080/api/health。返回正常之后再接Nginx。

4.3 前端构建与Nginx配置

前端构建项目根目录下执行:

npm install npm run build

构建完成后生成dist目录,把dist目录整个上传到服务器,比如放在/opt/office/dist。Nginx配置的核心是两点:一是静态资源指向dist目录,二是把API请求反向代理到后端8080端口。如果前端路由用history模式,还要加上try_files配置,否则刷新页面会出现404。

一个可以参考的server块配置如下:

server { listen 80; server_name your-domain-or-ip; root /opt/office/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; } }

修改完配置用nginx -t检查语法,然后nginx -s reload生效。这里要特别注意proxy_pass后面有没有斜杠。如果写成proxy_pass http://127.0.0.1:8080;,请求路径会原样转发,比如/api/login会到后端的/api/login,这是对的。如果写成http://127.0.0.1:8080/;,/api/前缀会被去掉,变成/login,前后端对不上就会404。这个细节我踩过一次,花了不少时间才定位。

4.4 上线自检清单

部署完成之后,我建议按下面的顺序做一遍自检,避免遗漏关键环节。

  • 检查防火墙是否放行80端口,云服务器安全组也要同步配置。
  • 后端日志无异常,数据库连接正常。
  • 浏览器访问域名或IP,能看到登录页。
  • 使用默认管理员账号登录,验证菜单是否正常加载。
  • 发起一条用章申请,走一遍审批流程,确认状态流转正确。
  • 刷新前端页面,确认不会出现404。
  • 修改默认管理员密码,并确认JWT密钥已经替换。
  • 验证上传文件的目录是否有写权限,如果系统包含文件上传功能。

这八项如果全部通过,基本可以宣布上线。线上运行之后还会遇到各种零碎问题,比如定时任务没触发、日志把磁盘写满、数据库连接池爆掉,这些问题在下一条会专门展开。

5. 常见问题与排查技巧实录

5.1 跨域与代理配置

开发环境下前端跑在8080,后端跑在8081,端口不一样必然产生跨域。解决办法有两种:一种是在后端配置CORS,另一种是在前端开发服务器配置代理。我用的是代理方式,因为CORS在生产环境通常用不上,Nginx已经做了同源代理。前端在vite.config.js或vue.config.js里配置:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

这样前端请求/api/login时,开发服务器会把它转发到后端,浏览器看到的请求地址还是前端自己的地址,不会触发跨域。生产环境只要按照上一节的Nginx配置反向代理,跨域问题也自然解决。有时候后端接口已经通了但前端报跨域,要优先检查是不是代理配置没生效,特别是改了配置之后忘了重启前端开发服务。

5.2 MySQL连接时区与中文乱码

MySQL经常出现的两个恶心问题都是连接串参数引起的。第一个是时区问题,报错说The server time zone value,解决办法是在数据库连接URL后面加serverTimezone=Asia/Shanghai。第二个是中文乱码,页面提交的中文保存到数据库变成问号,八成是连接URL少了characterEncoding=utf8,或者建表时字符集不是utf8mb4。我的统一连接串是:

jdbc:mysql://127.0.0.1:3306/office_admin?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false

注意useSSL参数也要显式写清楚,MySQL8.0在某些环境下默认尝试SSL连接,没有配置证书会一直握手超时。另外建库的时候执行:

CREATE DATABASE office_admin DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

表里的varchar字段也尽量统一utf8mb4,这样表情符号、中文生僻字都能正常存,不会出现部分字符变成问号的情况。

5.3 页面刷新404与静态资源路径

前端用Vue Router的history模式后,直接访问http://域名/meeting/list会请求服务器上的真实路径,Nginx找不到这个文件就返回404。解决办法就是配置try_files,把所有非文件路径的请求都重写到index.html,由前端路由接管:

location / { try_files $uri $uri/ /index.html; }

配置完之后,刷新页面、收藏夹直接打开深层链接,都不会再404。还有一个相关问题是静态资源加载失败,如果页面能打开但js/css文件报404,多半是打包时的publicPath配置不对。资源部署在域名根路径时,publicPath设置为/,如果部署在子路径,比如/office/,就要设置成/office/。这个路径和Nginx的root、location要完全对应,否则页面白屏。

5.4 部署后接口返回异常排查顺序

我遇到过一种情况:本地联调正常,部署到服务器之后部分接口报500。遇到这种问题,先别急着改代码,按顺序做:第一步看后端日志,日志里一般会有异常堆栈;第二步确认数据库连接是否正常,特别是数据库地址是否写成了localhost但应用在容器里;第三步用Postman或curl直接调后端接口,绕过Nginx定位是哪一层出的问题;第四步如果接口偶尔超时,看数据库慢查询日志和连接池配置。

还有一个经验是上线前把MyBatis的SQL日志打开,生产环境调试完再关掉。在application-prod.yml里配置:

logging: level: com.office.admin.mapper: DEBUG

这样每个SQL都会打印出来,接口报错时能看到实际执行的SQL和参数,排查效率提升不少。等确认稳定了再把日志级别调回INFO,避免过多的日志刷盘影响性能。

6. 开发到上线的实战经验与扩展方向

6.1 几个容易翻车的细节

写这套系统的时候,我在几个地方栽过跟头,总结出来给后面的人提个醒。第一个是事务失效,只在Controller的方法上加@Transactional是没用的,Spring事务是通过代理对象生效的,必须放在Service层,而且同一个Service内部方法自调用也不会触发事务。整个申请流程里包含查重、插入预约记录、写入流程记录三个操作,必须放在一个事务里,任何一个失败都要整体回滚,否则会出现会议室占用但预约单没生成的情况。

第二个是MyBatis的实体字段映射不要依赖自动驼峰转换。数据库字段是create_time,实体类是createTime,MyBatis的mapUnderscoreToCamelCase开启后可以自动映射,但多表联查时如果有重名字段,一定要用resultMap显式映射。我一开始图省事,查询统计报表时发现某一个字段总是取不到值,后来才发现是两张表都有一列叫name,MyBatis不知道取哪个,换成带别名的resultMap就解决了。

第三个是前端按钮权限不能只靠v-if。虽然菜单和按钮都按角色渲染了,但后端接口必须再校验一遍。有人直接在浏览器控制台改掉localStorage里的角色,然后调审批接口,如果后端不加权限拦截就能越权操作。这个我做了双层校验之后才放心。

第四个是日期字段不要用字符串拼接。数据库时间字段统一用datetime或timestamp,Java侧用LocalDateTime,前端用日期组件处理。对接的时候传输用时间戳或标准格式字符串,后端解析成LocalDateTime,这样排序、比较、时区转换都不容易出错。用字符串存日期一开始看起来方便,后面做范围查询和报表统计会非常痛苦。

6.2 后续功能扩展建议

系统上线之后如果还想要继续完善,可以考虑几个方向。第一个是接入企业微信或钉钉机器人,审批通过或者驳回时自动发送通知,这个比邮件提醒更及时。实现上不需要改动现有业务表,只要在流程记录写入后发一条MQ消息或者异步HTTP请求就行。第二个是引入工作流引擎,比如Flowable或Activiti,把现在手写的状态机替换成真正的流程定义。如果流程经常要动态调整,工作流引擎会更省心,但这会让项目复杂度上升不少,小项目不一定划算。

第三个是增加数据导出功能,管理后台经常要把列表导出成Excel。集成EasyExcel,在Controller加一个导出接口,把查询结果分页流式写入,内存占用控制得比较好。第四个是文件存储,目前文件上传到本地磁盘,如果服务器磁盘不够或者要做负载均衡,需要换成对象存储或MinIO,前端上传时先用后端签名拿到直传地址,上传完成再通知后端记录文件信息。第五个是前端技术栈可以往Vue3和Vite上迁移,组件库可以继续用Element Plus,代码写法改动不算特别大,但构建速度和生态会好一些。

我个人在实际操作中的体会是,这种管理系统的难点从来不在某个高深的算法,而在于把状态流转、权限边界和部署细节想清楚。项目做完后,把部署脚本和常用命令也整理进文档,下次再接手类似系统会省掉很多沟通成本。如果你照着这篇流程走,最后卡在某个报错上,多半是环境变量或者路径问题,先看日志,再逐层排查,基本都能解决。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询