微信小程序+SSM实验室预约管理系统开发实战剖析 简介在高校科研与实验教学场景中实验室资源的高效调度长期依赖人工登记常出现设备冲突、审批滞后等问题。借助微信小程序作为轻量级前端入口结合SSMSpringSpringMVCMyBatis这一经典Java后端框架可快速搭建一套支持学生预约、教师审批、管理员维护的实验室预约系统。其核心在于通过数据库设计、时间冲突检测SQL、RESTful接口统一数据契约实现预约流程的闭环管理。本文从项目技术选型出发完整拆解用户表、设备表、预约记录表的结构设计并深入讲解SSM分层实现、MyBatis关联查询、并发防重复预约等关键问题。无论用于Java毕设、课设还是企业内部门禁式设备管理该方案都能提供从表结构到前后端联调的工程化参考帮助开发者避开常见开发陷阱提升系统稳定性。 实验室管理小程序做到一半最让人头疼的往往不是某个页面怎么布局而是“代码跑起来了但预约流程走不通”。设备被重复预约、教师端看不到审核列表、学生提交的预约单状态一直卡在“待审核”……这些问题我在做实验室管理微信小程序 SSM 后端项目时几乎全踩了一遍。这个标题看着像打包好的毕设源码实际上背后是一整套完整的工程链路微信小程序做用户端SSMSpring SpringMVC MyBatis做后端接口MySQL 存业务数据。无论是拿来做毕业设计、期末课设还是单纯想系统走一遍前后端分离项目的完整流程这套组合都很值得参考。这篇就把这个项目从表结构到接口设计、从小程序页面到联调排错按我实际开发的顺序完整拆开讲一遍。适合刚结束 Java Web 基础学习、正在找项目练手的学生也适合想快速搭建一套实验室预约管理后台的开发者参考。1. 项目全貌与技术选型为什么是“小程序 SSM”而不是别的组合在聊具体代码之前先把这个项目是怎么回事理清楚。实验室管理系统这个名字听起来宽泛落到实际需求上核心就是解决一个场景学生要在特定时间段使用实验设备教师/管理员要审批和安排设备管理员要维护设备台账和借用记录。没有系统管理的时候这些事情靠纸质登记表和口头沟通时间一长就是一团乱账——谁借了示波器没还、哪个实验室周三下午被两个班同时预约、耗材库存还剩多少全得靠人肉确认。1.1 三个角色和核心业务闭环这个系统我把它拆成三个角色对应三类用户身份这也是很多内部管理系统的通用切分方式学生登录后查看实验室列表、预约实验时段、查看自己的预约记录与审批状态。教师/实验室管理员审批预约申请、管理实验室信息、维护实验设备数据、查看统计情况。系统管理员管理用户账号、重置密码、配置基础数据、查看全量预约流水。业务闭环是学生提交预约申请 - 教师端收到待审核记录 - 教师通过或驳回 - 学生看到结果并按时到场使用设备 - 管理员在后台登记设备实际使用情况。整个过程的状态流转是这套系统的核心逻辑后面接口设计也会围绕它展开。1.2 为什么是 SSM 而不是 Spring Boot标题里写的是“ssm”项目也确实是基于 Spring SpringMVC MyBatis 这一套经典组合实现的。有些人可能会问现在 Spring Boot 这么流行为什么还选 SSM这里有个实际情况很多高校的课程设计和毕业设计题目还在沿用 SSM 结构一方面是因为教学体系里 Spring 基础课程仍然以 SSM 为主另一方面是 SSM 的配置方式更容易把“Spring 容器管理 Bean”这个核心概念讲透——你需要亲手写applicationContext.xml、spring-mvc.xml、mybatis-config.xml而不是靠 Spring Boot 的自动配置黑盒省事。用 SSM 做这套系统分层非常清晰Spring管理 Service 层和 DAO 层的 Bean 实例处理事务边界。SpringMVC负责接收小程序端发来的 HTTP 请求做参数绑定返回 JSON 数据。MyBatis将 Java 方法与 SQL 语句映射起来把数据库查询结果转换成对象。如果后续你想把它升级成 Spring Boot 版本逻辑结构不用大改只要把 XML 配置替换成自动化配置即可因为业务代码和 Mapper 层的设计本来就互相独立。1.3 前后端交互流程与数据契约小程序端和后端的通信方式用的是 RESTful 接口风格。小程序发起wx.request请求后端通过 Controller 接收并处理结果统一封装成 JSON 结构返回给前端。我在项目里固定使用一个统一的返回体格式是这样的{ code: 200, message: success, data: { ... } }这样的好处是前端可以在 request 工具函数里统一判断code不需要每个页面各自处理异常分支。后续做登录状态失效拦截时也只需要在code为某个固定值比如 401 或 403时统一跳转登录页即可。接口路径按照资源来命名比如POST /api/student/appointment—— 学生提交预约GET /api/student/appointment/list?userIdxxx—— 查看我的预约PUT /api/teacher/appointment/audit—— 教师审批预约GET /api/admin/laboratory/list—— 管理员查看实验室列表小程序前端和后端关心的东西就一个词数据契约。字段名、类型、嵌套关系两边必须对齐这算是联调里最基础也最容易出问题的地方。2. 数据库设计实验室管理系统的数据核心做管理系统最重要的一件事就是先把表设计好。表结构定得不合理后面写 SQL、做 Mapper 映射、写业务逻辑全都别扭。我见过不少同学上来就写代码写着写着发现缺字段再回头加列改代码来回折腾。正确做法是先梳理业务需求把表设计的 ER 关系画清楚再动手写系统。2.1 用户表设计的思路用户表是这个系统的基础之一刚开始容易把三个角色拆成三张表来存学生表、教师表、管理员表各建一套。这样做在角色独立字段很多时是合理的但在这个项目里三个角色共用的属性占比更大——用户名、密码、姓名、联系方式、创建时间这些都是通用属性。所以我把用户设计成一张表通过role_type字段区分身份CREATE TABLE tb_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(32) NOT NULL, role_type TINYINT NOT NULL COMMENT 1-学生, 2-教师, 3-管理员, phone VARCHAR(11), email VARCHAR(64), avatar_url VARCHAR(255), status TINYINT DEFAULT 1 COMMENT 1-正常, 0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );密码字段至少要用 MD5 或 BCrypt 加密存储坚决不要明文保存。学生端登录时用学号作为 username教师和管理员用工号即可。real_name是为了在页面展示真实姓名避免每个业务表都冗余用户名。2.2 实验室与设备表的字段细节实验室表主要存实验室名称、位置、容纳人数、设备数量、当前状态开放/维护中、负责人等基础信息。字段本身不难关键是“实验室的开放时间段”和“设备借用状态”这两个边界要在字段上提前考虑清楚。设备表更值得多聊两句。实验设备在实验室管理系统里是真正的业务核心——学生预约的其实本质上是一次“使用某台设备的机会”。一张设备表大致需要这些字段CREATE TABLE tb_equipment ( id INT AUTO_INCREMENT PRIMARY KEY, lab_id INT NOT NULL, equipment_name VARCHAR(64) NOT NULL, equipment_code VARCHAR(32) NOT NULL COMMENT 设备编号, model VARCHAR(64), status TINYINT DEFAULT 1 COMMENT 1-可借用, 0-维修中, 2-已借出, buy_date DATE, price DECIMAL(10, 2), description VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (lab_id) REFERENCES tb_laboratory(id) );设备状态建议单独控制不要通过预约记录动态推算。因为设备可能因为故障临时报修这种状态在预约表里是看不到的。2.3 预约记录表时间冲突检测的基石预约记录表是业务的核心也是预约冲突逻辑的落点。我设计的预约表包含预约用户、所属实验室、设备、预约日期、开始时段、结束时段、用途、状态、审批人和审批意见。这里有个关键细节预约日期和时间段要分开存。appointment_date存日期start_time和end_time存时间。不要把它们拼接成一个 datetime因为时间段的粒度是按“节次”或“小时”来定的分字段存更方便做冲突判断。CREATE TABLE tb_appointment ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, lab_id INT NOT NULL, equipment_id INT, appointment_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, purpose VARCHAR(255), status TINYINT DEFAULT 0 COMMENT 0-待审核, 1-已通过, 2-已驳回, 3-已取消, 4-已完成, apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_user_id INT, audit_comment VARCHAR(255), audit_time DATETIME );状态值我用整数来定前后端约定一套枚举关系。开发者可以在后端写一个常量类统一管理避免魔法值散落在代码里。2.4 耗材管理与其他业务表除了实验室和设备很多实验室管理系统还会带上耗材管理模块。耗材表要记录名称、规格、单位、库存总量、剩余数量、预警阈值。耗材入库和出库各建一张流水表方便后续追溯。如果你的系统还要支持统计报表比如按实验室统计每周使用率那还需要一张实验室开放时间配置表把每个实验室每天可预约的时间段维护进去。这种表结构简单但和预约业务联动时能少写很多代码。我个人的建议是不要一开始就把所有表都想全但核心三张表用户、设备、预约的字段一定要在设计阶段多考虑一步尤其是状态字段和关联字段宁多勿少。后面加字段真的比想象中麻烦。3. 后端 SSM 框架的落地方案从 Maven 工程到接口开发环境这块我不展开太多简单说一下JDK 1.8 Maven 3.6 Tomcat 8.5 MySQL 5.7Java 版本和容器版本尽量保持主流稳定别一上来用 JDK 17 配老项目坑会非常多。3.1 Maven 工程结构和核心配置文件项目按标准的分层结构来组织src/main/java/com/example/labmanage/ ├── controller/ ├── service/ │ └── impl/ ├── dao/ ├── entity/ ├── common/ │ ├── Result.java │ ├── PageResult.java │ └── StatusCode.java └── interceptor/pom.xml 里需要引入的核心依赖包括spring-webmvc、mybatis、mybatis-spring、mysql-connector-java、jackson-databind、druid 连接池、lombok可选减少 getter/setter 冗余。有一个坑值得提醒JDK 8 环境下 Jackson 用 2.9.x 就好版本太高会跟低版本 Spring 有兼容问题。SpringMVC 的spring-mvc.xml里关键配置是两样一个是注解驱动mvc:annotation-driven message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper refjacksonObjectMapper / /bean /message-converters /mvc:annotation-driven另一个是静态资源放行虽然小程序不走页面跳转但图片上传回显会用到静态资源映射。如果你打算把上传的图片存在服务端本地需要做这样的配置mvc:resources mapping/upload/** location/WEB-INF/upload/ /MyBatis 配置里最需要注意的是驼峰映射数据库字段用下划线命名Java 实体类用驼峰命名没有开启驼峰映射的话查询结果会出现一堆 null。settings setting namemapUnderscoreToCamelCase valuetrue/ /settings3.2 登录鉴权从 Session 到 Token实验室管理系统毕竟是内部系统登录鉴权不算复杂。简便做法是使用 Session登录成功后把 userId 和 roleType 存进 Session拦截器里判断用户是否已登录。但小程序端有一个特点wx.request默认不携带 Cookie你要在登录成功后端返回自定义的 token 字符串再在小程序端每次请求时通过请求头传递。我建议在项目初期就采用 token 的思路。不需要引入 JWT 那种完整的机制可以直接在前端登录后存一个 localStorage 的 token小程序用wx.setStorageSync存起来请求时放在 header 的Authorization字段里。后端写一个拦截器统一从请求头取出 token 校验。SpringMVC 拦截器里校验 token 的逻辑大致是这个思路public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !tokenService.isValid(token)) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write(JSON.toJSONString(Result.error(401, 未登录或登录已过期))); return false; } return true; }3.3 预约接口的核心实现冲突检测 SQL预约提交接口是这个项目里业务含量最高的一块。核心逻辑就是同一个实验室在同一时间段只能有一条有效预约记录。这里“有效”指的是状态为待审核或已通过。查询某时间段是否已被占用的 SQL 是核心中的核心我给出常用的冲突判断写法SELECT COUNT(*) FROM tb_appointment WHERE lab_id #{labId} AND appointment_date #{appointmentDate} AND status IN (0, 1) AND ( (start_time #{endTime} AND end_time #{startTime}) )这个区间重叠判断的逻辑很多人第一次写会漏。判断两条记录是否冲突不能只判断包含关系要覆盖所有相交场景start_time #{endTime} AND end_time #{startTime}这一条可以覆盖所有交集情况。时间冲突检测通过后还要检查设备是否可用。如果设备状态是维修中或已借出预约直接驳回提示“设备不可用”。逻辑串联起来就是参数合法性校验时间不能早于当前时间、结束时间必须晚于开始时间实验室是否存在且开放设备是否存在且可借用当前时间段是否冲突插入预约记录状态置为待审核这个顺序就是完整的业务链路。我以前吃过一次亏把时间冲突检测放在了实验室校验前结果用户输入一个不存在的实验室ID时报错提示是“时间冲突”而不是“实验室不存在”非常误导。业务校验顺序一定是先基础数据再业务规则。3.4 MyBatis Mapper 中的关联映射处理预约列表页需要展示预约人姓名、实验室名称、设备名称而这些数据分散在多张表里。第一种写法是写联表查询第二种写法是查询主表后再调用其他 Mapper 填充。在数据量不大的场景下建议直接用联表查询代码量少且查询效率可控。以教师端待审核列表为例select idselectPendingList resultTypecom.example.labmanage.entity.AppointmentVO SELECT a.*, u.real_name AS userName, l.lab_name AS labName, e.equipment_name AS equipmentName FROM tb_appointment a LEFT JOIN tb_user u ON a.user_id u.id LEFT JOIN tb_laboratory l ON a.lab_id l.id LEFT JOIN tb_equipment e ON a.equipment_id e.id WHERE a.status 0 ORDER BY a.apply_time DESC /select用一个AppointmentVO类来承载展示字段不要直接在实体类上加冗余属性避免把持久化对象搞脏。3.5 统计报表的 SQL 聚合用法统计接口一般取两个维度设备使用频率和实验室预约数量。按周统计每个实验室的预约数量用GROUP BY配合日期函数即可SELECT lab_id, COUNT(*) AS appointmentCount, DATE_FORMAT(appointment_date, %Y-%u) AS weekNumber FROM tb_appointment WHERE status IN (1, 4) GROUP BY lab_id, weekNumber ORDER BY weekNumber DESC;这类统计接口在毕设答辩阶段属于加分项建议一定要做工作量不大但能显著提升系统完整性。4. 微信小程序前端的页面结构与交互细节后端接口设计好了小程序的开发就变成按图索骥。整体项目结构我用的是原生小程序写法没有引入 uniapp 或者 Taro。原生小程序的好处是页面结构直观、调试方便、不需要额外编译链路缺点是写复杂页面时样式和组件管理会有点啰嗦。但对于这种管理类系统来说原生完全够用。4.1 项目目录和核心工具封装小程序目录结构长这样pages/ ├── login/ ├── student/ │ ├── appointment/ │ ├── myAppointment/ │ └── index/ ├── teacher/ │ ├── auditList/ │ └── auditDetail/ └── admin/ ├── laboratory/ ├── equipment/ └── user/ utils/ ├── request.js └── util.js app.js app.json在request.js里封装了统一的请求方法。如果不做封装每个页面都要写wx.request的成功失败回调会出现大量重复代码。核心封装思路是const BASE_URL http://localhost:8080/labmanage/api const request (url, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token) wx.request({ url: BASE_URL url, method: method, data: data, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success: (res) { if (res.data.code 200) { resolve(res.data.data) } else if (res.data.code 401) { wx.navigateTo({ url: /pages/login/index }) reject(res.data) } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }) reject(res.data) } }, fail: (err) { wx.showToast({ title: 网络连接失败, icon: none }) reject(err) } }) }) } module.exports { request }这里有几个细节值得注意。第一wx.request得到的是完整 response我在封装里自动解包了data.data部分页面拿到的是实际业务数据不用每处再取一层。第二请求失败时统一用wx.showToast给出反馈不会出现点了按钮没反应的情况。第三401 状态统一跳转登录页业务代码里不用重复判断登录过期。4.2 预约页面日期选择器和时间段选择的实现方式学生预约页面是这个系统最核心的动态交互页面。需要选实验室、选设备、选日期、选时间段、填用途。实验室列表从后端拉取日期选择用小程序原生picker组件的 modedate时间段选择我建议用按钮组或者picker-view不要用picker modemultiSelector那个嵌套数据绑定写起来很麻烦。时间段有个联动逻辑不同的设备/实验室有不同的开放时段。最简单的做法是后端额外提供GET /api/common/timeSlots?labIdxxx接口返回该实验室可预约的时间段数组前端渲染成按钮列表。用户点选某天时还要再调用一次GET /api/common/timeSlots?labIdxxxdate2025-01-01因为周日和节假日的开放时段可能不同。这个前端交互流程用到的是数据联动但接口依赖是后端提前设计好的如果后端没做动态时段查询前端就只能写死时段列表了。4.3 列表页的加载与下拉刷新策略学生自己的预约记录、教师待审核列表、管理员设备列表这三处都是列表页。小程序列表页需要注意两个点第一个是分页加载。不要一次把所有数据查出来我用pageNum和pageSize两个参数触底时onReachBottom加载下一页。这个机制小程序已经封装好了只需要在生命周期回调里写加载逻辑即可。第二个是数据刷新。审核通过后返回列表页列表数据需要重新拉取。我在每个列表页的onShow生命周期里判断needRefresh标志来重新请求数据。还有一种常见优化是使用下拉刷新enablePullDownRefresh但这个开关会让用户在页面停留时直接下拉更新交互上更主动建议列表页两者都支持。4.4 登录状态与页面跳转权限控制小程序端需要根据用户角色来控制 tabBar 显示和页面访问。tabBar 只能配置固定几个页面所以最自然的方案是登录后根据roleType决定跳转到对应的首页学生首页 / 教师审核页 / 管理员页。普通用户如果手动改了 URL 进入教师页面由于后端拦截器会校验 token 和接口权限所以即使前端越权访问拿到的数据也是空的数据安全性在后端这一层兜底。小程序onLoad里先检查token是否已存在如果没有就跳转登录页。这个检查也可以提炼成一个公共方法放到 app.js 里页面生命周期里调用一下。5. 前后端联调阶段最典型的几个坑与排查思路联调阶段遇到的问题五花八门绝大部分不是 Java 后端或者小程序前端各自的语法问题而是两边的信息不对称和框架细节差异。我把项目里最有代表性的几个问题及完整排查过程写出来这些问题在搜索引擎里也常有人问。5.1 小程序端时间格式和时区导致的日期显示错乱第一个比较典型的坑是预约列表里的时间显示多了或者少了 8 小时。学生端提交预约时间后后台显示正常小程序端拿到的 JSON 是一个时间戳字符串格式类似2025-01-15 10:30:00但展示到页面上变成了“2025-01-15 02:30:00”。原因在于 Java 后端使用 Jackson 序列化java.util.Date时默认行为是按 GMT 时区输出而数据库连接字符串里通常配置了serverTimezoneAsia/Shanghai两边不一致JSON 字符串就出现了 8 小时偏移。解决办法是在spring-mvc.xml里配置 Jackson 的objectMapper把时区显式指定为Asia/Shanghaibean idjacksonObjectMapper classcom.fasterxml.jackson.databind.ObjectMapper property namedateFormat bean classjava.text.SimpleDateFormat constructor-arg valueyyyy-MM-dd HH:mm:ss/ /bean /property /bean还有一种更彻底的方式数据库字段全部用datetimeJava 实体类用LocalDateTime接收Jackson 对 Java 8 时间类型的序列化使用jackson-datatype-jsr310模块默认就是 ISO 格式且无时区偏移问题。推荐项目中直接使用LocalDateTime这种写法在 JDK 1.8 环境下没有任何障碍。5.2 图片上传后页面无法访问的 404 问题系统给实验设备做图片上传时会遇到这个问题上传接口返回成功但小程序端image的 src 指向http://localhost:8080/labmanage/upload/xxx.jpg时显示图片加载失败。排查三步走。第一确认网络路径是否可达浏览器直接访问该 URL 看是不是 404。第二确认upload目录在 Tomcat 部署路径下是否存在前面提到的mvc:resources mapping/upload/** location/WEB-INF/upload/ /是否配置。第三确认返回给前端的 URL 是否携带了部署路径前缀。如果项目发布到 Tomcat 时的 context path 是/labmanage那图片地址必须是http://ip:8080/labmanage/upload/xxx.jpg。很多后端人员只在本地 IDE 里测试时路径正常部署到 Tomcat 后因为 context path 变化导致路径全错。给前端返回图片 URL 时建议不要存储完整地址只存相对路径/upload/xxx.jpg前端在展示时拼接一个BASE_URL前缀即可。这样以后换 IP 或者换域名不用改数据库里的数据。5.3 同时提交预约导致超卖式冲突数据库唯一索引的必要性小程序端的按钮在用户点击后如果不做防重复处理用户连点两下“提交预约”会向后端发两次请求。后端即使做了时间冲突检测也要考虑并发场景两个请求同时查了一次预约表发现都不存在冲突记录然后都执行插入最终就出现两条重叠的预约记录。这属于典型的并发问题常规解法是在预约表上加一个唯一索引约束唯一键是lab_id appointment_date start_time end_time。如果status IN (0, 1)也有冲突则唯一索引需要配合状态值。这个方案的优点是数据库层天然拦截无论并发多少请求都不会产生重复预约缺点是某些状态比如已取消的记录和唯一索引冲突时要做额外处理。比较稳妥的方案是把唯一索引建立在“业务有效范围”上或者采用带状态的联合唯一索引。更简单的方案是给提交接口的申请加锁但分布式锁在这个场景里太重了。我个人推荐这样的处理查询冲突时加上事务隔离级别和悲观锁SELECT COUNT(*) FROM tb_appointment WHERE lab_id #{labId} AND appointment_date #{appointmentDate} AND status IN (0, 1) AND (start_time #{endTime} AND end_time #{startTime}) FOR UPDATE在执行插入之前先SELECT ... FOR UPDATE把这个实验室当天的时间记录锁住。这样并发请求会串行化执行后一个请求等前一个事务提交后再查就能看到前一个请求插入的数据冲突检测就生效了。这不是一个性能很高的方案但对于实验室管理系统这种低频业务场景完全足够。5.4 小程序端 request 失败时如何快速定位问题小程序 request 报错一般分三层网络层、服务端层、业务层。排查时我有固定的顺序第一步在开发者工具里打开 “不校验合法域名” 开关详情 - 本地设置否则本地调试时http://localhost:8080会被域名校验拦截直接报“url not in domain list”。第二步看 console 面板请求的具体报错。如果报 “net::ERR_CONNECTION_REFUSED”说明后端服务没起或者端口不对如果报 “timeout”说明后端一直没响应需要去看后端日志。第三步确认后端确实收到参数。在 Controller 方法第一行打日志输出前端传来的参数是否完整尤其是字段名大小写Java 端用驼峰小程序端 data 里也最好保持一致不要一个传userName一个传username。第四步如果前端报 500果断去 Tomcat 的 catalina.out 日志里看异常栈绝大多数是空指针或 SQL 错误。用日志确认比用 Debug 打断点排查效率高很多。5.5 微信小程序开发者工具连接本地后端时真机访问不到这是新人最容易卡壳的问题。模拟器里请求本地后端没毛病拿手机预览时却请求失败。原因很简单手机不能通过localhost去访问电脑上的 Tomcat。这里要求真机调试时需要把 BASE_URL 改成电脑在局域网中的 IP 地址比如http://192.168.x.x:8080/labmanage/api并确保手机和电脑在同一个 WiFi 下同时关闭电脑防火墙对 Tomcat 端口的拦截。这个调式建议配合后端的拦截器逻辑——token 校验的跨域支持也要在 SpringMVC 里配置好否则小程序发起跨域请求时会直接被浏览器或 WebView 拦截报 CORS 错误。配置 CORS 的方法是在spring-mvc.xml里加一个拦截器或者使用 Spring MVC 的注解驱动配置mvc:cors mvc:mapping path/api/** allowed-origins* allowed-methods*/ /mvc:cors实际测试的时候安卓手机和开发者工具的行为不完全一致不要以开发者工具的表现代替真机验证。6. 从毕设项目到可演示系统的完善建议项目功能如果已经都跑通了但总感觉演示的时候不够流畅或者给人一种“开发随意”的印象下面是几个低成本高回报的打磨思路。6.1 初始化数据要提前准备数据库里不要只有几条空记录。把实验室、设备、用户、预约记录都预置一批看起来像样的数据。比如三个实验室软件实验室、网络实验室、嵌入式实验室每个实验室配三到五台设备用户有学生账号和教师账号若干个预约记录覆盖不同状态——待审核、已通过、已完成、已驳回。这样演示时切换页面都有数据可看才像一个真实使用过的系统。6.2 做好演示前的事先演练毕设答辩或项目演示时最怕的是现场才去微信开发者工具里点“编译”然后发现后端服务没启动、数据库连接失败。这属于环境准备问题根本原因是没做预案。建议整理一份启动说明文件把下面这些信息写清楚MySQL 初始化脚本位置数据库账号密码以及如何修改连接配置后端 Java 启动步骤在 IDE 里运行 Tomcat还是用命令启动需要预置的管理员账号和测试学生账号小程序端如何修改 BASE_URL 指向后端这份文档对答辩和后续维护都有用强烈建议写。6.3 为项目增加一个真实亮点的思路如果想让项目比普通库存管理类系统有记忆点可以从下面几个方向挑一个入手工作量都不算大预约看板按实验室维度以一周时间网格展示预约占用情况用颜色区分状态。设备借用到期提醒后端定时任务扫描预约记录将即将到期的预约通过模板消息提醒学生。数据报表每周自动统计实验室使用率以柱状图形式展示在小程序端。设备维修流转记录学生或管理员可以提交维修申请设备进入维修状态并保留日志。这些点里随便挑一个实现的代码量都控制在两天之内但演示效果会明显上一个档次。我记得第一次完整跑通这套系统是在熬夜调试到凌晨两点的时候预约提交、审核、状态流转全部打通那一刻的成就感其实蛮大的。后来我把“时间冲突检测、登录鉴权、图片上传回显”这三个容易出问题的模块写成了文档记录再往后带同学做类似项目时就快了很多。做管理系统这类项目技术栈不是最难的真正难的是把业务逻辑理顺、把数据关系设计清楚、把可能出现的问题前置考虑。如果你也能顺着这个思路走下去这套系统不仅会跑起来而且会跑得很稳。本文还有配套的精品资源点击获取