☰
SpringBoot+Vue实验室管理系统:从需求拆解到部署全流程实战
2026/9/24 21:42:59 网站建设 项目流程

实验室管理系统这个东西,我在毕业设计阶段接触过不少同学的版本,自己也完整带过几个项目。说实话,SpringBoot + Vue 的前后端分离架构,放在实验室管理这个场景里,属于非常典型的“管理系统类”毕业设计选题。它的好处在于:业务边界清晰、技术栈主流、可扩展性强,而且演示效果特别直观——一个管理员登录进去,能看到用户管理、实验室预约、设备借用、审批流程这些模块,整套东西做出来,答辩时展示起来非常有说服力。

这篇文章我打算从一个实际可落地、能跑通、能讲清楚的设计出发,把整个实验室管理系统从需求拆解、技术选型、数据库设计、后端接口实现、前端页面联调,到部署演示的完整链路都过一遍。重点放在那些文档里不太会写、但真正做项目时一定会遇到的细节上,比如预约冲突怎么处理、权限按钮怎么控制、文件上传的路径问题、跨域配置怎么避免踩坑等。如果你正在做类似的毕业设计,或者想快速搭一个实验室管理项目作为项目经验,这篇文章可以作为一份参考路线图来用。

1. 项目整体设计与技术选型思路

1.1 核心需求拆解:实验室管理到底要管什么

先别急着写代码,做任何管理系统,第一步都是把需求理清楚。实验室管理系统的核心用户角色通常有三种:管理员、教师、学生。管理员负责系统配置和全局管控,教师主要是申请使用实验室,学生则是预约实验设备或查看实验安排。

从功能模块上看,一个能拿得出手、答辩时有话可说的实验室管理系统,至少要包含下面这些模块:

  • 用户管理:用户的注册、登录、角色分配、账号启停用。这里要注意密码不能明文存储,虽然毕业设计不要求极高安全性,但用 MD5 加盐或者 BCrypt 加密是个加分项。
  • 实验室信息管理:维护实验室的基本信息,比如实验室名称、位置、可容纳人数、设备配置、开放时间等。这个模块是基础数据,后续所有申请和预约都要关联它。
  • 实验室预约管理:用户选择时间段提交预约申请,管理员审批。这里的核心难点是“冲突检测”——同一个实验室在同一时间段不能重复预约,这个逻辑必须做对,否则答辩时被问到就露怯了。
  • 设备管理:实验室里的设备台账信息,设备名称、编号、状态(正常/维修/报废)、借用记录等。设备借用最好也带上审批流,形成完整的业务闭环。
  • 公告管理:发布实验室相关通知、安全须知等。这个模块虽然简单,但能让整个系统看起来更完整,也有利于丰富首页展示内容。
  • 统计报表:实验室使用率、预约数量排行、设备借用频次等。用 ECharts 画几张图表,放在管理员首页,视觉效果很好,也显得工作量充实。

提示:选题时如果觉得功能太多做不完,可以砍掉一部分,比如设备管理简化成设备列表 + 借用记录,不做完整审批流。但“预约 + 审批”这个核心流程一定要保留,这是整个系统的灵魂。

1.2 为什么选 SpringBoot + Vue 前后端分离

现在做管理系统类毕业设计,SpringBoot + Vue 前后端分离基本是主流。原因很实际:

从后端角度讲,SpringBoot 让 Java 后端开发的门槛降低了很多。不需要像 SSM 时代那样写一大堆 XML 配置,Maven 引入依赖、写几个注解、配一下数据源,一个能跑起来的 REST API 服务就出来了。Spring Data JPA 或者 MyBatis-Plus 做数据库操作也很方便,尤其是 MyBatis-Plus,单表 CRUD 几乎不需要手写 SQL,对于毕业设计来说效率极高。

从前端角度讲,Vue 的渐进式框架特性特别适合这种中后台管理系统。配合 Element UI(或 Element Plus)组件库,表格、表单、弹窗、分页这些常见界面元素都有现成组件,不用从零写 CSS,页面做出来也比较工整。再加上 Vue Router 管理页面路由、Axios 发送 HTTP 请求,整个前后端交互链路清晰、资料丰富,遇到问题搜索一下基本都能解决。

