简介:这是一套面向高校实验室管理人员与信息化建设者的微信小程序完整解决方案,聚焦解决传统实验室设备预约混乱、微信群登记低效、审批流程不透明等管理痛点。资源基于微信云开发实现,涵盖实验室动态发布、规章制度展示、基础/专业实验室分级预约、多级审批流、用户角色权限管理等核心模块,支持灵活设置预约时段与人数、自定义表单字段、二维码自助签到及Excel导出预约数据,助力实现有序实验、规范溯源与效率提升。压缩包含487个文件(3.55MB),以186个JS逻辑文件、105个WXSS样式文件、82个WXML页面结构文件为主,辅以JSON配置、图片资源及工具库(如qrcode_lib.js、faker_lib.js、db_util.js等),目录结构清晰,便于二次开发与功能扩展。目前已有290人学习下载,开箱即用,适合教学管理信息化实践与小程序全栈开发参考。
1. 项目概述与核心需求解析
1.1 为什么实验室预约系统非做不可
我在学校信息中心帮忙维护过很长一段时间的实验室管理系统,说实话,早期那种“管理员拿Excel登记、学生打电话预约、老师靠微信群确认”的方式问题太大了。高峰期几个实验室抢在一起,时间撞了又没人知道,学生到了现场才发现被占了,教师批假条和审批又全靠线下跑腿。那时候就有老师直接跟我抱怨:预约流程走了三天,实验课都上完了审批还没下来。这正是我下决心做一个“实验室预约管理小程序前后端完整”项目的直接原因。
再说直白一点,实验室预约系统的核心痛点其实就三件事:实验室状态透明可见、预约流程可追踪可审批、不同实验室的预约规则能灵活区分。我当时调研了好几个高校的实验室管理办法,发现基础实验室(比如通用机房、普通物理实验室)和专业实验室(比如高精密仪器室、危化品实验室、生物安全实验室)的预约逻辑完全不一样。前者频率高、周期短、审批简单,后者频率低但要求严格,通常需要实验员甚至系主任二次确认。如果一套逻辑走天下,不是把管理员累死,就是让有特殊要求的实验室形同虚设。所以我们在这个项目里把预约拆成两类:基础实验室预约和专业实验室预约,前者几乎全自动,后者走完整审批流。
这个项目做完以后能做什么,我简单梳理一下:学生端微信小程序可以直接查看实验室动态、阅读规章制度、提交预约申请;教师/实验员角色收到通知后在线审批;管理员在后台维护实验室基础数据、用户信息、规章制度,并能可视化查看实验室的使用情况。这就是一个完整的实验室预约管理小程序前后端闭环。
如果你正在做毕业设计,或者是在学校/小规模科研机构负责实验室信息化建设,又或者想学习“小程序 + Spring Boot + Vue3前后端分离”这套标准技术栈的实际落地,这篇文章的内容应该能帮你少走很多弯路。
注意:文章里的设计思路和核心代码片段都基于我实际做过的项目,可以直接参考复现。涉及具体业务参数(如预约时长上限、提前预约天数)不同学校规则不同,按自己情况调整即可。
1.2 项目整体范围与角色划分
动手之前,我建议大家先像这样梳理一遍角色和边界。实验室预约系统不是功能越多越好,而是要把每条业务线走通。在这个项目里我划了三种角色:
- 学生(普通用户):主要用微信小程序。能看实验室动态、查规章制度、提交预约申请、查看自己的预约记录、取消未审批的预约。
- 实验员/教师(审批角色):能收到预约申请通知,查看申请详情,通过或驳回预约,驳回时填写原因。可以管理自己负责的实验室信息。
- 系统管理员(后台管理):使用Vue3管理后台。负责用户管理(绑定角色、重置密码、禁用账号)、实验室管理(增删改查实验室、设置实验室类型)、预约审批(兜底处理所有异常单)、内容管理(发布实验室动态、编辑规章制度)。
角色的权限设计我后面会展开讲,这里先强调一个容易犯错的地方:前端的按钮级权限控制和后端接口鉴权必须同时做。只隐藏前端按钮不校验后端接口,等于把门锁贴在纸糊的墙上。我们这个项目里所有的预约修改、审核通过、用户删除等敏感操作,后端都会二次校验当前登录用户的角色,不是仅仅看一遍token就放行。
1.3 技术栈选型与分工
我把这个项目的技术选型放在前面说,因为这是大家最先关心的问题。整个项目按“小程序端 + 管理后台 + 后端服务 + 数据库”四块拆分:
| 端 | 技术栈 | 说明 |
|---|---|---|
| 用户端小程序 | uni-app / 微信小程序原生 | 我实际用的是uni-app,一套代码可以后续考虑编译到支付宝小程序,开发效率高 |
| 管理后台 | Vue3 + Element Plus + Vite | 管理员的Web操作界面,部署在服务器上通过浏览器访问 |
| 后端服务 | Spring Boot 2.7 + MyBatis-Plus | 标准Java后端,提供RESTful API给两端调用 |
| 数据库 | MySQL 8.0 + Redis | MySQL存业务数据,Redis存验证码、登录token(可选)和简单的热点数据缓存 |
| 部署 | Docker + Nginx | 前后端分离部署,Nginx做静态资源服务和反向代理 |
为什么选这套组合而不是全栈用Node.js或者Python Flask?我的理由很实际:实验室预约管理小程序整体属于典型的管理信息系统,核心是业务逻辑多、权限关系复杂、需要稳定的数据一致性。Spring Boot在这类场景下就是传统强项,生态完善,遇到问题很容易搜到答案。而且如果你是在校学生,用Spring Boot + Vue这套技术栈做项目,无论是毕业设计答辩还是找工作写简历,认可度都高一大截。
小程序端用uni-app还有一个隐藏好处:它内置了比较完善的组件库,日期时间选择、表单校验、图片上传这些功能都是现成的,能省下不少开发时间。本项目里比较高频用到的微信小程序单选框组件、日期选择器,uni-app里都有对应封装,不需要再自己去撸原生的picker逻辑。
2. 系统架构设计与数据库建模
2.1 前后端分离架构下的模块划分
整个项目的架构我按经典的“前端-后端-数据库”三层来组织,但细分下来可以拆成以下几个子模块,方便开发和后期维护:
- 预约核心模块:预约单的创建、取消、审批流、时间段冲突检测。
- 内容管理模块:实验室动态(类似公告新闻)和规章制度的发布、上下架、阅读统计。
- 用户与权限模块:登录认证(小程序用户的微信登录 + 管理后台的账号密码登录)、角色权限控制。
- 实验室资源模块:实验室基础信息的增删改查、实验室类型(基础/专业)分类、开放时间段设置。
- 统计与通知模块:预约记录的统计分析(后面可以扩展),以及审批状态变更时的站内通知/订阅消息。
如果用一个通俗的类比来解释这套结构:实验室资源相当于“房子”,预约模块相当于“租房合同”,审批流程是“中介审核”,用户模块是“门禁卡”,而内容管理模块就是大楼门口的“公告栏和住户手册”。五者缺一不可,各管一段。
2.2 数据库表结构设计:核心表和设计思路
这块是整个系统能不能稳定跑起来的基石。我第一版做得比较粗糙,把预约时间直接存成字符串,结果后面做冲突检测的时候痛苦不堪,后来老老实实改成标准时间字段。下面分享我最后沉淀下来的核心表结构,你可以直接参考。
用户表(sys_user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar | 账号(小程序用户为openid映射) |
| password | varchar | 密码(BCrypt加密) |
| real_name | varchar | 真实姓名 |
| role | tinyint | 1学生 2实验员 3管理员 |
| status | tinyint | 0禁用 1正常 |
| openid | varchar | 微信openid,小程序用户绑定 |
很多做实验室系统的朋友一开始只建一个user表,后面发现有的用户既是学生又要做实验室助理,单角色根本罩不住。我这里为了简单先用了单角色字段,如果你需要一个人多角色,建议做成user_role关联表,扩展性更好。
实验室表(lab_info)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| lab_name | varchar | 实验室名称 |
| lab_type | tinyint | 1基础实验室 2专业实验室 |
| location | varchar | 所在位置 |
| capacity | int | 容纳人数 |
| manager_id | bigint | 负责实验员id |
| open_start | time | 开放开始时间 |
| open_end | time | 开放结束时间 |
| status | tinyint | 0停用 1启用 |
这里最值得说的是lab_type字段。基础实验室和专业实验室的差异我是用这个字段区分,然后在预约服务层里针对不同类型走不同逻辑。基础实验室的预约单创建后自动进入“已通过”状态;专业实验室的预约单创建后进入“待审批”状态。以后如果新增第三种类型的实验室(比如考试专用实验室),只需要扩展type和审批策略,不用动表结构。
预约表(lab_reservation)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 预约人 |
| lab_id | bigint | 预约实验室 |
| reserve_date | date | 预约日期 |
| start_time | time | 开始时间 |
| end_time | time | 结束时间 |
| purpose | varchar | 预约用途 |
| status | tinyint | 0待审批 1已通过 2已拒绝 3已取消 4已完成 |
| audit_comment | varchar | 审批意见 |
| auditor_id | bigint | 审批人id |
| create_time | datetime | 创建时间 |
status字段其实就是整个预约流程的状态机。我在代码里定义了一个枚举类来处理状态流转,不要到处写魔法数字。
审批记录表(lab_approval_record)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| reservation_id | bigint | 关联预约单 |
| auditor_id | bigint | 审批人 |
| action | tinyint | 1通过 2驳回 |
| comment | varchar | 审批意见 |
| create_time | datetime | 审批时间 |
这张表是后来才加的。一开始我没做审批流水,结果管理员不记得自己当时为什么通过了一个奇怪的预约单,出了问题也没法追溯。加上审批记录表后,整个操作链路就清晰多了。这也是我特别想提醒大家的一点:凡是涉及审批的功能,一定要留审计痕迹。
**实验室动态表(lab_news)和规章制度表(lab_rule)**结构类似,都是标准的内容表:id、title、content、publisher_id、create_time、update_time、status。内容字段我用的是长文本类型,前端用富文本编辑器提交,小程序端用rich-text组件渲染。
2.3 时间冲突检测的核心逻辑
预约系统最容易出事故的就是时间冲突。实验员批了一张9点到11点的单,又有人预约10点到12点,两个单都显示通过,现场就炸了。这个问题的根源在于没有做数据库层面的交集判断。
冲突检测的SQL其实并不复杂。假设我们要插入一个新的预约单,需要判断该实验室在相同日期下,是否有“已通过”或“待审批”状态的预约单,且时间段与新预约有交集。两个时间段[newStart, newEnd]和[existStart, existEnd]有交集的判断条件是:
newStart < existEnd AND newEnd > existStart
这个条件覆盖了所有重叠情况:完全包含、部分重叠、首尾相接(首尾相接不算冲突,因为一个9点结束一个9点开始是可以的)。对应MyBatis-Plus的查询可以写成:
LambdaQueryWrapper<LabReservation> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(LabReservation::getLabId, labId) .eq(LabReservation::getReserveDate, reserveDate) .in(LabReservation::getStatus, Arrays.asList(0, 1)) .lt(LabReservation::getStartTime, endTime) .gt(LabReservation::getEndTime, startTime);用了lt和gt而不是le和ge,就是为了实现“首尾时间相等不算冲突”的规则。这个细节很多初学的人容易搞反,写成了大于等于/小于等于,结果明明有空档却提示冲突,被学生疯狂投诉。
但是,仅仅有查询判断还不够。如果两个学生同时提交预约,都执行了冲突检测,发现没有重叠,然后同时插入数据库,依然会产生冲突。这就要靠数据库层面的控制。我当时用了两种手段:第一,在预约表上建立唯一索引(lab_id + reserve_date + start_time),确保同一个实验室同一个日期的同一个开始时间只能有一条记录;第二,在插入预约前使用Redis分布式锁(也可以用select for update悲观锁)锁住实验室当天的时间段,保证串行化检测。
这里我的建议是:如果你的并发量不大(单实验室一天几百条预约以内),用数据库唯一索引加逻辑重试就够了,不需要上Redis。我实际开发时先加的唯一索引,后来演示时模拟并发压测发现偶尔会报重复键冲突,才在服务层加了锁。
注意:唯一索引加在
(lab_id, reserve_date, start_time)上,意味着同一实验室同一天同一个开始时间只能有一条预约记录。如果允许不同批次在同一开始时间预约不同时段,这个索引方案就不适用了,需要改为用应用层锁。具体取舍根据你的业务规则决定。
2.4 数据库设计阶段踩过的坑
这里整理几个我实际踩过的坑,希望能帮大家省点时间:
- 时间字段选用上,date和time分开存比datetime更灵活。预约通常只精确到某个日期的某个时间段,拆成reserve_date + start_time + end_time三个字段,做同一天排课查询非常方便,索引也好建。
- 不要用MySQL关键字做字段名。我第一版把预约表的“开始时间”字段命为
start,结果写SQL时各种报错,后来统一改成start_time才消停。 - 凡是金额、时长等字段,尽量用int或decimal,不要用float。比如预约时长可以用分钟数存,避免浮点误差。
- 删除数据用逻辑删除,不要物理删除。用户表、预约表都加了deleted字段,配合MyBatis-Plus的@TableLogic注解,查询自动过滤,既保留数据又能防止误删。
3. 核心功能模块实现详解
3.1 用户登录与权限控制
登录这块分了两个入口:小程序端使用微信登录(code换openid),管理后台使用账号密码登录。两套登录逻辑最终都会生成一个JWT token返回给前端,后续所有请求都在header里带Authorization字段。
小程序端登录流程如下:
- 前端调用
uni.login()获取code。 - 将code发送到后端
/api/auth/wx-login接口。 - 后端调用微信官方接口
jscode2session,用code换取openid和session_key。 - 如果openid在sys_user表中不存在,自动注册一个新账号(默认角色为学生);已存在则直接登录。
- 后端用openid生成JWT token返回给前端。
这个流程是最标准的做法,有个细节需要注意:不要在小程序端拿openid做登录凭证,直接用它当token。openid一旦泄露,任何人都能伪造你的身份调用接口。JWT本身可以设置过期时间,配合Redis做黑名单机制,安全性好得多。
后端权限控制我用Spring Boot的拦截器实现。定义注解@RequireRole(value = 2),标注在接口方法上,拦截器里解析token后判断当前用户角色是否满足要求。比如审批预约的接口就加了@RequireRole({2, 3}),学生角色无法调用。
public class RoleInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 1. 从header取token String token = request.getHeader("Authorization"); // 2. 解析token获得用户id和角色 LoginUser user = JwtUtil.parseToken(token); // 3. 判断当前请求接口是否要求特定角色 if (handler instanceof HandlerMethod) { RequireRole annotation = ((HandlerMethod) handler).getMethodAnnotation(RequireRole.class); if (annotation != null && !Arrays.asList(annotation.value()).contains(user.getRole())) { throw new BusinessException(403, "无权限访问"); } } return true; } }这个角色注解的实现让我在这个项目里省了大量重复的权限校验代码。一开始每个接口都手写if判断,改来改去容易漏,后来统一用拦截器加注解,所有接口的权限逻辑一目了然。
3.2 基础实验室预约流程:自动审批如何实现
基础实验室预约的典型场景是:学生要普通机房练个习,约个两小时;或者通识课要安排上机实验。这类需求量大、风险低,人工审批不现实,所以我设计成创建预约单后直接变为“已通过”状态。
具体流程:
- 学生选择基础实验室、选择日期、选择开始时间和结束时间(小程序端用日期时间选择器限制可选范围)。
- 提交预约时,后端先做实验室类型判断,确认lab_type = 1。
- 后端执行冲突检测(前面说过的时间交集查询)。
- 若无冲突,创建预约单并设置status = 1(已通过)。
- 返回预约成功信息,页面显示预约凭证(预约单号等)。
这里有一个业务细节:基础实验室虽然自动通过,但也不是无限制的。我加了“每个学生每天最多预约3个时段、每周最多预约10个时段”的限制。这套规则用数据库查询统计出来,写在了预约创建的校验逻辑里。实际运行下来效果不错,既能满足大多数人的使用需求,又防止了个别同学长期霸占实验室资源。
3.3 专业实验室预约流程:审批流怎么设计
专业实验室(比如贵重仪器室、需要特殊安全防护的实验室)的逻辑完全不同。这类实验室不仅要确定时间,还要审核使用人的资格和实验内容,所以必须走人工审批。
专业实验室预约流程:
- 学生选择专业实验室,填写更详细的预约信息,包括:实验内容、需要使用设备、预计人数、安全注意事项等。
- 后端检验实验室类型为lab_type = 2,创建预约单并设置status = 0(待审批)。
- 小程序端可以配置微信订阅消息,在学生提交申请后给负责该实验室的实验员发送审批提醒(用订阅消息的一次性模板)。
- 实验员登录查看待审批列表,点击“通过”或“驳回”。驳回必须填写原因,原因会显示在小程序端供学生查看。
- 审批通过后,学生可以在小程序看到“预约成功”的状态,并依预约时间到实验室使用。
- 实际使用完成后,实验员可手动标记“已完成”,或者系统根据预约的end_time到期后自动将状态置为“已完成”。
这个流程里我最想强调的是审批记录的留存。每一条通过/驳回操作都会写入lab_approval_record表,包含审批人、操作时间、审批意见。后期有任何争议,直接按预约单id查记录表就能还原当时的操作链路。这也是我在实验员培训时反复强调的一点:审批不只是点个按钮,而是在留证据。
3.4 实验室动态与规章制度模块的实现
这两个模块属于内容管理,虽然技术上不难,但直接决定了实验室的日常运营效率。我把它们做成了通用内容系统:
实验室动态:
- 管理员在Vue3后台发布动态,支持富文本编辑、图片上传。
- 小程序首页展示最新动态列表,点击可查看详情。
- 动态有置顶功能(通过sort字段实现),重要通知可以排在最前面。
- 新动态发布后可以顺便更新小程序首页的公告栏,让用户第一时间看到。
规章制度:
- 规章制度按分类管理(比如安全守则、设备操作规程、预约规则)。
- 小程序端提供列表页和搜索功能,用户可以通过关键词快速找到相关制度。
- 关键制度支持强制阅读弹窗:新用户注册后第一次进入小程序会弹出安全须知,点“已阅读”才能继续使用。这个设计在答辩时经常被老师夸,其实是参照了很多APP的用户协议设计。
实现上,这两个模块都复用了后台的通用CRUD逻辑,用MyBatis-Plus的BaseMapper快速搞定。富文本内容存到数据库后,小程序端用rich-text组件渲染,Vue后台用wangEditor或quill编辑器。我选的是wangEditor,轻量,中文文档全,后端接图片上传接口也容易。
3.5 用户管理模块如何兼顾权限与体验
后台用户管理是管理员的日常操作台,我集成了以下功能:
- 用户列表与查询:支持按用户名、真实姓名、角色、状态筛选。
- 新增用户:管理员手动创建账号。主要用于为没有微信的实验员/教师创建后台登录账号。
- 编辑用户:调整用户的真实姓名、角色、所属学院/系部(这里可以补一个department字段)。
- 重置密码:忘记密码时管理员可以重置,重置后默认密码为123456,用户首次登录强制修改。
- 禁用/启用账号:账号被禁用后,该用户无法登录,已有预约单保留但不可新增。
这里有个容易踩的坑:禁用用户时,要不要同时取消他未来的预约?我的处理是:不禁用已通过的预约,但禁止新建预约;如果用户多次违规,管理员可以手动将未来的预约批量取消。这个规则在开发文档里写清楚,避免管理员误操作引发纠纷。
4. 前后端交互与关键实现
4.1 小程序的API请求封装与状态管理
小程序端所有请求都通过统一的request工具发出去,我在工具里做了几件事:基础URL配置、token自动携带、统一错误处理、loading状态管理。
// utils/request.js const BASE_URL = 'https://api.example.com/api'; export function request({ url, method = 'GET', data = {} }) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': uni.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { // token过期,跳转登录页 uni.navigateTo({ url: '/pages/login/login' }); reject(res.data); } else { uni.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail: (err) => { uni.showToast({ title: '网络请求失败', icon: 'none' }); reject(err); } }); }); }BASE_URL如果是在本地开发,记得在小程序开发者工具里勾选“不校验合法域名”。部署上线时,必须把后端接口域名配置到小程序后台的request合法域名里,而且要HTTPS。这一步我当初卡了一下午,以为代码有问题,后来发现是域名没配置。
4.2 Vue3管理后台的核心页面实现
管理后台我用的是Vue3 + Element Plus。核心页面包括:
- 登录页面:账号密码登录,配合图片验证码(可以用后端生成,也可以用第三方库)。
- 仪表盘:展示今日预约数、待审批数、实验室使用率等统计数据。
- 预约管理页面:分“待审批”“已通过”“已驳回”“已完成”几个Tab,支持按实验室筛选、按日期范围筛选。
- 实验室管理页面:实验室列表,支持新增、编辑、删除、启用/停用。编辑时会用到表单校验,比如开放时间段结束不能早于开始。
- 用户管理页面:用户列表,支持搜索、新增、编辑角色、重置密码、禁用。
- 内容管理页面:实验室动态列表、规章制度管理,富文本编辑。
预约管理页面是整个后台的核心,我通过Tab切换不同的状态列表,“待审批”Tab下面直接显示审批操作按钮,点击打开详情抽屉,里面展示了预约人信息、预约时段、实验内容、实验室信息,审批人在同一个抽屉里填审批意见并点通过/驳回。整个操作流程控制在10秒以内,实验员使用体验很好。
4.3 前后端如何交接预约数据
前后端分离模式下,预约数据的交互格式需要提前统一。我定义了一套标准响应格式:
{ "code": 200, "msg": "操作成功", "data": {} }所有接口统一返回这个结构,前端根据code判断业务状态。data可以是对象、数组或分页数据。分页数据结构统一为:
{ "total": 100, "records": [] }这样约定好处是,前端request工具只需要写一次成功/失败处理逻辑,所有页面公用一套模式。当时和我一起开发前端的小伙伴说这是整个项目里最有价值的设计,因为不用每个接口都单独处理错误分支。
4.4 文件上传与富文本图片处理
实验室动态和规章制度里会大量使用图片,所以文件上传功能是必须的。我实现的是后端用本地存储,图片上传到服务器的/upload目录,通过Nginx映射为静态资源URL访问。简单直接,适合中小项目。
上传接口的实现思路:
- 前端用uni.uploadFile或Element Plus的el-upload组件把文件POST到
/api/upload。 - 后端接收MultipartFile,校验文件类型(jpg、png、gif等)和大小(限制5MB以内)。
- 文件重命名为uuid + 原始扩展名,保存到指定目录。
- 返回访问URL,前端拿到URL后插入富文本编辑器或直接展示。
要注意的是,如果服务器重启后上传的文件丢失,这个问题需要处理。我用了外部挂载目录(Docker volume),把上传目录持久化,这样容器重建后数据还在。部署部分是后面会详细说。
5. 项目部署与环境搭建实录
5.1 部署方案:Docker + Nginx前后端分离
这个项目最终要跑起来,最简单可靠的方案是用Docker Compose编排全套服务。我实际部署用的是阿里云2核4G的轻量服务器,系统Ubuntu 22.04,装了Docker和Docker Compose。
部署架构如下:
- Nginx容器:监听80/443端口,负责三件事。第一,托管Vue3构建出来的静态文件;第二,将
/api开头的请求反向代理到后端服务的Spring Boot容器;第三,代理/upload开头的图片访问请求到后端的静态资源目录。 - 后端容器:运行Spring Boot的JAR包,端口8080。
- MySQL容器:运行MySQL 8.0,数据目录挂载到宿主机。
- Redis容器(可选):如果预约并发冲突检测用到了Redis分布式锁,就需要启动这个容器。
Nginx配置片段长这样:
server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://backend: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; } # 上传文件访问 location /upload/ { alias /data/upload/; expires 30d; } }Docker Compose是我最喜欢的部署方式。相比手动在服务器上装MySQL、JDK、Nginx再配置环境变量,Compose文件写好后一条命令docker-compose up -d就能拉起整个系统,升级后端也简单:重新build新镜像,再docker-compose restart backend即可。
version: "3.8" services: mysql: image: mysql:8.0 container_name: lab-mysql environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: lab_management volumes: - ./mysql-data:/var/lib/mysql ports: - "3306:3306" backend: build: ./backend container_name: lab-backend depends_on: - mysql environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql DB_PORT: 3306 DB_NAME: lab_management DB_USERNAME: root DB_PASSWORD: root123456 ports: - "8080:8080" frontend: image: nginx:alpine container_name: lab-frontend depends_on: - backend volumes: - ./frontend/dist:/usr/share/nginx/html - ./nginx.conf:/etc/nginx/conf.d/default.conf - ./upload-data:/data/upload ports: - "80:80" - "443:443"5.2 生产环境MySQL初始化
数据库初始化我建议直接用SQL脚本执行,而不是用ORM自动建表。好处是表结构可以精确控制,方便后续增量修改。
初始化流程:
- 在服务器上创建MySQL容器。
- 执行项目根目录下
sql/init.sql脚本,创建库、表结构、初始管理员账号。 - 后续表结构变更,新增
sql/upgrade_v1.1.sql之类的增量脚本,不要直接改init.sql。
初始管理员账号我建议通过脚本插入,密码用BCrypt加密后的值,不要直接在SQL里写明文密码。比如用Spring Boot启动时的一个CommandLineRunner来检测,如果sys_user表里没有管理员,自动创建一个默认admin账号。这样即使换了环境,也能保证系统第一次启动就能登录后台。
5.3 小程序端如何连接HTTPS后端
微信小程序的要求是所有request请求的服务器域名必须是HTTPS,且已在小程序管理后台配置。所以在部署时,HTTPS证书是躲不开的。如果你有自己的域名,可以用免费证书工具申请(比如acme.sh),配合Nginx配置SSL。
一个简单的HTTPS配置示例:
server { listen 443 ssl; server_name your-domain.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend:8080; } }开发阶段可以不用HTTPS,在微信开发者工具里勾选“不校验合法域名”。但真机预览或者上线时就必须配置HTTPS了。这也是新手最常碰壁的环节,经常是小程序在开发者工具里一切正常,一到真机测试就白屏或者请求失败,大概率就是域名和HTTPS没配好。
5.4 Docker部署时容易踩的坑
- 容器间通信别用localhost。后端容器访问MySQL,要写成mysql而不是127.0.0.1。因为每个容器都有自己的网络命名空间,localhost指向的是容器自身。Compose里可以直接用服务名作为主机名。
- 数据卷一定要挂载。MySQL的数据目录和上传文件目录必须挂载到宿主机,否则容器删了数据就没。我之前演示时不小心执行了
docker-compose down,MySQL容器被删,所有用户和预约数据全部丢失,那个教训太深刻了。 - 修改前端代码后要重新构建镜像。Nginx容器里挂载的是dist构建产物,改完代码要重新
npm run build才能生效,不是改完源文件刷新浏览器就行。 - 端口冲突。服务器上已经有其他服务占用80/443时,Nginx端口要改,或者用不同端口加反向代理。
6. 常见问题与排查技巧实录
6.1 并发预约冲突问题
现象:两个学生同时提交同一个实验室同一个时间段的预约,都提示预约成功,但数据库里出现了两条记录。
排查思路:
- 检查冲突检测SQL是否有时间交集判断错误。
- 检查是否使用了数据库唯一索引。如果没有,必须加
(lab_id, reserve_date, start_time)唯一索引。 - 如果加了唯一索引,再检查服务层是否有事务管理。在MyBatis-Plus中,如果插入操作没有加
@Transactional,可能无法保证多个步骤之间的原子性。 - 如果并发量更高,可以在Redis里加分布式锁,锁的key设计为
lab:reserve:{labId}:{reserveDate}。
这个问题我印象太深了,当时是学生做并发测试,100个线程同时抢某一实验室9点的预约,结果进去了20多条,整个脸都绿了。最终解决方案是唯一索引打底,再用Redis分布式锁兜底,把并发插入串行化。
6.2 小程序端请求后端接口报错“未能完成操作”
现象:请求一直失败,开发者工具控制台提示“url not in domain list”。
原因:微信小程序/uni-app编译后,在开发者工具中默认校验request合法域名。如果你只是本地联调,请求地址是http://localhost:8080,需要勾选工具右上角“详情 - 本地设置 - 不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”。
解决方法:
- 本地开发:勾选不校验域名。
- 真机测试:必须将后端接口地址改为HTTPS,并在微信公众平台配置request合法域名。
6.3 管理员审批后小程序端状态未更新
现象:管理员在后台通过了预约申请,但小程序端一直显示“待审批”。
原因:前端页面没有自动刷新,需要重新进入页面或下拉刷新。这是状态管理的问题,不是后端逻辑有问题。
解决方法:
- 小程序页面在onShow生命周期里重新拉取列表数据。
- 或者使用uni.$emit和uni.$on做跨页面事件通知,审批页操作成功后通知列表页更新。
6.4 Docker部署后上传图片404
现象:部署后动态里上传的图片无法访问,控制台报404。
原因:Nginx容器里没有挂载上传目录,或者location配置和实际路径不对应。
解决方法:检查docker-compose.yml中./upload-data:/data/upload的路径映射,以及Nginx配置中location /upload/ { alias /data/upload/; }是否正确。注意alias后面的路径必须以/结尾,否则nginx会拼出奇怪的路径。
6.5 微信小程序基础库版本导致的日期组件兼容问题
现象:部分用户手机上日期时间选择器无法正常弹出。
排查思路:检查用户微信版本和基础库版本。uni-app的picker组件在比较老的基础库版本上可能存在兼容性问题。解决方法是调整app.json里的"libVersion",或者针对低版本用户降级处理,使用input输入代替picker组件。
这个坑在实际用户反馈中占比不低,后来我在代码里统一加了基础库版本判断,低于2.10.0的使用平台原生的日期选择逻辑,问题才消失。
7. 实操心得与后续扩展建议
做完这个项目,说实话最大的体会是:实验室预约管理系统并不难,难的是把业务规则想清楚,再用技术手段稳稳落地。尤其时间冲突检测、审批流设计、权限控制这三块,几乎占了整个项目的80%工作量。如果你现在也要做类似系统,我强烈建议先在纸上画出业务流程和状态机,不要一上来就写代码。状态流转图清楚以后,后端接口设计自然也就顺了。
关于开发顺序,我个人推荐按这个路径走:先搭数据库表结构和后端基础框架,然后做用户登录和权限,再做实验室CRUD和预约核心逻辑,之后是审批流和内容管理,最后做小程序端页面和管理后台。如果先把小程序页面做完再回头补后端接口,你会发现反复改接口格式的代价很大。
这个项目后续可以扩展的方向也很多。比如预约时间冲突提示做得更智能一些,用户选择时间段后前端立即展示哪些时间可用;可以接入实验室门禁系统,预约通过后自动生成二维码通行证;可以在后台增加使用率统计报表,按实验室、按日期维度分析资源利用率;还可以把微信订阅消息做完整,预约状态变更自动推送给用户。每一步扩展都是在现有架构上加功能,不需要推翻重来,这也是当初把模块边界划清楚的回报。
最后分享一个我在整个开发过程中最有价值的习惯:每次遇到bug,不要急着改代码,先把复现步骤、日志、现象记录下来,分析根因后再动手。这个项目里很多棘手问题都是靠日志定位解决的,比如并发冲突、审批状态不一致,都是先看调用链日志再定位到具体代码行。希望这篇文章能帮你少走弯路,欢迎在评论区交流你的实现方案和遇到的问题。
本文还有配套的精品资源,点击获取