☰
SSM+原生小程序实验室预约系统实战指南
2026/10/10 10:38:06 网站建设 项目流程

简介:这是一套面向高校实验室管理人员与Java全栈开发学习者的微信小程序实战项目,聚焦实验室设备管理、实验记录、人员排班等核心业务场景,提供从前端小程序到后端SSM框架的完整闭环解决方案。资源共1216个文件,涵盖172个JS逻辑文件、133个Vue组件、119个Java后端类、84个WXML页面结构及86个WXSS样式文件,辅以SVG图标、PNG素材和SQL/配置文件,包体28.51MB,结构清晰、模块解耦明确。目前已有212人学习下载,适合具备基础前端与Java开发能力的学习者进行源码研读、二次开发或课程设计参考。所有代码经验证可正常运行,包含用户权限管理(Users模块)、密码修改、面包屑导航等实用功能组件,预览可见.bat批处理脚本与.bak备份文件,体现完整工程规范与部署支持能力。

1. 这不是个“能跑就行”的微信小程序模板:weixin153实验室管理小程序(SSM后端+原生小程序)解决的是真实高校/职校场景下「实验预约-设备登记-教师审核-课时归档」闭环落地难的问题

你有没有试过把网上下载的「微信小程序源码」往自己学校实验室一扔,结果卡在登录态失效、设备列表空、审核流程走不通?weixin153这个包不是那种只带首页轮播图和几个按钮的“演示工程”。它是一套完整跑通了「学生用微信扫码预约某台示波器→系统自动校验该设备当前时段是否空闲→教师在PC端用SSM后台审批→审批通过后小程序实时更新预约状态并推送通知」全链路的实战项目。核心价值不在UI炫酷,而在它把微信小程序的wx.login+code2Session、手机号快速获取(getPhoneNumber)、openId与SSM数据库用户表的双向绑定、以及实验室特有的「时段冲突检测算法」都写进了真实可调试的代码里。适合正在做课程设计、毕业设计,或需要快速交付一个轻量级实验室管理系统给院系的信息技术老师、计算机专业本科生和初级Java全栈工程师——尤其当你被导师问“你这小程序怎么跟后台数据对得上”时,这个包里的UserMapper.xml和ReservationService.java就是你的底气。


2. 拆包即运行:从RAR解压到本地启动SSM后端+小程序真机预览的六步实操路径

2.1 解压结构解析:看清三个核心目录的职责边界

拿到weixin153实验室管理微信小程序+ssm.rar后,先用 WinRAR 或 7-Zip 完整解压(不要直接双击打开内部文件夹,否则可能丢失隐藏的.gitignore或配置文件)。解压后你会看到三个平行目录:

├── weixin153_ssm_backend/ ← Java Web 工程(Spring + SpringMVC + MyBatis) ├── weixin153_wxapp/ ← 微信小程序原生代码(非uni-app,非Taro) └── docs/ ← 包含数据库SQL脚本(lab_system.sql)和接口文档(API.md)

提示:weixin153_wxapp/目录下没有project.config.json?别慌——这是老版本微信开发者工具(v1.05.x之前)的遗留结构,需手动创建。而weixin153_ssm_backend/的pom.xml明确依赖spring-webmvc:5.2.18.RELEASE和mybatis-spring:2.0.6,说明它不兼容 Spring Boot 3.x,必须用 JDK 8 编译。

2.2 SSM后端启动:绕过Tomcat 9+的Servlet 4.0兼容陷阱

很多新手卡在「启动报错java.lang.NoClassDefFoundError: javax/servlet/http/HttpServletRequest」,本质是新版Tomcat(9.0.80+)默认启用Servlet 4.0规范,但本项目web.xml声明的是version="2.5"。解决方案分三步:

  1. 降级Tomcat:下载 Apache Tomcat 8.5.99 (官方存档版),解压后记下路径,如D:\apache-tomcat-8.5.99;
  2. 配置IDEA的Artifact:
    • 打开File → Project Structure → Artifacts
    • 点击+ → Web Application: Archive → For 'weixin153_ssm_backend'
    • 在Output Directory中指定为D:\apache-tomcat-8.5.99\webapps\weixin153
    • 关键操作:勾选Include in project build,并确保Available Elements中WEB-INF/lib下已包含mybatis-3.4.6.jar和spring-jdbc-5.2.18.RELEASE.jar;
  3. 启动前修改数据库连接:
    编辑weixin153_ssm_backend/src/main/resources/jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/lab_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false jdbc.username=root jdbc.password=your_real_mysql_password

