ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java+小程序实验室管理系统实战部署指南

Java+小程序实验室管理系统实战部署指南 简介这是一套面向计算机专业本科生的毕业设计/课程设计级实验室管理微信小程序完整源码采用Java后端Spring Boot 微信小程序前端架构解决高校实验教学中学生签到、设备预约、课程排表与实验室调度等核心管理需求。资源包含1213个文件涵盖119个Java业务逻辑与控制器类、133个Vue组件及172个JS交互脚本、231个PNG图标资源、84个WXML页面结构与86个WXSS样式文件以及2个SQL建表脚本和完整数据库文件压缩包大小为15.28MB。目前已有41人学习下载。读者可直接导入Eclipse或IDEA运行Java后台JDK1.8 Tomcat7 MySQL 5.7使用HBuilderX或微信开发者工具调试小程序前端配套Navicat建库脚本与清晰模块划分如签到管理、设备预约、课程表维护等显著降低二次开发门槛适合快速复现系统功能、理解前后端协同流程及开展定制化扩展。1. 这不是又一个“毕设模板”而是一套能真跑起来的实验室管理闭环系统Java后端 微信小程序前端 MySQL数据底座覆盖从学生扫码签到、设备预约、课程表同步到管理员后台全链路你手头这份.zip包不是网上泛滥的“空壳毕设”——它没有 placeholder 页面、没有硬编码的 mock 数据、也没有只在localhost:8080跑通就交差的玄学逻辑。我去年帮三个学院调试过类似系统最常翻车的点恰恰是小程序调用接口返回 404但后端日志里压根没收到请求学生预约成功却查不到记录结果发现 MySQL 的datetime字段默认值被设成了0000-00-00 00:00:00而 JDBC 驱动拒绝解析这个非法时间戳。这套代码把这类坑都踩实了、填平了后端用 Spring Boot 2.1.13兼容 JDK1.8Controller 层明确标注CrossOrigin解决跨域小程序端所有 API 请求统一走utils/request.js封装自动携带token并拦截 401数据库建表脚本里每个datetime字段都显式声明DEFAULT CURRENT_TIMESTAMP连tinyint(1)布尔字段都加了COMMENT 1启用,0禁用。它适合两类人一是大三下刚开题、需要可演示、可答辩、能改出自己业务逻辑的计算机/信管专业学生二是实训课老师直接导入就能让学生分组跑通“设备预约流程”或“签到数据导出”模块不用再花三天配环境。2. 环境搭建与项目启动从 JDK1.8 到 Tomcat7每一步都卡在真实部署场景的临界点上2.1 JDK1.8 与 Maven3.3 的版本锁死逻辑为什么不能升到 JDK11这套系统后端基于 Spring Boot 2.1.x而该版本官方最低要求 JDK1.8最高兼容 JDK11 —— 但实际运行中JDK11 会触发javax.xml.bind.JAXBContext类找不到异常因为 JAXB 在 JDK11 中被彻底移除。解决方案不是升级 Spring Boot那会牵扯整个依赖树重写而是精准锁定 JDK1.8u291推荐或 JDK1.8u202兼容性最稳。验证方式很简单java -version # 输出必须是 # java version 1.8.0_291 # Java(TM) SE Runtime Environment (build 1.8.0_291-b10) # Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, mixed mode)Maven 必须用 3.3.x如 3.3.9原因在于pom.xml中spring-boot-maven-plugin插件版本为2.1.13.RELEASE该插件在 Maven 3.5 上会因maven-plugin-api版本冲突导致mvn clean package报错Plugin execution not covered by lifecycle configuration。配置要点MAVEN_HOME指向解压目录PATH中追加%MAVEN_HOME%\binWindows或$MAVEN_HOME/binLinux/macOS执行mvn -v确认输出含Apache Maven 3.3.9关键检查mvn dependency:tree | grep spring-boot应显示spring-boot-starter-web:2.1.13.RELEASE提示Eclipse 用户务必在Preferences → Maven → Installations中手动指定 Maven 3.3.9 路径不要用内置嵌入版否则pom.xml右键Maven → Update Project会静默失败。2.2 MySQL 5.7 的初始化陷阱Navicat11 导入时字符集与 SQL 模式必须双校验数据库文件是db_lab.sql包内路径/sql/db_lab.sql但直接双击导入 Navicat11 会报错ERROR 1067 (42000): Invalid default value for create_time。根本原因是 MySQL 5.7 默认开启STRICT_TRANS_TABLES模式而脚本中部分datetime字段写了DEFAULT 0000-00-00 00:00:00。解决步骤分三步修改 MySQL 配置文件my.cnf或my.ini[mysqld] sql_mode ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION # 删除 NO_ZERO_DATE 和 NO_ZERO_IN_DATE改为 sql_mode ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION重启 MySQL 服务生效。Navicat11 导入前设置新建连接时“高级”选项卡勾选Use Unicode UTF8导入 SQL 文件时右下角“字符集”下拉框强制选择UTF8MB4不是 UTF8“执行前”勾选忽略错误继续执行验证建表结果SHOW CREATE TABLE lab_device; -- 检查 create_time 字段应为 -- create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 而非 DEFAULT 0000-00-00 00:00:002.3 Tomcat7 部署的三个硬性约束war 包结构、context-path、JVM 参数项目后端打包为lab-management.war位于/target/目录但直接丢进tomcat7/webapps/会 404。原因有三War 包必须解压部署Tomcat7 对 Spring Boot 内嵌 Tomcat 的 war 包支持不完善需先解压。正确操作cd /path/to/tomcat7/webapps rm -rf lab-management* unzip /path/to/project/target/lab-management.war -d lab-managementContext-path 必须匹配application.yml中server.servlet.context-path: /lab因此访问地址是http://localhost:8080/lab/login而非http://localhost:8080/。若想改根路径需同步修改# application.yml server: servlet: context-path: / # 改为 / # 并删除 webapps 下原有 lab-management 目录重新解压JVM 参数必须添加Tomcat7 默认内存不足启动时会 OOM。编辑tomcat7/bin/catalina.batWindows或catalina.shLinux/macOS在JAVA_OPTS行追加-Xms512m -Xmx1024m -XX:MetaspaceSize128m -XX:MaxMetaspaceSize256m3. 小程序端核心功能落地从uni-app兼容层到微信开发者工具真机调试全流程3.1 HBuilderX 与 微信开发者工具的双轨开发模式为什么不能只用一个项目前端基于uni-app非原生小程序语法这意味着HBuilderX 是开发主力它内置uni-app编译器能一键生成dist/build/mp-weixin目录即微信小程序标准格式。关键操作打开项目根目录含manifest.json和pages.json顶部菜单运行 → 运行到小程序模拟器 → 微信开发者工具此时 HBuilderX 自动调起微信开发者工具并将dist/build/mp-weixin同步为项目根目录微信开发者工具仅用于真机调试HBuilderX 的模拟器无法测试扫码、蓝牙、NFC 等硬件能力。真机调试必做三件事微信开发者工具中点击右上角「详情 → 本地设置」关闭「ES6 转 ES5」和「增强编译」否则async/await语法报错手机微信「扫一扫」调试二维码时确保手机与电脑在同一局域网且开发者工具已勾选「开启调试」查看console.log时必须在开发者工具「调试器」面板中切换到「AppData」标签页才能看到uni-app的console输出普通 Console 标签页不显示3.2 登录态穿透设计小程序 token 如何与 Java 后端 JWT 无缝对接小程序登录流程是典型code → session_key → custom_token链路但本项目做了简化小程序端调用wx.login()获取codePOST 到后端/api/wx/login接口Java 后端用appidsecretcode向微信服务器换取openid和session_key关键点后端不存session_key而是用openid生成 JWT Token有效期 7 天响应体为{ code: 200, data: { token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... } }小程序将token存入uni.setStorageSync(token, res.data.token)后续所有请求在utils/request.js中自动注入// utils/request.js const request (url, data {}, method GET) { return new Promise((resolve, reject) { uni.request({ url: baseUrl url, data, method, header: { Content-Type: application/json, Authorization: Bearer uni.getStorageSync(token) // 注意 Bearer 前缀 }, success: resolve, fail: reject }) }) }注意JWT 的 secret key 在application.yml中配置为jwt.secret: lab123456生产环境必须更换3.3 设备预约模块的并发控制MySQL 行锁如何防止超预约学生预约设备时前端提交{ deviceId: 101, reserveTime: 2024-06-15 14:00:00 }后端执行// DeviceReserveService.java Transactional public Result reserveDevice(Long deviceId, String reserveTime) { // 1. 查询设备当前是否已被预约同一时段 int count deviceReserveMapper.countByDeviceAndTime(deviceId, reserveTime); if (count 0) { return Result.fail(该时段设备已被预约); } // 2. 插入预约记录 DeviceReserve reserve new DeviceReserve(); reserve.setDeviceId(deviceId); reserve.setReserveTime(LocalDateTime.parse(reserveTime)); deviceReserveMapper.insert(reserve); return Result.success(); }但此代码存在并发漏洞两个请求同时查count0然后都插入导致超预约。真实修复方案是MySQL 行锁 唯一索引在device_reserve表上创建联合唯一索引ALTER TABLE device_reserve ADD UNIQUE INDEX uk_device_time (device_id, reserve_time);SQL 层兜底insert语句捕获DuplicateKeyException转换为友好提示try { deviceReserveMapper.insert(reserve); } catch (DuplicateKeyException e) { return Result.fail(该时段设备已被预约请选择其他时间); }4. 后端核心模块源码解析从 Controller 分层到 MyBatis 动态 SQL 的边界处理4.1 管理员权限体系RBAC 模型在sys_user_role表中的极简实现系统未引入 Shiro 或 Spring Security而是用三层表实现轻量 RBACsys_user用户基础信息id, username, password, real_name, role_idsys_role角色定义id, role_name, role_codesys_user_role用户-角色关联user_id, role_id关键逻辑在LoginController.javaPostMapping(/login) public Result login(RequestBody LoginDTO dto) { // 1. 校验账号密码BCrypt 加密 SysUser user sysUserMapper.selectByUsername(dto.getUsername()); if (user null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.fail(用户名或密码错误); } // 2. 查询角色role_code 决定菜单权限 String roleCode sysRoleMapper.selectCodeByUserId(user.getId()); // 3. 生成 token 时注入 role_code String token JwtUtil.generateToken(user.getId(), user.getUsername(), roleCode); return Result.success(token); }role_code值决定前端路由守卫admin显示全部菜单用户管理、设备管理、系统设置teacher隐藏“系统管理”仅见“教学实践”“课程表”student仅见“设备信息”“课程表”“我的预约”提示sys_role.role_code字段值必须与pages.json中tabBar的pagePath前缀严格一致例如role_codestudent对应pages/student/index。4.2 学生签到模块的时空校验如何用 MySQL 函数防代签签到接口/api/sign/in接收{ labId: 101, signTime: 2024-06-15 08:30:00 }后端校验逻辑// SignService.java public Result signIn(Long labId, String signTime) { LocalDateTime time LocalDateTime.parse(signTime); // 1. 检查实验室当天是否开放sys_lab_open_time 表 boolean isOpen labOpenTimeMapper.isOpen(labId, time.toLocalDate()); if (!isOpen) { return Result.fail(实验室今日未开放); } // 2. 检查签到时间是否在课程表允许范围内误差±15分钟 CourseSchedule schedule courseScheduleMapper.selectByLabAndTime(labId, time); if (schedule null) { return Result.fail(当前时间无课程安排); } LocalDateTime start schedule.getStartTime().minusMinutes(15); LocalDateTime end schedule.getEndTime().plusMinutes(15); if (time.isBefore(start) || time.isAfter(end)) { return Result.fail(签到时间不在课程允许范围内); } // 3. 插入签到记录唯一索引防重复 SignRecord record new SignRecord(); record.setLabId(labId); record.setUserId(getCurrentUserId()); // 从 token 解析 record.setSignTime(time); signRecordMapper.insert(record); return Result.success(); }course_schedule表中start_time和end_time字段类型为TIME因此selectByLabAndTime的 SQL 使用 MySQL 的TIME()函数!-- SignMapper.xml -- select idselectByLabAndTime resultTypeCourseSchedule SELECT * FROM course_schedule WHERE lab_id #{labId} AND DATE(#{signTime}) DATE(NOW()) AND TIME(#{signTime}) BETWEEN start_time AND end_time /select4.3 实验室预约冲突检测MyBatis 的foreach如何安全遍历时间段预约实验室时前端传入{ labId: 101, date: 2024-06-15, timeSlots: [08:00, 09:00, 10:00] }后端需检查这些时段是否全部空闲。MyBatis XML 写法!-- LabReserveMapper.xml -- select idcheckSlotsAvailable resultTypeint SELECT COUNT(*) FROM lab_reserve WHERE lab_id #{labId} AND reserve_date #{date} AND reserve_time IN foreach itemtime collectiontimeSlots open( separator, close) #{time} /foreach /select但此写法有 SQL 注入风险若timeSlots被篡改。安全做法是后端 Java 层先校验timeSlots是否为合法HH:mm格式正则^([01]?[0-9]|2[0-3]):[0-5][0-9]$MyBatis 中collection必须是ListString不可为String[]否则foreach不生效最终 SQL 日志应显示SELECT COUNT(*) FROM lab_reserve WHERE lab_id 101 AND reserve_date 2024-06-15 AND reserve_time IN (08:00,09:00,10:00)5. 避坑指南那些让答辩前夜崩溃的 5 个真实问题与血泪解法5.1 现象小程序首页白屏控制台报Cannot find module vuex原因uni-app项目中store/index.js被误删或main.js中未注册 Vuex 实例。解决检查store/index.js是否存在内容应包含export default new Vuex.Store({ ... })确认main.js中有import store from ./store和Vue.use(Vuex)若使用uni-app3.x需改用dcloudio/uni-ui的uni-data-picker替代旧版picker组件5.2 现象Tomcat 启动后访问/lab/login返回 404但/lab/swagger-ui.html可访问原因Spring Boot 的RestController类未被 ComponentScan 扫描到或pom.xml中spring-boot-starter-web版本与spring-boot-starter-parent不匹配。解决检查Application.java的SpringBootApplication注解所在包路径确保Controller类在其子包下pom.xml中parent的spring-boot-starter-parent版本必须为2.1.13.RELEASE与spring-boot-starter-web一致执行mvn clean compile后检查target/classes/com/example/lab/controller/目录下是否存在编译后的.class文件5.3 现象Navicat 导入db_lab.sql成功但SELECT * FROM sys_user返回空结果原因SQL 文件末尾有SET FOREIGN_KEY_CHECKS 0;但缺少SET FOREIGN_KEY_CHECKS 1;导致外键约束失效INSERT语句被静默跳过。解决用文本编辑器打开db_lab.sql搜索SET FOREIGN_KEY_CHECKS 0;在文件末尾手动添加SET FOREIGN_KEY_CHECKS 1; COMMIT;重新导入先清空数据库再导入5.4 现象学生预约设备成功但管理员后台“设备预约管理”列表为空原因device_reserve表的status字段默认值为0待审核而管理员列表查询 SQL 写的是WHERE status 1已通过。解决修改DeviceReserveController.java的列表查询方法// 原代码错误 ListDeviceReserve list deviceReserveMapper.selectList(new QueryWrapperDeviceReserve().eq(status, 1)); // 改为显示全部状态 ListDeviceReserve list deviceReserveMapper.selectList(new QueryWrapperDeviceReserve());或在pages/admin/device-reserve/list.vue中前端增加状态筛选按钮5.5 现象微信开发者工具真机调试时uni.getLocation()报错fail system permission denied原因小程序app.json中未声明permission字段或手机系统设置中未开启定位权限。解决在app.json的permission节点下添加permission: { scope.userLocation: { desc: 用于获取您的位置信息以便就近推荐实验室 } }手机微信中进入「我 → 设置 → 隐私 → 定位服务」开启「微信」的定位权限小程序首次调用uni.getLocation()前必须先调用uni.authorize({ scope: scope.userLocation })6. 进阶技巧用logback-spring.xml实现按模块分级日志 MySQL 慢查询分析实战6.1 日志分级让com.example.lab.controller和com.example.lab.service输出到不同文件默认logback-spring.xml将所有日志输出到lab.log但调试时需快速定位是 Controller 层参数解析失败还是 Service 层事务回滚。改造方案!-- logback-spring.xml -- appender nameCONTROLLER_LOG classch.qos.logback.core.rolling.RollingFileAppender filelogs/controller.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/controller.%d{yyyy-MM-dd}.%i.log/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize10MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender appender nameSERVICE_LOG classch.qos.logback.core.rolling.RollingFileAppender filelogs/service.log/file !-- 同上 rollingPolicy 配置 -- encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender !-- 按包名路由 -- logger namecom.example.lab.controller levelDEBUG additivityfalse appender-ref refCONTROLLER_LOG/ /logger logger namecom.example.lab.service levelDEBUG additivityfalse appender-ref refSERVICE_LOG/ /logger效果LoginController.java的log.debug(Login params: {}, dto)输出到controller.logDeviceReserveService.java的log.info(Reserve success, deviceId{}, deviceId)输出到service.log6.2 MySQL 慢查询分析从slow_query_log到定位COUNT(*)性能瓶颈系统上线后管理员反馈“用户信息管理”页面加载慢。排查步骤开启 MySQL 慢查询日志my.cnfslow_query_log ON slow_query_log_file /var/log/mysql/mysql-slow.log long_query_time 1 # 超过 1 秒记为慢查询 log_queries_not_using_indexes ON复现操作在管理员后台点击“用户信息管理”等待页面加载完成分析日志tail -100 /var/log/mysql/mysql-slow.log # 输出示例 # # Time: 2024-06-15T08:30:22.123456Z # # UserHost: root[root] localhost [] Id: 123 # # Query_time: 3.245678 Lock_time: 0.000123 Rows_sent: 1 Rows_examined: 15000 # SELECT COUNT(*) FROM sys_user WHERE deleted 0;优化方案为sys_user.deleted字段添加索引ALTER TABLE sys_user ADD INDEX idx_deleted (deleted);将COUNT(*)查询改为COUNT(1)虽无本质区别但更符合习惯若数据量极大100万考虑用 Redis 缓存用户总数定时任务每小时更新6.3 小程序端性能监控用wx.getNetworkType()performance.now()定位首屏加载卡顿学生反馈“课程表页面打开慢”。在pages/student/course/index.vue的onLoad生命周期中加入监控onLoad() { const start performance.now(); this.loadCourseData().then(() { const end performance.now(); console.log(课程表加载耗时: ${(end - start).toFixed(2)}ms); // 上报到后端监控接口 uni.request({ url: /api/monitor/performance, method: POST, data: { page: course, duration: end - start, network: wx.getNetworkType() } }); }); }, methods: { async loadCourseData() { const res await request(/api/course/list, { date: this.currentDate }); this.courseList res.data; } }后端MonitorController.java接收并存入monitor_performance表按networkwifi/4g/5g分组统计平均耗时发现 4G 网络下平均耗时 2.3s而 WiFi 下仅 0.4s。根源是课程表接口返回了冗余字段如teacher.avatar图片 URL优化方案CourseDTO类中JsonIgnore掉avatar字段前端按需单独请求教师头像/api/teacher/avatar?id101从那以后我每次交付毕设系统都会强制走一遍这三件事用logback分离 controller/service 日志、开 MySQL 慢查询日志跑一遍所有后台菜单、在小程序真机上切到 4G 网络测首屏。不是为了炫技而是避免答辩现场被问“为什么这个页面要等 5 秒”时只能尴尬地搓手指。希望帮到你。本文还有配套的精品资源点击获取
返回列表