ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

微信小程序 + Spring Boot 前后端分离实验室预约管理系统设计与实现

微信小程序 + Spring Boot 前后端分离实验室预约管理系统设计与实现 1. 项目概述与整体设计思路1.1 这个项目到底做了什么先说项目本身。高校实验室管理系统本质上是把实验室的预约、设备借用、耗材领用、使用记录这些线下流程搬到线上。学生通过微信小程序查看空闲实验室、提交预约教师或实验员在管理后台审核、管理实验室和设备信息系统管理员负责用户和权限。三个角色两个端一个后端服务。这个标题里最关键的两个词是“微信小程序”和“前后端分离”。小程序端面向学生和教师管理端用 Vue 搭建后端统一用 Spring Boot 提供 RESTful 接口。我在实际开发中把整个工程拆成了三个目录backendSpring Boot 服务端、admin-vuePC 管理端、miniapp微信小程序端。源码、文档、调试说明都按这个结构整理拿到手的同学不需要花时间去猜代码放在哪里。这类项目在高校里是真有落地场景的。实验室排课、开放预约、设备外借以前靠纸质登记本管理员每天要手动核对时间冲突学生也不知道哪个时段有空位只能一趟趟跑实验室。把预约流程线上化之后核心价值就两个降低管理成本提高实验室利用率。对做毕设或者学习 Spring Boot Vue 小程序这套技术栈的同学来说它也是一个麻雀虽小但五脏俱全的完整案例——有用户体系、有权限控制、有核心业务逻辑、有前后台交互。1.2 为什么选这套技术栈组合Spring Boot 负责后端接口和业务逻辑微信小程序负责学生端的预约操作Vue 负责 PC 管理后台。这个选型不是我拍脑袋定的而是这类管理系统最常见的组合原因很实在第一微信小程序对学生端的触达成本最低。高校场景里学生不可能为了预约一个实验室去安装一个 APP小程序扫码即用用完即走完全符合这个使用场景。第二管理端用 Vue Element Plus 做后台界面效率非常高表格、表单、弹窗这些后台管理的常见组件都是现成的。第三Spring Boot 天然适合做这种中小型系统的后端内置容器、自动配置、生态成熟写 CRUD 接口速度极快。前后端分离在这个项目里的意义不只是技术选型的问题而是本身就存在两类使用终端。小程序跑在微信环境里管理端跑在 PC 浏览器里两者交互逻辑完全不同物理上就必须分成两套前端。后端只需要统一暴露 JSON 接口谁调用都行。这也方便后续扩展——将来要加一个 H5 端或者教师端 APP前端重写就行后端接口完全可以复用。我一般建议第一次做这种全栈项目的同学先把后端接口设计好拿 Postman 把每个接口调通再去做前端页面。否则前后端同时写出了问题很难定位是接口的问题还是页面的问题。2. 后端核心模块与数据库设计细节2.1 核心业务模块拆解后端按业务域拆包每一个模块对应一张主表。整个项目里我最看重的几个模块是这样划分的用户模块学生、教师、管理员三种角色JWT 登录鉴权微信小程序通过wx.login换取 openid 自动注册。实验室模块实验室的基础信息、可用时间段、容量、设备清单的关联维护。管理端支持对实验室信息做增删改查。预约模块学生提交预约申请选择实验室、时间段、用途教师或实验员在管理端审核。这是整个系统的核心业务。设备模块实验室内的设备清单管理员维护设备状态正常/维修/报废预约实验室时可以顺带查看可用设备。使用记录模块预约通过后自动生成使用记录实验完成后可补充实际使用情况。划分的原则就一条高内聚、低耦合。每个模块之间通过接口交互不互相直接操作表。比如预约模块不直接改实验室表的字段而是通过查询判断冲突这样后续改需求的时候不会牵连一片。2.2 数据库表结构设计的几个关键决策这部分是整个项目里最容易返工的地方。我第一版设计的时候就踩过坑把预约信息、实验室信息、设备信息全塞在几张宽表里后面加字段改需求非常痛苦。第二版按主题拆分后结构清晰了很多。拿核心的预约表做例子字段名类型说明idbigint主键自增user_idbigint预约人 ID关联用户表lab_idbigint实验室 ID关联实验室表reserve_datedate预约日期start_timetime开始时段end_timetime结束时段purposevarchar预约用途statustinyint0待审核 1已通过 2已拒绝 3已取消 4已完成create_timedatetime提交时间设计这张表的时候我特意把 reserve_date、start_time、end_time 拆成三个字段而不是合并成一个 datetime。原因在于实验室预约大都是按“某天的某段时间”来组织的白天分上午下午晚上分几个课时段日期和时段是天然分开的筛选维度。如果合成一个字段后面做“某一天的所有预约记录”和“某个时段是否被占用”的查询条件都会变得很别扭。另外一个关键决策是预约状态用 tinyint 而不是 varchar 存中文。用数字存状态配合后端枚举类做映射好处有两个一是数据库体积小、查询效率高二是避免了中文在不同字符集下可能出现的匹配问题。前端拿到 0-4 的数字后自己去映射显示文案也不会因为数据库里的中文改了导致全链路崩溃。2.3 数据访问层的选择与配置数据访问我用的是 MyBatis-Plus而不是原生 MyBatis。原因很简单这个项目大部分操作都是单表 CRUDMyBatis-Plus 的BaseMapper直接内置了selectById、insert、updateById、selectPage这些常用方法我连 SQL 都不用写。只有在预约冲突检测这种需要多表联合查询的场景才手写一条 SQL。select idselectConflictReservations resultTypecom.example.entity.Reservation SELECT * FROM reservation WHERE lab_id #{labId} AND reserve_date #{reserveDate} AND status IN (0, 1) AND start_time lt; #{endTime} AND end_time gt; #{startTime} /select这段 SQL 是预约模块的核心看懂了它就理解了这个系统的防冲突逻辑。两个预约产生冲突的条件是同一个实验室、同一天、时间段有交叉、且状态都是有效的待审核或已通过。时间段交叉的判断用的是“开始时间小于别人的结束时间且结束时间大于别人的开始时间”这个条件覆盖了包含、相交、首尾相接所有情况。我在初版写的时候只看了一眼两边完全相等的情况结果一个 10:00-12:00 和一个 11:00-13:00 的预约同时在系统里通过了审核后来测试才发现这个问题。字段名里start_time和end_time在 MySQL 里不是保留字可以直接用。但如果是desc、order这类词就得加反引号。这个项目里我给所有表字段都用了_分隔的命名风格比如create_time、lab_id因为 MyBatis-Plus 默认开启了驼峰命名映射Java 里的createTime字段能自动对应数据库的create_time列省掉了大量TableField注解。3. Vue 管理端的实现与前后端联调3.1 Vue 开发环境搭建的版本坑管理端我选的是 Vue 3 Vite Element Plus。这套组合在 2024 年已经是绝对主流了但很多同学在环境配置阶段就被卡住。最常见的原因是两个Node 版本太老或者 npm 镜像源太慢。Vite 5 要求 Node 版本 18 以上如果你机器上还是 Node 14/16启动就直接报错。我建议直接用 nvm 管理 Node 版本指定安装 Node 18 LTS 即可。另一个是 npm 安装依赖国内用户建议先设置镜像源否则npm install卡半小时都装不完npm config set registry https://registry.npmmirror.com装好依赖后npm run dev启动开发服务器。Vite 默认跑在 5173 端口后端的 Spring Boot 跑在 8080 端口这就涉及前后端联调最核心的问题——跨域。3.2 跨域问题的两种解法开发环境最简单的方案是用 Vite 的代理功能在vite.config.js里配置export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端代码里请求/api/user/loginVite 会自动转发到http://localhost:8080/api/user/login浏览器的同源策略就不会拦截了。后端也要做一层 CORS 兜底因为生产环境前端是部署在 Nginx 上的Nginx 直接转发/api开头的请求但如果有人直接访问后端地址跨域问题就会暴露出来。在 Spring Boot 里加一个配置类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); } }这里踩过一个坑allowCredentials(true)的时候allowedOrigins不能写*必须用allowedOriginPatterns否则前端带 Cookie 或者 Token 的请求会被浏览器拦截。初次排查的时候我以为是跨域配置没生效折腾了半天才发现是这个细节。3.3 管理端的页面结构与权限控制管理端页面按角色和功能划分登录后根据用户角色动态渲染菜单。管理员能看到全部菜单实验员只能看到实验室管理、预约审核和使用记录这几个模块。路由权限控制的思路是在路由对象里给每个路由配置一个meta.roles字段登录后从后端获取当前用户的角色通过 Vue Router 的beforeEach全局守卫做判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else if (token to.path /login) { next(/) } else { next() } })这里的逻辑比较基础但演示了前端路由守卫配合 JWT 做页面级权限控制的基本套路。真正实用的角色权限控制一定会同时在后端接口层面再校验一遍前端隐藏菜单只是用户体验后端校验才是真正的安全边界。管理端的核心页面就是预约审核页。这个页面的数据量大、交互复杂我用的是 Element Plus 的el-tableel-pagination做分页列表每条记录后面根据状态展示不同的操作按钮待审核状态的显示“通过”和“拒绝”已通过的显示“完成”。这里又涉及一个细节——按钮要根据状态动态显示我把状态判断逻辑抽成了计算属性而不是在每个按钮上写一堆v-if维护起来清爽很多。4. 微信小程序端的关键实现细节4.1 登录与手机号获取的现状小程序端第一个绕不开的模块就是登录。2023 年之后微信官方调整了手机号快速验证组件的规则原来那个通过wx.login拿到 code 再调getPhoneNumber获取手机号的旧方案已经废弃了。现在比较稳妥的做法是基础登录用wx.login换取 openid在服务端校验通过后颁发 JWT如果需要手机号就引导用户点击页面上的button open-typegetPhoneNumber通过bindgetphonenumber事件拿到动态令牌再传给后端解密获取真实手机号。我在这个项目里简化了一步登录时只拿 openid 作为用户唯一标识手机号作为可选项在个人资料页补全。原因是预约审核本身是校内流程学生对身份的识别主要靠学号手机号只是联系用的辅助信息没必要在登录环节强制要求。服务端对应的接口接收code之后调用微信的jscode2session接口public String code2Session(String code) { String url String.format( https://api.weixin.qq.com/sns/jscode2session?appid%ssecret%sjs_code%sgrant_typeauthorization_code, appId, appSecret, code); RestTemplate restTemplate new RestTemplate(); String response restTemplate.getForObject(url, String.class); return response; }这里要注意一个细节appSecret绝对不能放在小程序前端代码里只能放在后端。有同学把 secret 写在小程序里直接请求微信接口等于把口令交给所有人这个协议层面就是违规的。4.2 顶部导航栏高度适配的通用解法小程序的自定义导航栏在适配不同机型时经常出问题。标题里特别提到“顶部导航栏高度”说明这是很多初学者的痛点。不同机型的状态栏高度不一样iPhone X 系列有刘海状态栏高度大概 44px普通 Android 机大约是 24px。如果你做自定义导航栏硬编码一个高度就会出现顶部遮挡或者空白过多的问题。我常用的方案是在app.js的onLaunch里计算导航栏高度存到全局变量页面里动态绑定const systemInfo wx.getWindowInfo() const menuButton wx.getMenuButtonBoundingClientRect() const navBarHeight (menuButton.top - systemInfo.statusBarHeight) * 2 menuButton.height this.globalData.navBarHeight navBarHeight this.globalData.statusBarHeight systemInfo.statusBarHeightgetMenuButtonBoundingClientRect返回的是右上角胶囊按钮的位置信息。用“胶囊顶部到状态栏底部的距离 × 2 胶囊按钮高度”计算出来的导航栏高度能保证不同机型上胶囊按钮都垂直居中在自定义导航栏里这是目前适配性最好的方案。页面端就是一个 slot 布局自定义导航栏的样式保持和系统导航一致——左侧返回箭头、中间标题文字上下间距通过 padding 撑开。4.3 预约流程的交互设计小程序端预约流程我设计成四个步骤每一步都尽量减少用户操作首页展示本周可预约的实验室列表卡片上显示实验室名、位置、可容纳人数。点击“预约”进入详情页选择日期和时段。时段的选项由后端根据已通过审核的预约动态计算已经被占用的时段直接置灰不可选。填写用途如课程实验、科研项目提交后弹出确认框展示预约的完整信息。提交成功跳转到“我的预约”列表可以查看当前所有预约的审核状态。这个流程里最值得展开的是第二步。时段选择组件的数据来源不是前端写死的而是后端实时计算返回的。后端在预约接口里先把实验室开放的所有基础时段返回给前端如 08:00-10:00、10:00-12:00、14:00-16:00、16:00-18:00、19:00-21:00然后查询该实验室当天的有效预约把冲突的时段标记为不可选。这样用户在界面上看到的永远是真实可预约的时段不会出现提交之后后端告诉你说“该时段已被预约”的尴尬情况。提交预约和审核通过的接口都有重复提交的并发问题。我用了两个层面的保护一是数据库层的唯一索引(lab_id, reserve_date, start_time, end_time)二是 MySQL 的select ... for update配合事务。在实际压测里这两个措施基本把并发冲突挡掉了。后面在问题排查部分再细说。5. 项目部署与上线调试全流程5.1 三端部署的正确姿势项目开发完成后部署到服务器上要分三步走。顺序不能乱后端mvn clean package打成 jar 包扔到服务器上执行nohup java -jar lab-system.jar --spring.profiles.activeprod 。生产环境的数据库连接、微信小程序 appid、secret 这些配置写在application-prod.yml里不写进代码仓库这是底线要求。管理端npm run build构建出 dist 静态文件放到 Nginx 的html/admin目录下配置一个 server 块。Vue 项目部署最大的坑是路由的 history 模式——如果前端用了createWebHistory()刷新页面就会出现 404。解决办法是在 Nginx 配置里加一个 try_files 回退location /admin/ { alias /usr/share/nginx/html/admin/; try_files $uri $uri/ /admin/index.html; }小程序端微信开发者工具里点击“上传”填写版本号和备注然后在微信公众平台提交审核。这里有一个硬性要求小程序请求的域名必须是 HTTPS 且已备案、已配置在后台的合法域名里。开发阶段可以在开发者工具里勾选“不校验合法域名”但上线前必须关掉这个选项否则真机预览直接失败。5.2 接口联调与文档管理接口文档我用的 Knife4jSwagger 的增强版集成到 Spring Boot 里非常方便引入依赖后加一个配置类就能用。访问/doc.html就能看到所有接口的调试页面支持在线传参测试。开发过程中我要求自己每写完一个接口就同步写好注解不要等全部写完了再补不然容易漏。小程序端请求封装不要直接拿wx.request硬写一定要封装一层const request (url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: ${baseUrl}${url}, method, data, header: { Authorization: Bearer ${wx.getStorageSync(token)} }, success: (res) { if (res.data.code 200) { resolve(res.data.data) } else if (res.data.code 401) { wx.navigateTo({ url: /pages/login/login }) } else { reject(res.data) } }, fail: reject }) }) }统一封装有几个实际好处所有请求自动带 token、统一处理错误码、401 统一跳转登录页。我自己在开发中经常改后端接口的响应结构如果每个页面的请求都是裸写的改动量会非常大有了这层封装改一个地方就全局生效了。5.3 真机调试的几个经验小程序和 Vue 管理端有个本质区别小程序跑在微信里调试不能靠浏览器开发者工具必须频繁用真机预览和扫码测试。真机调试最常遇到的怪问题就是“请求失败”或者“网络异常”。排查思路按照下面的顺序来确认手机和开发电脑连接的是同一个局域网本地开发时baseUrl要写成电脑的局域网 IP比如http://192.168.1.101:8080不能写localhost。确认微信开发者工具里“不校验合法域名”开关已打开真机调试时这个功能需要在预览二维码弹窗里选择“真机调试 2.0”模式基本都能解决。确认后端接口的主机地址是能被手机访问的。很多同学困在“电脑上浏览器访问接口正常手机小程序访问失败”基本都是因为后端启动时绑定的地址是127.0.0.1改成0.0.0.0才能被局域网内其他设备访问。我还遇到过一个小程序特有的问题Android 手机上wx.request走 HTTP 明文会被拦截。微信官方要求线上环境必须 HTTPS但在调试阶段Android 机型的默认策略可能直接拒绝明文流量。处理方式是在微信公众平台的小程序开发设置里临时把对应域名加到“开发调试白名单”或者干脆用真机调试的 proxy 模式。6. 常见问题与排查技巧实录6.1 Spring Boot 版本相关的坑这个话题反复被提到“springboot版本太高”确实是这个项目最高频的坑。我用 Spring Boot 3.2.1 做后端要求 JDK 17 以上。很多同学的电脑上默认装的还是 JDK 8启动就报UnsupportedClassVersionError或者干脆Exception in thread main java.lang.RuntimeException。解决方案有两个。一个是你愿意升级环境用 JDK 17 跑 Spring Boot 3.x这是长期正解。另一个是坚持用 JDK 8那就把 Spring Boot 版本降到 2.7.18这个版本是 2.x 的最后一个版本稳定性和文档都齐全。另外特别注意Spring Boot 2.7.x 和 3.x 的springdoc配置类写法有差异。如果你用 3.xjavax.servlet相关的依赖要全部换成jakarta.servlet手写拦截器时import javax.servlet.*会编译不通过。我第一次升级到 3.x 时被这个问题卡了一整晚后来用全局搜索把javax全部替换成jakarta才解决。还需要提醒的是不要在 pom.xml 里加一堆用不上的依赖。有同学参考别人的项目把自己需要的依赖全加进来比如引入了 ActiveMQ、Flink 这种和实验室管理毫无关系的组件导致启动变慢而且报一堆无意义的错误。这是很多“无效依赖导致排查困难”案例的根源。6.2 小程序端异常问题的速查表问题现象排查思路解决方案小程序请求后端接口超时检查后端地址是否用了 localhost改为局域网 IP确认手机与电脑同网段导航栏高度在不同机型上不一致硬编码了高度值用getMenuButtonBoundingClientRect动态计算提交预约提示“请先登录”token 过期或未正确传递检查请求封装是否带了 Authorization 头真机预览无法访问接口后端绑定了 127.0.0.1后端启动时绑定0.0.0.0手机号获取组件报错使用了旧的getPhoneNumber接口改用手机号快速验证组件头像上传失败开发环境证书不合法开发阶段用本地临时工具生产环境用 HTTPS这里面最值得展开的是预约冲突导致的并发问题。两个学生同时提交同一实验室同一时段的预约后端如果只是先查询再判断再插入就会存在并发竞态。我在上面提过用数据库乐观锁处理具体实现是给预约表加一个version字段更新时UPDATE reservation SET status 1 WHERE id ? AND version 1。如果更新影响行数为 0说明数据已经被别的操作改了直接提示用户失败。对于初学者来说我建议最简单的做法先在数据库层面建一个针对(lab_id, reserve_date, start_time, end_time)的唯一索引在业务代码里把这个竞争化为数据库的唯一约束异常代码短且可靠。6.3 前后端联调的其他踩坑经验最后说几个很碎但很容易反复踩的细节。后端返回的时间字段格式。Spring Boot 默认返回的时间格式是带毫秒的时间戳比如2024-06-01T09:30:00.00000:00小程序的new Date()处理这种字符串偶尔会出现格式兼容问题。我给后端加了统一的 Jackson 配置格式化日期为yyyy-MM-dd HH:mm:ss。一个小配置能少掉一堆前端的日期解析异常。前端请求参数和后端 Java Bean 的字段映射。前端习惯用驼峰命名如userName后端如果没用驼峰映射数据库字段user_name就接收不到。确认 MyBatis-Plus 的map-underscore-to-camel-case配置已开启否则数据对不上排查起来会想骂人。接口返回的数据结构一定要前后端约定好。我项目里统一用的返回类是ResultT包含 code、message、data 三个字段。code 为 200 代表成功401 未登录500 服务器异常。前端请求封装层就是按这个结构解析的。如果某个接口返回结构不一致页面拿到数据就undefined但这种报错往往没有任何报错信息排查全靠打断点。调试阶段多打日志别只靠 Postman 看响应。我习惯在后端每一个 controller 方法接收请求时打一行log.info(接收到预约请求: {}, reservation)在业务逻辑关键节点再打几行状态日志。生产环境出了问题先翻日志定位是在哪一层挂掉的效率比瞎猜高得多。7. 源码交付与后续扩展方向7.1 我习惯的源码交付结构项目交付时三个工程的目录划分我习惯这样组织lab-system/ ├── backend/ # Spring Boot 后端 │ ├── src/main/java │ ├── src/main/resources │ └── pom.xml ├── admin-vue/ # Vue 管理端 │ ├── src/ │ ├── package.json │ └── vite.config.js ├── miniapp/ # 微信小程序端 │ ├── pages/ │ ├── app.js │ └── app.json ├── sql/ │ └── lab_system.sql # 建库建表脚本 └── README.md # 部署说明文档README 文档我坚持要写清楚三件事环境要求JDK 版本、Node 版本、启动步骤先后端还是先前端、默认账号密码。很多同学的毕设源码拿给别人跑不起来90% 是缺了环境要求说明。另外 SQL 脚本要留着不要只导出一个.sql文件藏在本机交付时直接把初始化数据也带上省得别人自己乱造数据。7.2 系统后续还能扩展什么这个项目做完之后我根据自己的使用体会梳理了几个值得扩展的方向。第一个是消息通知。目前预约审核的结果只能在管理端看到学生打开小程序才能查看到。如果接入订阅消息或者企业微信机器人预约通过后自动推送到学生微信闭环体验会完整很多。第二个是排课功能。高校实验室通常有固定的实验课程安排目前的预约系统只处理学生自主预约。排课表和预约表如果合并成一张日程表统一管理能避免学生和老师各约各的撞在一起。第三个是数据统计分析。后端已经积累了预约记录、使用记录、实验室利用率这些原始数据做一个 Dashboard 展示各实验室的使用率排行、各院系的预约占比、高峰时段的分布对管理者是很有价值的辅助决策信息。管理端里我目前只做了一个最基础的统计页按实验室和月份查使用次数后续可以扩展成真正具备分析能力的数据看板。项目做到这个程度框架的搭建已经完成了。麻雀虽小五脏俱全从用户注册登录、角色权限、核心业务审批流到前后端部署联调全部跑通。剩下的事情就是在真实使用中不断发现需求、补齐细节的过程。
RELATED READING

延伸阅读

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