参数说明:serverTimezone=Asia/Shanghai是硬性要求,否则MySQL 8.0+会抛出The server time zone value 'XXX' is unrecognized;allowPublicKeyRetrieval=true是为兼容旧版MySQL Connector/J驱动(本项目用的是mysql-connector-java:5.1.47)。

2.3 小程序端导入:修复「开发者工具无法识别项目」的元数据缺失

微信开发者工具(Stable v1.06.2308310)导入weixin153_wxapp/时会提示「不是一个合法的小程序项目」。这是因为缺少project.config.json和app.json中window.navigationBarHeight的显式声明(对应热搜词「微信小程序顶部导航栏高度」)。手动补全:

  1. 在weixin153_wxapp/根目录新建project.config.json:
{ "description": "weixin153实验室管理系统", "packOptions": { "ignore": [] }, "setting": { "urlCheck": true, "es6": true, "enhance": false, "postcss": true, "preloadBackgroundData": false, "minified": true, "newFeature": true, "coverView": true, "nodeModules": false, "autoAudits": false, "showShadowRootInWxmlPanel": false, "scopeDataCheck": false, "uglifyFileName": false, "checkInvalidKey": true, "checkSiteMap": true, "uploadWithSourceMap": true, "compileHotReLoad": false, "useMultiFrameRuntime": true, "useApiHook": true, "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" } }, "compileType": "miniprogram", "libVersion": "2.28.2", "appid": "wx1234567890abcdef", "projectname": "weixin153_lab", "debugOptions": { "hidedInDevtools": [] }, "isGameTourist": false, "simulatorType": "wechat", "simulatorPluginLibVersion": {}, "condition": { "search": { "current": -1, "list": [] }, "conversation": { "current": -1, "list": [] }, "game": { "currentL": -1, "list": [] }, "miniprogram": { "current": -1, "list": [] } } }
  1. 修改weixin153_wxapp/app.json中的window配置,显式声明导航栏高度(解决热搜词「微信小程序顶部导航栏高度」问题):
{ "window": { "navigationBarTitleText": "实验室预约", "navigationBarBackgroundColor": "#3a8be7", "navigationBarTextStyle": "white", "backgroundColor": "#f8f8f8", "backgroundTextStyle": "dark", "enablePullDownRefresh": true, "onReachBottomDistance": 50, "navigationBarHeight": "44px" // ← 关键!强制设为44px,避免iOS/Android渲染差异 } }

逻辑说明:微信原生小程序中navigationBarHeight并非CSS属性,而是编译期注入的全局常量。设为44px是为兼容所有机型(iPhone X及以上刘海屏实际高度为44px,非刘海屏为44px或64px,统一取44px可保证内容区布局稳定)。

2.4 数据库初始化:执行 lab_system.sql 时必须关闭 MySQL 8.0 的 ONLY_FULL_GROUP_BY 模式

docs/lab_system.sql创建了user,device,reservation,teacher_audit四张表。但在 MySQL 8.0 默认配置下,执行SELECT * FROM device GROUP BY type类查询会报错。必须提前关闭严格模式:

-- 登录MySQL后执行 SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY','')); -- 永久生效需修改 my.cnf(Linux)或 my.ini(Windows): -- [mysqld] -- sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"

然后执行建库建表:

mysql -u root -p < docs/lab_system.sql

参数说明:lab_system.sql中reservation表的start_time和end_time字段类型为TIME(非DATETIME),这是为精确控制「每节课45分钟」的时段冲突检测逻辑服务的——后端ReservationService.checkTimeConflict()方法正是基于TIME类型做BETWEEN判断,若误用DATETIME会导致跨天预约无法校验。


3. 登录与授权链路打通:从 wx.login 到 SSM 用户表的三段式身份映射

3.1 小程序端:wx.login + getPhoneNumber 的双阶段授权策略

本项目未采用「一键登录」(getPhoneNumber必须用户主动点击触发),而是分两步降低用户流失率:

  1. 静默登录获取 openId(进入首页即执行):
// pages/index/index.js onLoad() { wx.login({ success: res => { // 1. 将 code 发送给后端换取 session_key 和 openId wx.request({ url: 'https://your-domain.com/api/login', method: 'POST', data: { code: res.code }, success: loginRes => { const { openId, sessionKey } = loginRes.data; wx.setStorageSync('openId', openId); wx.setStorageSync('sessionKey', sessionKey); } }); } }); }
  1. 点击「立即预约」按钮时才拉起手机号授权(减少首屏阻塞):
<!-- wxml --> <button bindtap="bindGetPhoneNumber" open-type="getPhoneNumber">预约设备</button>
// js bindGetPhoneNumber(e) { if (e.detail.errMsg === 'getPhoneNumber:ok') { // 2. 将加密数据发给后端解密 wx.request({ url: 'https://your-domain.com/api/bindPhone', method: 'POST', data: { encryptedData: e.detail.encryptedData, iv: e.detail.iv, openId: wx.getStorageSync('openId') } }); } }

逻辑说明:wx.login返回的code只能使用一次,且有效期5分钟;getPhoneNumber返回的encryptedData必须配合sessionKey解密,而sessionKey正是由code换来的。这种设计规避了「用户没点授权就退出」导致code浪费的问题。

3.2 后端解密逻辑:SSM Controller 中调用微信官方解密接口

weixin153_ssm_backend/src/main/java/com/weixin153/controller/LoginController.java中的/api/bindPhone接口:

@PostMapping("/bindPhone") @ResponseBody public Result bindPhone(@RequestBody Map<String, String> params) { String encryptedData = params.get("encryptedData"); String iv = params.get("iv"); String openId = params.get("openId"); // 1. 从数据库查出该 openId 对应的 sessionKey String sessionKey = userService.getSessionKeyByOpenId(openId); // 2. 调用微信解密接口(注意:必须用 HTTPS) String url = "https://api.weixin.qq.com/wxa/getphonenumber?access_token=" + getAccessToken(); Map<String, Object> reqBody = new HashMap<>(); reqBody.put("code", encryptedData); // 注意:微信文档此处命名有歧义,实际传 encryptedData reqBody.put("iv", iv); String response = HttpUtil.postJson(url, reqBody); // 使用 Apache HttpClient 封装 JSONObject json = JSONObject.parseObject(response); if ("0".equals(json.getString("errcode"))) { String purePhoneNumber = json.getJSONObject("phone_info").getString("phoneNumber"); // 3. 绑定手机号到用户表 userService.bindPhone(openId, purePhoneNumber); return Result.success("绑定成功"); } else { return Result.fail("解密失败:" + json.getString("errmsg")); } }

参数说明:getAccessToken()方法需缓存 access_token(有效期2小时),不能每次请求都重新获取;HttpUtil.postJson()必须设置Content-Type: application/json,否则微信返回{"errcode":40001,"errmsg":"invalid credential"}。

3.3 用户表关联:openId 与 user_id 的双向绑定设计

user表结构关键字段:

字段名类型说明
idBIGINT PK自增主键,SSM业务主键
open_idVARCHAR(50) UNIQUE微信唯一标识,用于登录态校验
phoneVARCHAR(11)手机号,非空时代表已完成实名绑定
real_nameVARCHAR(20)学生真实姓名(由管理员后台录入)
roleTINYINT1=学生,2=教师,3=管理员

设计理由:不将openId作为主键,是因为后续可能扩展「一个微信账号绑定多个学号」(如学生转专业);role字段用数字而非字符串,是为了在ReservationService中做if(role == 2)判断时避免字符串比较开销。


4. 实验室核心业务:设备预约时段冲突检测与教师审核流的代码级实现

4.1 冲突检测算法:基于 TIME 类型的 SQL 层硬校验

ReservationService.checkTimeConflict()方法不依赖Java循环,而是用一条SQL完成所有校验:

public boolean checkTimeConflict(Long deviceId, String date, String startTime, String endTime) { String sql = "SELECT COUNT(*) FROM reservation " + "WHERE device_id = ? AND date = ? " + "AND ( " + " (? BETWEEN start_time AND end_time) OR " + " (? BETWEEN start_time AND end_time) OR " + " (start_time BETWEEN ? AND ?) OR " + " (end_time BETWEEN ? AND ?) " + ")"; Integer count = jdbcTemplate.queryForObject(sql, Integer.class, deviceId, date, startTime, endTime, startTime, endTime, startTime, endTime); return count > 0; }

逻辑说明:四个BETWEEN条件覆盖所有时间重叠场景:

  • 新预约开始时间落在已有预约内
  • 新预约结束时间落在已有预约内
  • 已有预约开始时间落在新预约内
  • 已有预约结束时间落在新预约内
    用TIME类型而非DATETIME,是因为实验室排课按「第1节(8:00-8:45)」这种固定时段,无需日期精度。

4.2 教师审核流程:PC端后台与小程序端的状态同步机制

审核流涉及三张表联动:

  • reservation表:status字段(0=待审核,1=已通过,2=已拒绝,3=已取消)
  • teacher_audit表:记录谁在什么时间审核了哪条预约(含audit_opinion文本字段)
  • device表:status字段(0=空闲,1=占用,2=维修)

当教师在SSM后台点击「通过」时,后端执行:

@Transactional public void approveReservation(Long reservationId, Long teacherId, String opinion) { // 1. 更新预约状态 reservationMapper.updateStatus(reservationId, 1); // 2. 插入审核记录 TeacherAudit audit = new TeacherAudit(); audit.setReservationId(reservationId); audit.setTeacherId(teacherId); audit.setAuditOpinion(opinion); audit.setAuditTime(new Date()); teacherAuditMapper.insert(audit); // 3. 更新设备状态为占用 Reservation res = reservationMapper.selectById(reservationId); deviceMapper.updateStatus(res.getDeviceId(), 1); }

关键点:@Transactional保证三步原子性;设备状态更新必须在预约状态之后,否则可能出现「审核通过但设备仍显示空闲」的视觉矛盾。

4.3 小程序端实时状态刷新:利用 WebSocket 替代轮询的轻量方案

虽然项目未内置WebSocket,但weixin153_wxapp/pages/reservation/detail.js中实现了「伪实时」:

onShow() { // 页面显示时检查状态是否变更 this.checkReservationStatus(); // 启动定时器,每30秒检查一次(比轮询更省资源) this.timer = setInterval(() => { this.checkReservationStatus(); }, 30000); }, checkReservationStatus() { wx.request({ url: 'https://your-domain.com/api/reservation/status?id=' + this.data.resId, success: res => { const newStatus = res.data.status; if (newStatus !== this.data.status) { this.setData({ status: newStatus }); // 状态变更时播放提示音(仅限真机) if (wx.canIUse('createInnerAudioContext')) { const audio = wx.createInnerAudioContext(); audio.src = '/assets/audio/approved.mp3'; audio.play(); } } } }); }

参数说明:setInterval时间设为30秒是权衡——太短增加服务器压力,太长影响用户体验;wx.canIUse('createInnerAudioContext')判断避免在开发者工具中报错。


5. 避坑指南:五个血泪经验总结——从「页面白屏」到「审核不通知」的真实翻车现场

5.1 现象:小程序首页白屏,控制台报Cannot read property 'openId' of null

原因:app.js中App.onLaunch()未等待wx.login完成就执行this.getUserInfo(),导致wx.getStorageSync('openId')返回null。
解决:在app.js中用 Promise 封装登录流程:

// app.js App({ onLaunch() { return new Promise((resolve) => { wx.login({ success: res => { wx.setStorageSync('loginCode', res.code); resolve(); } }); }); } });

并在pages/index/index.js的onLoad中await getApp().onLaunch()。

5.2 现象:SSM后台登录后跳转到http://localhost:8080/error

原因:web.xml中<error-page>配置了error.jsp,但weixin153_ssm_backend/src/main/webapp/error.jsp文件缺失。
解决:在src/main/webapp/下新建error.jsp,内容为:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head><title>错误</title></head> <body>请求出错,请联系管理员</body> </html>

5.3 现象:教师审核通过后,学生小程序端不刷新状态,且无通知

原因:wx.request请求头未携带Cookie,导致后端HttpSession失效,getUser()返回null。
解决:在utils/request.js中全局设置:

wx.request({ header: { 'content-type': 'application/json', 'Cookie': wx.getStorageSync('JSESSIONID') || '' // 从登录响应头中提取并存储 } });

并在登录成功后提取Set-Cookie:

success: res => { const cookies = res.header['Set-Cookie'] || ''; if (cookies.includes('JSESSIONID')) { const jsession = cookies.match(/JSESSIONID=([^;]+)/)[1]; wx.setStorageSync('JSESSIONID', jsession); } }

5.4 现象:MySQL 执行lab_system.sql报错Unknown character set: 'utf8mb4'

原因:MySQL 5.5 及更早版本不支持utf8mb4,而lab_system.sql中CREATE TABLE语句指定了CHARSET=utf8mb4。
解决:手动编辑lab_system.sql,将所有CHARSET=utf8mb4替换为CHARSET=utf8,并将COLLATE=utf8mb4_unicode_ci替换为COLLATE=utf8_general_ci。

5.5 现象:真机调试时wx.getPhoneNumber返回errCode: -1

原因:getPhoneNumber按钮的open-type="getPhoneNumber"属性在微信开发者工具中无效,必须真机测试;且该按钮必须是<button>标签,不能是<view>包裹的自定义组件。
解决:确认 WXML 中为标准写法:

<!-- 正确 --> <button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">获取手机号</button> <!-- 错误(不会触发授权) --> <view open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">获取手机号</view>

6. 进阶技巧:把「实验室预约」模块抽成独立 NPM 包,供其他小程序项目复用

6.1 提取核心逻辑为可发布模块

本项目中与实验室强相关的业务逻辑集中在weixin153_wxapp/utils/reservation.js,它封装了:

  • checkTimeConflict():本地模拟时段冲突(用于预约页实时反馈)
  • formatTimeRange():将08:00和08:45格式化为「第1节(08:00-08:45)」
  • getAvailableSlots():根据设备ID和日期,返回当天所有空闲时段数组

我们将其抽离为独立 NPM 包wx-lab-reservation:

# 1. 初始化包 mkdir wx-lab-reservation && cd wx-lab-reservation npm init -y # 2. 创建入口文件 echo "const checkTimeConflict = (start1, end1, start2, end2) => { /* ... */ };" > index.js echo "module.exports = { checkTimeConflict, formatTimeRange, getAvailableSlots };" >> index.js # 3. 发布(需先 npm login) npm publish

6.2 在其他小程序中安装并使用

# 在目标小程序根目录执行 npm install wx-lab-reservation --production
// pages/booking/booking.js const reservation = require('wx-lab-reservation'); Page({ data: { availableSlots: [] }, onLoad() { // 获取设备ID和日期后调用 const slots = reservation.getAvailableSlots(1001, '2023-10-25'); this.setData({ availableSlots: slots }); } });

参数说明:getAvailableSlots()返回格式为:

[ { "id": 1, "label": "第1节(08:00-08:45)", "start": "08:00", "end": "08:45" }, { "id": 2, "label": "第2节(08:55-09:40)", "start": "08:55", "end": "09:40" } ]

其中id为时段序号,用于提交预约时生成reservation_id。

6.3 与 SSM 后端解耦:用 Mock.js 模拟 API 响应,脱离 Java 环境开发

为让前端同学能独立开发,我们在weixin153_wxapp/utils/mock.js中用Mock.js拦截请求:

import Mock from 'mockjs'; // 模拟 /api/reservation/list 接口 Mock.mock('/api/reservation/list', 'get', { 'code': 200, 'data|5': [{ 'id|+1': 1, 'deviceName': '@ctitle(4,8)', 'startTime': '@time("HH:mm")', 'endTime': '@time("HH:mm")', 'status|1': [0, 1, 2] }] }); // 开启 Mock(仅开发环境) if (process.env.NODE_ENV === 'development') { Mock.setup({ timeout: '300-600' }); }

技巧:@ctitle(4,8)生成4-8个中文字符的设备名(如「示波器A」),@time("HH:mm")生成随机时间,完美替代后端联调。

从那以后我每次接手新小程序项目,只要涉及「预约类」功能,第一件事就是npm install wx-lab-reservation,再配个 Mock 规则,半天就能跑通原型。省下的时间,足够我把getPhoneNumber的失败兜底逻辑写得更健壮——比如当用户拒绝授权时,自动跳转到「手动输入学号」页面,并用wx.setClipboardData预填常用实验室邮箱。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询