前后端分离还有一个现实的好处:分工明确。如果你是和同学组队做,一个人负责后端接口开发,一个人负责前端页面,双方只需要约定好接口文档就能并行推进,效率比传统 JSP 单体项目高得多。而且答辩的时候,你可以同时打开后端 Swagger 接口文档管理和前端页面,展示效果非常有层次感。

1.3 技术栈选型和版本踩坑记录

这里整理一份我在实际开发中验证过的技术栈版本组合。为什么单独强调版本?因为 SpringBoot 版本太高或者太低,都会带来一堆意想不到的问题,比如依赖冲突、API 变更、启动报错等。热门搜索词里“springboot版本太高”说明这正是新手高频踩坑点。

技术/组件推荐版本备注
JDK1.8 或 11不要轻易上 17,部分旧依赖可能不兼容
SpringBoot2.7.x稳定且资料最多,避免用 3.x 那种太新的版本
MyBatis-Plus3.5.x配合 SpringBoot 2.x 使用,CRUD 效率极高
MySQL5.7 或 8.08.0 注意驱动配置 com.mysql.cj.jdbc.Driver
Vue2.x 或 3.x如果熟练就上 Vue3 + Element Plus,否则 Vue2 + Element UI 更稳
Node.js14/16/18太新的 Node 可能和旧版 Vue CLI 冲突
Maven3.6+3.8+ 有时会出现镜像源问题

注意:如果使用 SpringBoot 3.x,需要 JDK 17+,同时很多第三方 starter 的兼容性需要重新确认。对于毕业设计来说,没必要追求最新,稳定压倒一切。我见过太多同学在答辩前一周还在和版本兼容问题搏斗,真的没有必要。

2. 数据库设计与核心表结构实现

2.1 数据表规划的底层逻辑

数据库设计决定了整个系统的上限。表设计得好,后面写业务代码就是顺理成章;表设计得乱,后面每写一个功能都要返工。

我的建议是:先把实体关系理清楚,再建表。实验室管理系统的核心实体有:用户(User)、实验室(Lab)、设备(Equipment)、预约记录(Reservation)、借用记录(BorrowRecord)、公告(Notice)。实体之间的关系大概是:

  • 用户与预约记录:一对多。一个用户可以有多个预约记录。
  • 实验室与预约记录:一对多。一个实验室在某时间段被预约,预约记录指向一个实验室。
  • 用户与设备借用记录:一对多。
  • 设备与借用记录:一对多。

基于这个关系,表结构就比较容易推导出来了。

2.2 核心表格结构参考

这里给出最关键的几张表的设计,你可以直接照着建。

用户表 t_user

字段名类型说明
idbigint主键自增
usernamevarchar(50)登录用户名,唯一
passwordvarchar(100)密码(BCrypt加密后存储)
real_namevarchar(50)真实姓名
rolevarchar(20)角色:admin/teacher/student
phonevarchar(20)联系电话
emailvarchar(100)邮箱
statustinyint状态:0禁用 1启用
create_timedatetime创建时间

实验室表 t_lab

字段名类型说明
idbigint主键
lab_namevarchar(100)实验室名称
locationvarchar(200)所在位置
capacityint可容纳人数
equipment_infovarchar(500)设备配置说明
open_timevarchar(100)开放时间说明
statustinyint状态:0停用 1启用
remarkvarchar(500)备注

预约记录表 t_reservation

字段名类型说明
idbigint主键
user_idbigint预约用户ID
lab_idbigint预约实验室ID
reserve_datedate预约日期
start_timetime开始时间段
end_timetime结束时间段
purposevarchar(500)预约用途
statustinyint状态:0待审批 1已通过 2已拒绝 3已取消
audit_userbigint审批人ID
audit_timedatetime审批时间
audit_remarkvarchar(500)审批意见
create_timedatetime创建时间

设备表 t_equipment

