高校实验室管理系统,听起来像是一张"预约登记表"的电子化,真正接手过一个完整项目之后才知道,它要解决的不只是预约,而是实验室资源在校内被不同角色反复申请、分配、使用、维护的一整套流程。我最近在处理一个基于SpringBoot + 微信小程序 + Vue的前后端分离高校实验室管理系统项目,源码、文档、调试配置都齐全,但真正把这些东西吃透并稳定跑起来,还是花了不少功夫。这篇文章不打算铺开讲每个页面怎么画,而是把我认为最重要的六件事讲清楚:业务范围怎么拆、技术栈为什么这么选、数据库怎么建表才能避免预约冲突、后端接口的状态机怎么设计、小程序端和Vue管理端各自要注意什么,以及联调和部署阶段会遇到哪些坑。如果你手里也有一份类似的项目源码,或者正准备从零手写一个,这篇内容可以作为对照清单。
1. 拿到项目先别急着敲代码:先把实验室管理的业务全貌拆清楚
1.1 四条业务主线,预约只是其中一条
这个系统最常见的触发场景是:学生要做实验,必须先申请某间实验室的某个时间段;老师或管理员收到申请后要审批;实验室里的设备、耗材需要登记;实验课程的固定排课又和随机预约抢占同一批资源。把这些需求摊开之后,系统实际由四条业务主线组成,我在项目里是用一份需求清单逐条核对走下来的。
第一条是预约与审批。学生提交预约,内容包括实验室、日期、开始时间、结束时间、用途说明;老师或管理员审批,通过后学生才可以使用。很多半成品系统只有"已提交"和"已通过"两个状态,导致学生提交后不知道到底有没有被受理,老师也不知道哪些申请还没处理,这个"待审批"的中间态是必须要有的。
第二条是实验室资源台账。每间实验室有名称、位置、容纳人数、开放时段、设备清单、当前状态。实验室表是预约功能的基础,预约时必须校验实验室状态是"开放"而不是"维护中"。
第三条是设备与耗材管理。设备借用、维修状态、耗材库存领用,这部分和预约是弱关联,但管理员非常看重,年底盘点全靠这些记录撑着。
第四条是通知与课表。管理员发布实验通知,固定课表占用的时间段必须参与冲突判断,否则会出现"预约和上课撞车"的问题。早期需求里课表没有接入冲突逻辑,后来实际运行才发现,同一间实验室的固定上课时间和临时预约重叠,两边都很尴尬。
1.2 角色权限边界:四种角色,三张操作面
系统里通常有四类角色:学生、教师、实验管理员、系统管理员。它们看起来都是"登录后操作",但权限边界差异很大,我把它们整理成一张权限表,后续做后端接口校验就是照着这张表来。
| 角色 | 核心操作 | 数据可见范围 |
|---|---|---|
| 学生 | 提交预约、取消预约、查看自己的预约记录 | 只看自己的预约 |
| 教师 | 审批预约、查看实验课表 | 可查看与自己相关的预约 |
| 实验管理员 | 审批所有预约、维护实验室/设备/耗材、发布通知 | 全部预约和资源数据 |
| 系统管理员 | 用户管理、角色分配、系统配置 | 全部数据 |
边界一旦定清楚,后端接口的权限校验就变得很直接:每个接口标注允许访问的角色,拦截器统一处理,这个逻辑在第4章会详细展开。我想强调的一点是,权限判断一定不能只靠前端隐藏按钮,后端每个接口都要自己校验角色,否则绕过页面直接调接口就能越权操作。
2. 技术栈的分工逻辑:SpringBoot、小程序和Vue各自扛什么
2.1 两个客户端入口,一个REST后端
这套系统的最终形态是:学生用微信小程序,管理员和教师用Vue网页后台,两个入口共享一个SpringBoot后端。请求链路可以这样理解:
小程序通过wx.request发请求,经过SpringBoot的Controller、Service、Mapper,最终落到MySQL;Vue后台通过axios发请求,走的是完全相同的链路。小程序和Vue在这里只是"皮肤",真正的业务逻辑、权限判断、数据校验全都在后端完成。
这样的前后端分离设计好处很明显:改预约规则时只需要动后端,两个端同时生效;以后如果要增加一个管理端入口,也只需要复用现有API,不需要重写业务层。
2.2 为什么小程序端用原生开发而不是跨端框架
很多团队会在原生微信小程序和uni-app之间纠结。这个项目选原生,核心原因有三个。
第一,跑在微信环境里,原生的基础库对接口支持最及时,碰到登录、订阅消息这类能力时直接用官方API,少一层框架桥接也少一层坑。第二,开发周期短,这类项目的页面数量不超过二十个,原生的小程序语法本身已经足够简单。第三,没有跨端需求,不需要为安卓App或支付宝小程序复用代码。如果后续真的有跨端诉求,再把业务逻辑抽成独立模块迁移也不迟。
2.3 后端和管理端选型的实际考量
SpringBoot是这类系统非常稳妥的选择:内嵌Tomcat,自动配置,生态成熟,打包成一个可执行jar就能部署。Controller/Service/Mapper的分层非常清晰,做预约这种强流程业务很顺手。配合MyBatis Plus,单表的增删改查基本不用手写SQL,可以把精力放在状态流转和冲突判定这类核心逻辑上。
Vue管理端胜在组件化,Element UI提供现成的表格、表单、日期选择器、弹窗组件,审批列表和实验室管理页面几乎全是组件拼装出来的,开发效率比写原生HTML高一个量级。前后端分离之后,Vue只需要维护后端返回的JSON结构,和后端接口并行开发互不阻塞。
3. 数据库建模:预约冲突、设备状态和权限就靠这几张表
3.1 用户表和实验室表:先定义"谁"和"资源"
用户表是最基础的,我建议字段至少包含:id、username、password、real_name、role、department、phone、openid。password字段存的是BCrypt加密后的密文,明文密码进数据库是绝对不能做的事情。openid字段用来绑定微信账号,小程序登录时先根据openid定位用户,查不到就走"账号绑定"流程。
实验室表的核心字段是:id、name、location、capacity、description、open_time、close_time、status。open_time和close_time限定了每天可用时段,比如8:00到22:00,预约提交时要校验日期、时间段和开放时段的关系。status字段用数字表示:0开放、1维护中、2停用。维护中的实验室必须在前端接口层面被过滤掉,避免学生选了之后又被告知不能用。
3.2 预约表:一个状态字段撑起整个流程
预约表是整张数据库的核心,字段包括:id、user_id、lab_id、date、start_time、end_time、purpose、status、approver_id、approve_time、remark。其中status字段定义成一个整型状态码,我会建议项目按下面这张表统一口径,避免前端拿到的状态和后端定义对不上。
| 状态码 | 含义 | 前端展示 | 允许操作 |
|---|---|---|---|
| 0 | 待审批 | 等待审批 | 学生可取消 |
| 1 | 已通过 | 预约成功 | 学生可确认使用/取消 |
| 2 | 已拒绝 | 审批未通过 | 学生可重新申请 |
| 3 | 已取消 | 已取消 | 无 |
| 4 | 已完成 | 使用完成 | 无 |
这个状态字段贯穿了预约生命周期,第4章的接口代码全部基于这套定义来写。状态码用数字存储,是从存储和传输效率角度考虑的;为了代码可读性,在Java里用枚举或者常量类做映射,不要在业务代码里直接散落各种魔法数字。
3.3 冲突判断:一条SQL挡住重复预约
预约功能最容易翻车的地方就是时间冲突。同一间实验室,同一个日期,如果已经有待审批或已通过的预约占用了某个时间段,新预约就不能再申请重叠的时段。核心查询我建议写成这样:
SELECT COUNT(*) FROM reservation WHERE lab_id = #{labId} AND date = #{date} AND status IN (0, 1) AND start_time < #{endTime} AND end_time > #{startTime}这里的逻辑起点是区间相交判断:只要新时段的开始时间早于已有时段的结束时间,并且新时段的结束时间晚于已有时段的开始时间,那么两个区间一定有重叠。这个写法比"开始时间在已有区间内或结束时间在已有区间内"更稳妥,因为它能覆盖一方完全包含另一方的情况。
状态条件里排除2、3、4是有讲究的:被拒绝、已取消的预约不占资源,已完成的预约时间已经过去,也不影响新的申请。为了查询性能,预约表建议加一个联合索引(lab_id, date, status),这个索引能同时覆盖实验室、日期、状态三个过滤维度,数据量大了之后也不会太慢。
3.4 设备、耗材和通知:辅助资源管理的三张表
设备表建议字段:id、lab_id、name、model、status、buy_date。设备挂在实验室下面,状态区分正常、借用中、维修中。管理员在维护设备时,不能只改设备状态,借用和归还也要有记录,否则月底对不上账。
耗材管理建议用两张表:耗材表记录名称、库存、单位、预警阈值,耗材领用记录表记录领用人、领用数量、领用时间、用途。只改库存不记流水是常见坑,出现对不上账的时候就只能干瞪眼。
通知表相对简单:id、title、content、target_role、create_time、creator_id。target_role决定这条通知是发给所有学生还是只发给老师,前端根据角色拉取对应通知。
4. 后端接口与核心逻辑:登录鉴权、预约提交和审批状态机
4.1 微信登录与自研Token鉴权
小程序端登录不能直接用账号密码,标准流程是:小程序调用wx.login拿到临时code,把code交给后端;后端拿code去微信接口换openid;再用openid查用户表,查到就生成token返回,查不到就返回"请先绑定账号"的错误码。前端把token存到本地缓存,之后所有请求在header里带上token。
我建议这个项目自定义token而不是直接放openid,因为openid一旦泄露就等于账号被人拿走。自定义token用UUID或者一串随机字符串,存在一张token表里,或者引入Redis做缓存。下面的代码是登录流程的骨架,核心在于code换openid之后一定要兜底处理"用户不存在"的情况。
@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { // 1. 调用微信 jscode2session 接口,用 code 换 openid String openid = wechatService.code2Session(dto.getCode()); // 2. 根据 openid 查询用户,查不到则返回需要绑定的提示 UserVO user = userService.findByOpenid(openid); if (user == null) { return Result.error(1001, "该微信未绑定账号,请先绑定"); } // 3. 生成 token 并保存 String token = UUID.randomUUID().toString().replace("-", ""); tokenService.save(token, user.getId()); // 4. 返回 token 和用户基本信息 return Result.ok(new LoginResult(token, user)); }鉴权就用一个拦截器统一处理。拦截器从header里取出token,校验token有效后把userId放进请求上下文,后续Controller直接取用。需要角色权限的接口,在拦截器里再读一次user表的role字段做判断。这个方案比在每个Controller里写重复的校验代码干净得多。
4.2 预约提交:先查状态再插入,并发下也要防一手
预约接口是学生触达最频繁的接口,它的逻辑顺序非常关键,我按下面的顺序实现了这个接口:
- 拦截器完成登录鉴权,确认当前用户是学生角色;
- 校验实验室存在,且status为开放;
- 校验预约日期不能早于今天;
- 校验endTime必须晚于startTime;
- 检查预约时段是否落在实验室的开放时段内;
- 执行冲突查询,存在重叠预约则直接拒绝;
- 插入预约记录,初始status为0。
@PostMapping("/reserve") @Transactional(rollbackFor = Exception.class) public Result reserve(@RequestBody ReserveDTO dto) { Laboratory lab = labService.getById(dto.getLabId()); if (lab == null || lab.getStatus() != 0) { return Result.error("该实验室当前不可预约"); } if (dto.getEndTime().isBefore(dto.getStartTime())) { return Result.error("结束时间必须晚于开始时间"); } if (reservationService.hasConflict(dto)) { return Result.error("该时段已被预约,请更换时间"); } reservationService.create(dto, currentUserId()); return Result.ok(); }这里要特别说一个隐患:同时发起两个相同时间的预约请求,两个请求都可能通过冲突查询,然后都插入成功。小型项目里这种并发概率很低,但如果想彻底防住,可以在插入时再做一次条件检查,或者给预约表加一个不可重复的约束。实际项目采用的是"事务包裹冲突查询+插入"的方案,服务端加事务注解,能覆盖绝大多数场景。
4.3 审批接口:用条件更新防止重复审批
审批接口的常规写法是:先查出预约记录,判断状态,然后改成通过或拒绝。这样写的问题是,如果管理员A和管理员B同时打开同一条待审批记录,两个人都点了"通过",第二次更新会把第一次的审批人覆盖掉,最后这条记录到底是谁审批的说不清楚。
更稳的写法是用条件更新,把"状态必须是0"作为更新的前置条件:
UPDATE reservation SET status = #{newStatus}, approver_id = #{approverId}, approve_time = NOW() WHERE id = #{id} AND status = 0执行完看影响行数,如果等于1说明审批成功;如果等于0说明这条记录已经被人处理过了,直接返回"该预约已被处理"。这种方式不需要加锁,也不用引入Redis分布式锁,在这个业务体量下是最划算的方案。
同时要守住状态机的非法转移:只有status=0的预约可以变成1(通过)或2(拒绝);status=1的预约可以变成3(取消)或4(完成);已经取消或拒绝的记录不能再变回其他状态。这些规则在后端Service里统一判断,前端再怎么乱点也影响不到数据正确性。
5. 小程序端和Vue管理端的实现细节:从登录到操作完成的每一环
5.1 小程序端:登录态、预约表单和列表分页
小程序端核心页面是首页、预约提交、我的预约、个人中心。我建议把所有请求封装到一个公共request模块里,统一加token、统一处理错误码,避免每个页面重复写一堆wx.request的样板代码。封装之后,页面里调用接口就变得很简洁。
预约提交流程中,表单校验不能只靠后端,前端要先拦一道:日期不能早于今天,结束时间必须晚于开始时间,实验室必须选择。日期选择可以用picker组件设置start属性为今天,时间选择用两个独立的picker,在确认时比较起止时间。
我的预约列表要处理分页。小程序列表页常见的做法是用onReachBottom触底加载下一页,每次请求携带pageNum和pageSize。
onReachBottom() { this.data.pageNum += 1; this.loadReservations(); }列表加载完成后,根据每条记录的status字段渲染不同的操作按钮:待审批的显示"取消",已通过的显示"确认使用",已拒绝的显示"重新申请"。这样用户不看任何文字说明也能理解当前状态。
5.2 Vue管理后台:审批工作台和数据统计
Vue管理端的关键页面是登录页、首页统计、预约审批、实验室管理、设备管理和用户管理。审批列表用表格组件实现,每行显示学生姓名、实验室名称、日期、时段、用途,右侧放"通过"和"拒绝"两个按钮,操作前用弹窗做二次确认,防止手滑点错。
axios拦截器是Vue端必须配好的基础设施。请求拦截器从store或localStorage里取token加到header;响应拦截器统一处理401跳转登录,业务错误码统一弹出提示。如果每个页面单独处理这些逻辑,代码会重复到你怀疑人生。
首页统计是管理员比较看重的功能,不需要上太重的大屏方案,用统计卡片加简单表格就够:今日预约数、待审批数、实验室总数、实验室使用率。这些数据都可以通过后端聚合接口一次返回,前端拿到数据直接渲染。
5.3 角色差异在页面层怎么处理
学生和教师在Vue端看到的东西完全不同,但我不建议在页面里到处写role判断。更干净的做法是:登录接口返回用户信息和角色,前端根据角色配置不同的路由表,管理员进管理后台,教师进审批页面,学生端则完全使用小程序。Vue Router的beforeEach里做一次路由守卫,没有权限的跳转到对应入口。
这里要记得,前端隐藏路由和菜单只是体验优化,真正的权限边界必须由后端接口守住。有人会直接修改前端路由配置绕过菜单,但只要后端每个接口都做了角色校验,越权操作就不会成功。
6. 联调、部署与维护:本地跑通不等于能上线
6.1 本地联调阶段最容易卡住的三个点
本地开发时,小程序默认不允许访问http://localhost,需要在小程序开发者工具的"详情-本地设置"里勾选"不校验合法域名",这是开发期正常操作,上线前一定要关掉校验。
第二个点是跨域。Vue开发服务器默认跑在8080端口或5173端口,浏览器直接请求后端接口会跨域,一般通过开发服务器的proxy把/api代理到后端地址,这样前后端联调时只需关注接口路径。如果后端和前端部署在不同域名,也要在后端配置CORS规则,两者结合使用。
第三个点是真机预览。小程序开发者工具里请求localhost没问题,但手机预览时localStorage里的baseURL还是localhost就会失败。真机调试时要用电脑在局域网内的IP地址,手机和电脑连同一个WiFi,才能访问到后端的接口服务。
6.2 上线部署:HTTPS域名、jar包和Nginx反向代理
正式上线前要准备一台云服务器和一个已备案的HTTPS域名。HTTPS不是可选项,微信小程序正式版本对request域名强制要求HTTPS,也就是说域名必须有有效的SSL证书,而且这个证书要覆盖到接口域名。
后端部署的步骤可以归纳为四条:先在服务器上导入SQL初始化数据库,并修改application.yml里的数据库地址、账号、密码;再用mvn clean package打出可执行jar包;上传到服务器后用nohup java -jar启动;最后用日志确认服务正常监听端口。
Vue管理端打的是静态资源:执行npm run build后,把dist目录上传到服务器,由Nginx托管,同时把/api路径反向代理到后端端口。Nginx配置示意如下:
server { listen 443 ssl; server_name your-domain.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location / { root /usr/share/nginx/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }最后到微信公众平台后台配置request合法域名,把https://your-domain.com填进去。这里要提醒一句:后端如果保留了8080端口直接暴露,建议用安全组只放行443和SSH端口,请求一律走Nginx转发,减少业务接口被直接扫描的风险。
6.3 运行维护中的几个常见坑
第一个是时区。MySQL的连接串里一定记得加serverTimezone参数,否则时间差8个小时的诡异问题会让人排查很久。第二个是字符集,建库时用utf8mb4,不然学生提交的备注里有生僻字或颜文字就会入库失败。第三个是Nginx下刷新页面404,解决办法是用hash路由,或者在Nginx里配置try_files回退到index.html。第四个是jar包更新,先备份旧包、再停进程、替换文件、重启、最后盯日志,顺序不能乱;启动失败时优先检查端口占用,8080被其他进程占住是常见问题。第五个是HTTPS证书过期,小程序端所有请求会直接失败,建议提前设置证书到期的提醒,我吃过这个亏之后每次部署都会在日历上标记。
最后再分享一个我在实际维护中的体会:这类系统上线之后最大的压力不在代码本身,而在预约规则的频繁变化。比如某个学院临时开放一批实验室、期末考试期间延长开放时间、某些仪器设备临时停用维护。如果这些规则都硬编码在代码里,每次变更都要重新改代码发版。最好在管理端预留一个"预约规则配置"页面,把开放时段、提前预约天数、最大预约时长都做成可配置项,减少后续维护成本,也能让系统在真实运行环境中活得更久。