字段名类型说明
idbigint主键
equipment_namevarchar(100)设备名称
lab_idbigint所属实验室ID
equipment_novarchar(100)设备编号
statustinyint状态:0正常 1维修中 2已报废
buy_datedate购买日期
remarkvarchar(500)备注

小技巧:status字段用数字而不是字符串,处理起来方便,前端可以通过数字映射显示文字标签。比如预约记录的状态,0 显示“待审批”、1 显示“已通过”、2 显示“已拒绝”、3 显示“已取消”,用 Element UI 的 el-tag 组件配合 type 属性展示不同颜色,效果非常直观。

2.3 数据库设计的几个关键注意点

第一,外键要不要建。我的建议是物理外键可以不建,但在逻辑上通过业务代码保证关联关系。这样做的好处是:导入导出数据方便、删除数据时不会被外键约束卡住、后续做分库分表时更容易扩展。实际开发中很多项目也是这样做的——用逻辑外键替代物理外键。

第二,时间字段统一用 datetime,不要有的用 date、有的用 varchar 存字符串。预约日期和开始/结束时间要区分清楚:日期是某一天,时间则是当天的时间段。如果把日期和时间混在一起存成一个字符串,后面做冲突检测时特别麻烦。

第三,预留一个 create_time 字段。几乎每张业务表都应该有创建时间,这不仅是规范问题,也是后面做统计排序的抓手。比如查询最新预约记录,直接按 create_time desc 排序即可。

3. 后端核心功能实现与关键代码

3.1 项目初始化与分层结构

后端项目用 Spring Initializr(或 IDEA 内置的 Spring Boot 创建向导)生成即可。包结构建议如下:

com.example.lab ├── controller # 控制层,接收前端请求 ├── service # 业务层,核心逻辑 ├── mapper # 数据访问层,继承 BaseMapper ├── entity # 实体类 ├── dto # 数据传输对象(请求/响应) ├── config # 配置类(跨域、拦截器等) ├── common # 通用类(统一返回结果、异常处理等)

这是一个非常标准的分层结构,答辩时被问到“为什么这样分层”,可以回答说:Controller 只负责参数接收和结果返回,Service 层处理业务逻辑,Mapper 层负责数据库交互,这样各层职责清晰,便于维护和测试。

统一返回结果类是前后端分离项目必不可少的。前端通过 axios 接收到后端返回的数据,必须要有固定的结构,前端才好统一处理。一般格式是:

public class Result<T> { private Integer code; // 状态码,200成功,500失败 private String message; // 提示信息 private T data; // 具体数据 // 静态工厂方法 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

3.2 登录认证与拦截器实现

实验室管理系统的登录认证,毕业设计阶段不一定要上 Spring Security + JWT 那套完整方案,但建议至少使用JWT(JSON Web Token)+ 拦截器的方式实现简单的 token 鉴权。这样既比 session 方案更符合前后端分离的风格,又不会像 Spring Security 那样配置复杂。

核心思路:用户登录成功后,后端生成一个 JWT token 返回给前端;前端把 token 存储在 localStorage 或 sessionStorage 中;之后每次请求都在请求头里带上Authorization: token;后端写一个拦截器,对需要认证的接口检查 token 是否有效。

@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 String uri = request.getRequestURI(); if (uri.contains("/login") || uri.contains("/register")) { return true; } String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { // 没有token,返回401 response.setStatus(401); return false; } // 校验token try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { response.setStatus(401); return false; } } }

注意:拦截器配置中要放行登录、注册等公开接口,以及静态资源。如果忘记放行swagger接口,调试时会一直报 401,很影响心情。另外,token 过期时间建议设置 2 小时,用户操作时间长了被踢出来需要重新登录,这个体验问题可以在前端做一个统一的响应拦截器,检测到 401 时跳转回登录页。

3.3 预约冲突检测:核心业务逻辑

预约冲突检测是整个系统最容易出错、也最值得详细讲解的地方。场景是:用户提交一个预约申请(某实验室在某天某时间段),系统要判断这个时间段里该实验室是否已被占用。

我建议的设计思路:在 Service 层做一次查询验证。查询条件为:

  • lab_id= 当前申请实验室ID
  • reserve_date= 当前申请日期
  • status!= 已拒绝 且状态不是已取消(也就是说,待审批和已通过的预约都算占用)
  • 时间区间存在交集

时间区间交集判断逻辑:

start_time < 新结束时间且end_time > 新开始时间,如果成立,则说明时间段有重叠。

public Result<?> addReservation(ReservationDTO dto) { // 校验参数 if (dto.getStartTime().isAfter(dto.getEndTime())) { return Result.error("开始时间不能晚于结束时间"); } // 冲突检测 LambdaQueryWrapper<Reservation> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Reservation::getLabId, dto.getLabId()) .eq(Reservation::getReserveDate, dto.getReserveDate()) .in(Reservation::getStatus, 0, 1) // 待审批和已通过都算占用 .lt(Reservation::getStartTime, dto.getEndTime()) .gt(Reservation::getEndTime, dto.getStartTime()); Long count = reservationMapper.selectCount(wrapper); if (count > 0) { return Result.error("该时间段实验室已被预约,请选择其他时间"); } // 保存预约 Reservation reservation = new Reservation(); BeanUtils.copyProperties(dto, reservation); reservation.setUserId(当前登录用户ID); reservation.setStatus(0); // 待审批 reservation.setCreateTime(LocalDateTime.now()); reservationMapper.insert(reservation); return Result.success(null); }

这段代码逻辑不复杂,但却是系统的关键点。答辩时如果能主动讲清楚“如何通过时间区间交集算法防止重复预约”,并且说出“待审批的预约也要锁定时间”这个细节,老师会觉得你是真的思考过业务逻辑,而不是只会调用 CRUD。

3.4 审批流程与状态流转

预约提交后,管理员登录系统,能看到所有待审批的预约记录。审批操作其实就是更新预约记录的status字段:通过则改成 1,拒绝则改成 2,同时填写审批意见。这里有一个业务细节:审批通过前,该时间段已经被锁定,因为前面冲突检测时把待审批状态也视为了占用,这样就避免了管理员同时通过两个时间冲突的申请。

状态流转图(文字描述):

待审批(0) -> 已通过(1) 待审批(0) -> 已拒绝(2) 待审批(0) -> 已取消(3)【用户主动取消】 已通过(1) -> 已取消(3)【必要时管理员可取消】

关于“取消预约”这个功能,建议做用户权限控制:用户只能取消自己发起的、且状态为待审批或已通过的预约。已拒绝的预约理论上可以取消但没意义,前端可以直接隐藏取消按钮。

public Result<?> cancelReservation(Long id, Long currentUserId) { Reservation reservation = reservationMapper.selectById(id); if (reservation == null) { return Result.error("预约记录不存在"); } if (!reservation.getUserId().equals(currentUserId)) { return Result.error("只能取消自己的预约"); } if (reservation.getStatus() != 0 && reservation.getStatus() != 1) { return Result.error("当前状态不可取消"); } reservation.setStatus(3); reservationMapper.updateById(reservation); return Result.success(null); }

这个小功能虽然简单,但体现了“权限控制 + 状态机”的思想,属于面试八股文里常考的“状态流转”场景,写在简历的项目经验里是很加分的。

3.5 MyBatis-Plus 的 CRUD 与分页

MyBatis-Plus 最让人省心的就是内置了通用 CRUD 方法。实体类继承BaseMapper<T>后,selectById、selectList、insert、updateById、deleteById这些方法开箱即用。分页查询要用分页插件,配置如下:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

查询预约列表时,分页 + 多条件查询的写法:

public Result<?> pageReservations(int pageNum, int pageSize, Long labId, Integer status) { Page<Reservation> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Reservation> wrapper = new LambdaQueryWrapper<>(); // 条件拼接,不为空才拼接 if (labId != null) { wrapper.eq(Reservation::getLabId, labId); } if (status != null) { wrapper.eq(Reservation::getStatus, status); } // 按创建时间倒序 wrapper.orderByDesc(Reservation::getCreateTime); Page<Reservation> result = reservationMapper.selectPage(page, wrapper); return Result.success(result); }

注意:分页返回的数据结构里包含records、total、size、current等字段,前端拿到后直接把records渲染到表格,把total传给分页组件的 total 属性即可。不要自己手动拼一个 Map 去返回,MyBatis-Plus 内置的 Page 对象就是标准结构,前端处理起来最顺手。

4. 前端 Vue 项目搭建与页面实现

4.1 环境准备:Node、Vue CLI 与依赖安装

前端部分的环境准备,是很多新手第一个卡住的地方。如果你用的是 Vue 2,先安装 Node.js 14 或 16 版本。安装完成后,确认 npm 可用:

node -v npm -v

然后安装 Vue CLI 脚手架工具:

npm install -g @vue/cli@4.5.15

注意:Vue CLI 的版本和 Node 版本有一定对应关系,Node 版本太高会导致node-sass编译报错。网上搜索“vue安装及环境配置”的帖子很多,但大部分都没强调版本匹配问题。我的建议是:如果不想折腾,直接放弃node-sass,改用sass(Dart Sass)作为编译库,兼容性更好。

创建项目:

vue create lab-web

选择默认的 Vue 2 配置,进入项目目录后安装依赖:

cd lab-web npm install axios element-ui vue-router@3 echarts

注意:Vue 2 对应的是vue-router@3,如果你装了默认版本可能直接是 v4,那是给 Vue 3 用的,会导致路由无法使用。这种版本坑在毕业设计中非常常见。

4.2 Axios 封装与请求拦截

前后端分离项目中,axios 封装是前端必须做好的基础设施。所有请求统一走封装好的实例,统一处理 token 和错误码。

// src/utils/request.js import axios from 'axios'; import { Message } from 'element-ui'; import router from '../router'; const request = axios.create({ baseURL: '/api', // 这里配合后端跨域配置或代理配置 timeout: 10000 }); // 请求拦截器:每次请求自动携带 token request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = token; } return config; }); // 响应拦截器:统一处理返回码 request.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { Message.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res; }, error => { if (error.response && error.response.status === 401) { // 登录过期,清除token并跳转登录页 localStorage.removeItem('token'); router.push('/login'); Message.error('登录已过期,请重新登录'); } else { Message.error('网络请求异常'); } return Promise.reject(error); } ); export default request;

4.3 登录页面与动态路由

登录页面逻辑比较简单,就是收集表单数据,调用后端登录接口。关键是登录成功后要做几件事:

  • 把返回的 token 存到 localStorage
  • 把用户信息存到 Vuex(或 pinia,Vue3 用)
  • 根据角色跳转到不同首页
  • 动态生成路由菜单(如果做了权限菜单的话)

权限菜单这块稍微复杂一点。简单做法是:前端根据用户角色字段,用v-if判断是否显示某些菜单项。更规范的做法是后端返回该用户可访问的菜单列表,前端动态注册路由。建议毕业设计阶段用简单方案即可,但可以在答辩时提一下“更完善的方案是动态路由”。

4.4 核心页面:实验室预约页面

预约页面是展示系统业务能力的重要窗口。页面上有三块核心内容:

  • 选择实验室的卡片或下拉列表
  • 选择日期和时间段
  • 填写预约用途,提交

时间段的处理建议用 Element UI 的el-time-select组件,可以配置固定的时间步长,比如每 1 小时一个选项。这样用户就不用手动输入时间了,后端校验也方便。

<el-form-item label="开始时间" prop="startTime"> <el-time-select v-model="form.startTime" :picker-options="{ start: '08:00', step: '01:00', end: '21:00' }" placeholder="选择开始时间"> </el-time-select> </el-form-item> <el-form-item label="结束时间" prop="endTime"> <el-time-select v-model="form.endTime" :picker-options="{ start: '08:00', step: '01:00', end: '22:00' }" placeholder="选择结束时间"> </el-time-select> </el-form-item>

提交后提示成功,然后跳转到“我的预约”页面。这个页面展示当前登录用户的历史预约记录,状态用 el-tag 展示不同颜色:

<el-tag v-if="scope.row.status === 0" type="warning">待审批</el-tag> <el-tag v-else-if="scope.row.status === 1" type="success">已通过</el-tag> <el-tag v-else-if="scope.row.status === 2" type="danger">已拒绝</el-tag> <el-tag v-else type="info">已取消</el-tag>

4.5 首页数据看板:ECharts 统计图

管理员的首页是整个系统的“门面”,建议放一张欢迎卡片 + 几个统计图表。ECharts 在 Vue 项目里使用方式很简单:

npm install echarts

然后在组件中创建图表实例:

import * as echarts from 'echarts'; export default { mounted() { this.initChart(); }, methods: { initChart() { const chart = echarts.init(this.$refs.chartRef); chart.setOption({ title: { text: '实验室使用率TOP5' }, tooltip: {}, xAxis: { type: 'category', data: ['实验室A', '实验室B', '实验室C', '实验室D', '实验室E'] }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: [85, 72, 68, 55, 43], itemStyle: { color: '#409EFF' } }] }); } } };

统计数据的接口可以后补一个,比如查询各实验室的预约次数,按数量排序返回 Top5。数据库有预约记录表,这个统计写起来很快。

5. 前后端联调与跨域问题处理

5.1 联调时最坑的跨域问题

前端跑在http://localhost:8080,后端跑在http://localhost:8081,端口不同,浏览器就会触发跨域限制。解决跨域的方法有很多种,常见的有:后端添加 CORS 配置、前端配置 devServer 代理、Nginx 反向代理。

推荐方案是配置前端代理。在vue.config.js中:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true, pathRewrite: { '^/api': '' } } } } };

这样前端请求/api/login时,开发服务器会把请求转发到http://localhost:8081/login。你可能会问,为什么后端接口没有/api前缀?这里是通过 pathRewrite 把/api前缀去掉后转发的,实际请求的是后端的根路径接口。

如果不想用代理,也可以在后端配置 CORS:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

注意:跨域配置方式要看你用的是哪种。如果前端已经通过代理转发,后端就不需要再开 CORS;如果前端直接请求后端地址(比如http://localhost:8081/api),后端就必须配置 CORS。两个都用可能也不会出什么大问题,但有时候会出现 “preflight 请求返回 401” 这种坑,所以建议二选一。

5.2 后端接口测试工具

联调之前,建议先用接口测试工具把后端接口全部跑通。常用的有 Postman 和 Apifox。我个人比较推荐 Apifox,因为它是中文界面,支持接口文档自动生成和 mock 数据,对毕设来说非常友好。

逐个接口调试时,重点关注几点:

  • 参数校验:缺参数或类型错误时,后端有没有返回友好提示?
  • 异常处理:系统抛异常时,返回的是不是统一的 Result 结构?
  • 权限校验:未登录或登录过期的请求,是不是正确拦截?

5.3 前后端联调的流程建议

建议按照以下顺序联调:

  1. 先调登录接口。确保用户能登录成功并拿到 token。
  2. 再调用户信息查询接口。确保 token 鉴权生效。
  3. 然后调实验室列表接口。前端页面能看到实验室数据,说明基础链路通了。
  4. 接着调预约提交和冲突检测接口。这才是核心流程。
  5. 最后调审批流程和统计图表接口。

每调通一个功能,就在前端点击操作一遍,确认页面交互正常。这个顺序能保证每一步的问题都能快速定位,不会出现“不知道是前端问题还是后端问题”的困境。

6. 部署与演示准备

6.1 打包前端项目

部署前要先把前端打包成静态文件。在项目根目录执行:

npm run build

执行完成后,项目目录下会出现dist文件夹,里面的index.html和static目录(Vue CLI 默认配置下是js、css等文件夹)就是打包好的前端资源。

注意:本地接口联调时,前端请求路径是/api,依赖开发服务器的代理。打包后没有了 devServer 代理,需要把baseURL改成线上后端的真实地址,或者用 Nginx 配置反向代理。这是最容易踩的部署坑,很多同学本地运行一切正常,打包后所有接口都请求失败,就是这里忘了改。

6.2 后端打包与运行

后端项目用 Maven 打包:

mvn clean package -DskipTests

生成的 jar 包在target目录下:

java -jar lab-management-0.0.1-SNAPSHOT.jar

后端启动后,默认端口是 8081(需在application.yml中配置)。启动时如果提示端口被占用:

# 查看端口占用 netstat -ano | findstr 8081 # 或者用 lsof 命令 lsof -i:8081

6.3 演示数据准备

答辩演示前,建议提前往数据库里造一批“真实感强”的数据。比如:

  • 5 个实验室,名称、位置、容量各不相同
  • 20 个用户,覆盖管理员、教师、学生三种角色
  • 30 条预约记录,覆盖不同状态、不同日期、不同实验室
  • 10 台设备,部分状态是维修中

有了这些数据,演示的时候切后台能直接看到图表和列表效果,而不是空荡荡的页面。这也是答辩细节里的加分项。

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

7.1 前端 npm install 卡住或报错

这是一个出现频率极高的问题。解决办法:

  • 使用国内 npm 镜像:npm config set registry https://registry.npmmirror.com
  • 删除node_modules和package-lock.json后重新安装
  • 不要用 cnpm 安装某些依赖,容易出现奇怪的符号链接问题

7.2 启动 SpringBoot 项目后端口被占用

本地开发时,Idea 里旧实例没有完全停掉,再次启动就会报端口被占用。先把之前的控制台进程停掉,或者改application.yml里的server.port。如果确实有进程占用,找到进程号杀掉即可。

7.3 数据库连接报错

常见的几种情况:

  • MySQL 驱动版本和 MySQL 版本不匹配。
  • application.yml中数据库名写错。
  • 时区配置问题:连接串后面的serverTimezone=Asia/Shanghai不要漏掉。
  • MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,MySQL 5.7 是com.mysql.jdbc.Driver,这个别用混。
spring: datasource: url: jdbc:mysql://localhost:3306/lab_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver

7.4 前端页面能打开但数据空白

这种情况先打开浏览器开发者工具(F12),查看 Console 和 Network 面板。

  • 如果请求显示 404,说明接口路径不一致,检查后端 Controller 的@RequestMapping路径和前端请求路径。
  • 如果请求显示 CORS error,回到跨域配置部分排查。
  • 如果请求返回 401,检查 token 是否成功传递,以及后端拦截器是否放行了对应路径。
  • 如果请求返回 200 但数据为空,检查Result返回结构里code字段是否为 200,以及字段名是否和前端解析的一致。

7.5 预约时间段冲突检测漏掉边界情况

比如一个用户预约 10:00-12:00,另一个用户预约 12:00-14:00,这两个时间在边界上是相接的,不算冲突。用start_time < 新结束时间 AND end_time > 新开始时间判断,12:00 与 12:00 相等时不满足<条件,不视为冲突,这个逻辑是正确的。

但如果预约时间是 10:00-12:00 和 11:00-13:00,这两个明显有交集,会被正确识别为冲突。边界条件建议在答辩前专门测试一下,把你设计的测试用例和结果整理出来。

7.6 上传功能(如附件)后的路径问题

部分实验室预约可能需要上传实验方案附件。文件上传成功后,后端返回的通常是文件的存储路径,比如/upload/xxx.pdf。前端在显示下载链接时,不能直接拼接前端地址,要拼接后端访问地址。建议后端写一个文件访问映射配置,把本地存储目录映射为静态资源路径。

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/upload/"); } }

7.7 “SpringBoot 版本太高”引发的连锁报错

这是网上出现频率特别高的搜索词。很多同学创建项目时顺手选了最新版 SpringBoot 3.x,结果发现:JDK 17 没装、MyBatis-Plus 的 starter 不兼容、javax包变成了jakarta。最稳妥的办法就是创建项目时选择 SpringBoot 2.7.x。

8. 体验优化与细节打磨

8.1 表单校验与友好提示

前端表单校验是最容易出彩也最容易忽略的地方。比如预约日期不能选过去的时间,结束时间必须晚于开始时间,用途不能为空等。Element UI 的表单校验规则写起来很快,但效果非常直观。

rules: { labId: [{ required: true, message: '请选择实验室', trigger: 'change' }], reserveDate: [{ required: true, message: '请选择日期', trigger: 'change' }], startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }], endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }] }

8.2 按钮级权限控制

除了页面级权限,按钮级权限更能体现系统的精细度。比如普通学生身份登录后,设备借用按钮应该隐藏,管理员才显示。最简单的实现是自定义一个指令,根据用户角色来控制按钮显隐。

// 全局自定义指令 v-permission Vue.directive('permission', { inserted(el, binding) { const role = localStorage.getItem('role'); const requiredRole = binding.value; if (role !== requiredRole) { el.parentNode && el.parentNode.removeChild(el); } } })

使用方式:

<el-button v-permission="'admin'" @click="handleAudit">审批</el-button>

提示:这种纯前端控制只是体验优化,不是真正的安全控制。真正安全的权限必须后端也做。答辩时如果老师问“前端隐藏了按钮但如果手动调接口怎么办”,你可以回答“后端同样有权限校验”,把后端接口上的@RequireRole之类的角色判断逻辑展示出来,这就是一个很扎实的交流点。

8.3 空数据展示

页面没有数据时,表格中间一片空白很影响观感。Element UI 的 el-table 支持自定义空数据状态:

<el-table :data="tableData"> <!-- 列定义 --> <template slot="empty"> <el-empty description="暂无数据"></el-empty> </template> </el-table>

首屏加载时加一个 loading 状态:

<el-table v-loading="loading" :data="tableData">

这些小小的体验细节加在一起,能让整个系统看起来完成度更高。

9. 扩展思路与答辩加分项

9.1 从毕业设计到项目经验

如果你的目标是春招或秋招时把实验室管理系统写进简历,建议在基础功能之上打磨几个亮点:

  • 高性能方向:给预约查询接口加 Redis 缓存,减少数据库压力。
  • 消息通知方向:预约审批通过后,发一封邮件或站内信通知用户,用 Spring 事件机制实现解耦。
  • 工作流方向:把审批流程改成 Flowable 或 Activiti 工作流引擎,适合对工作流感兴趣的岗位方向。
  • 监控方向:集成 SpringBoot Admin 监控后端服务状态,面试时可以聊线上问题排查。

9.2 论文与文档准备

毕业设计不只是写代码,论文也是大头。实验室管理系统的技术路线、需求分析、系统设计、功能实现、测试验证这几个章节,其实在开发过程中就应该同步整理。每写一个模块,顺手截图保存页面效果和接口测试结果;每条数据表设计,记录字段含义和设计理由。到写论文时,素材都现成,不用临时补。

9.3 演示注意事项

答辩演示时,建议提前准备好几个典型操作场景:

  • 新用户注册并登录。
  • 学生预约实验室,时间冲突时收到提示。
  • 管理员审批预约,查看统计图表。
  • 设备借用流程。

每个场景 3 分钟内能完整走一遍,整个演示控制在 10 分钟左右,既充分展示系统功能,又不会让现场节奏拖沓。演示数据里一定要有一两条看起来“很真实”的预约记录,比如“人工智能实验课预约人工智能实验室 19:00-21:00”,比用测试数据效果好得多。

做这类管理系统项目,我的体会是:真正的难点从来不是某个技术突破,而是把业务需求梳理清楚、把边界条件考虑周全、把细节体验打磨到位。预约冲突检测多写一个边界校验,审批状态流转多考虑一种情况,页面加载失败给用户一个友好提示——这些才是一个项目从“能跑”到“像样”的关键。实验室管理系统恰好是练习这种思维的好载体,流程完整、需求清晰、技术栈主流,做完之后你掌握的不仅是 SpringBoot 和 Vue 的基本用法,更是从零搭建一个完整应用的全局视角。照着这篇思路一步步走下来,你的项目一定能做得踏实、讲得清楚。

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

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

立即咨询