简介:这份资源是面向微信小程序开发者与计算机专业学生的实验室管理系统完整源码包,采用微信小程序前端搭配SSM(Spring、SpringMVC、MyBatis)后端框架,可用于课程设计、毕业设计或二次开发学习。压缩包共1216个文件,约28.51MB,涵盖js、vue、java、json、wxss、wxml等主要类型,分别对应小程序逻辑、管理后台页面、后端服务、配置数据与界面样式,另有png、svg、jpg等图片资源及sql脚本、bat启动脚本等辅助文件,结构完整。目前已有212人学习下载。源码可直接运行,读者能借此掌握小程序页面开发与接口调用、SSM框架的分层搭建、用户角色权限设计以及实验室设备、记录、人员等模块的实现思路,适合作为全栈入门与项目实战的参考样例。
1. 实验室管理微信小程序+SSM:一套能跑通的资产台账方案长什么样
实验室管理这件事,真正难的不是写页面,而是把「设备台账、耗材库存、预约记录、报废流程」这几本账对齐。很多课题组还在用 Excel 加微信群接龙,设备借出去三天没人记得,耗材月底盘点永远对不上。这套「实验室管理微信小程序 + SSM」的组合,本质是用微信小程序做移动端入口,用 SSM(Spring + SpringMVC + MyBatis)做后端服务,把台账数据集中到一张表里,谁借、谁还、剩多少,扫码就能查。
它适合两类人:一类是计算机专业做课程设计或毕设的学生,需要一套结构完整、能讲清楚分层架构的项目;另一类是实验室管理员或课题组负责人,想用最低成本把纸质登记换成线上流程。下面按「先立住架构、再动手复现、最后讲坑」的顺序拆开,每一步都落到能抄的配置和代码上。
2. 为什么选 SSM 做后端、小程序做前端:分层与选型理由
2.1 SSM 三层结构在实验室管理里的对应关系
SSM 不是三个孤立框架,而是一条请求的完整链路。Controller 层接小程序的 HTTP 请求,Service 层写业务规则(比如「同一台设备同一时段只能被一个人预约」),Dao 层用 MyBatis 操作 MySQL。放到实验室管理场景里,这条链路对应得很清楚:
- Controller:接收
wx.request发来的 JSON,返回统一格式{code, msg, data}。 - Service:处理预约冲突检测、库存扣减、借还状态流转。
- Dao:MyBatis 的 Mapper 接口 + XML,写设备表、耗材表、预约表的增删改查。
选 SSM 而不是 Spring Boot 的理由很实际:课程设计和老项目里,SSM 的 XML 配置能让你看清每个 Bean 怎么装配、事务怎么切,排查问题时不会像 Spring Boot 自动配置那样变成黑匣子。代价是配置量大,但实验室管理这种表结构稳定的系统,一次配好后面很少动。
2.2 小程序端为什么用原生而不是 uniapp
热词里 uniapp 打包微信小程序出现频率很高,但实验室管理这类工具型小程序,我一般建议用原生开发。原因有三点:第一,原生wx.request、wx.login、wx.chooseImage的调用链最短,出问题看官方文档就能定位;第二,实验室管理不需要同时发 App 和 H5,跨端收益为零;第三,原生包体积小,不会触发 uniapp 常见的source size exceed max limit 2mb报错。
小程序端页面不多,通常五到六个:登录页、设备列表页、设备详情/预约页、耗材库存页、我的借还记录页。列表加载更多用onReachBottom配合分页参数,这是原生最稳的写法。
2.3 数据库表设计的最小集合
在动手写代码前,先把表定下来,否则后面改字段会牵动 Mapper、Service、前端三处。最小可用集合是四张表:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| lab_user | id, openid, name, role | role 区分学生/管理员 |
| lab_device | id, name, model, status, stock | status: 0 可借 1 借出 2 维修 |
| lab_consumable | id, name, unit, stock, warn_line | warn_line 低于则预警 |
| lab_record | id, user_id, target_id, type, create_time, back_time | type 区分设备/耗材 |
openid是小程序用户唯一标识,登录时由后端用code换回来存库,不要存 session_key 到前端。
3. 从零跑通后端:SSM 配置与核心接口实现
3.1 环境准备与依赖版本
后端跑起来需要 JDK 8、Maven 3.6+、MySQL 5.7 或 8.0、Tomcat 8.5。依赖用 Maven 管理,核心几个坐标如下,版本按你本地仓库已有的选,不要盲目追新:
<!-- pom.xml 关键依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency>逻辑说明:spring-webmvc提供 DispatcherServlet 和注解驱动;mybatis-spring负责把 SqlSessionFactory 交给 Spring 管理;fastjson用于接口返回 JSON 序列化。参数上,MySQL 8 的驱动类要写com.mysql.cj.jdbc.Driver,连接串加serverTimezone=Asia/Shanghai,否则时间字段会差 8 小时。
3.2 登录接口:用 code 换 openid 的完整链路
小程序端调wx.login()拿到临时 code,发给后端,后端请求微信接口换 openid。这是整个系统唯一需要外网请求的地方,其余接口都在内网完成。
// UserController.java 登录接口 @RequestMapping("/api/login") @ResponseBody public Map<String, Object> login(@RequestBody Map<String, String> body) { String code = body.get("code"); // 拼接请求参数,appid 和 secret 从配置文件读取 String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appId + "&secret=" + appSecret + "&js_code=" + code + "&grant_type=authorization_code"; String resp = HttpUtil.get(url); // 自行封装 GET 工具 JSONObject json = JSON.parseObject(resp); String openid = json.getString("openid"); // 查库,不存在则注册 LabUser user = userService.findByOpenid(openid); if (user == null) { user = userService.register(openid); } Map<String, Object> result = new HashMap<>(); result.put("code", 0); result.put("userId", user.getId()); result.put("role", user.getRole()); return result; }逻辑说明:code只能用一次,五分钟过期,所以前端拿到后要立刻发请求,不要缓存。参数上appid和appSecret必须放在后端配置文件,绝不能写进小程序代码,否则被人抓包就泄露了。返回的openid是用户唯一标识,session_key不要返回给前端。
3.3 设备预约接口:事务与冲突检测
预约是实验室管理里最容易出并发问题的地方。两个人同时点「预约」同一台设备,如果不加锁,两条记录都会写进去。
// DeviceServiceImpl.java 预约核心逻辑 @Transactional public Map<String, Object> reserve(Integer deviceId, Integer userId) { // 1. 行锁查询设备,防止并发超借 LabDevice device = deviceMapper.selectForUpdate(deviceId); if (device.getStock() <= 0) { return ResultUtil.fail("库存不足"); } // 2. 检查该用户是否已有未归还记录 int count = recordMapper.countActive(userId, deviceId); if (count > 0) { return ResultUtil.fail("你已借出该设备,请先归还"); } // 3. 扣库存 + 写记录 deviceMapper.decreaseStock(deviceId); recordMapper.insert(userId, deviceId, 0); return ResultUtil.success(); }逻辑说明:selectForUpdate对应 Mapper XML 里的SELECT ... FOR UPDATE,在事务内锁住这一行,第二个请求会等待。参数上@Transactional默认只对 RuntimeException 回滚,如果 Service 里抛的是受检异常,要加rollbackFor = Exception.class。countActive查的是back_time IS NULL的记录,防止一个人重复借同一台设备。
3.4 MyBatis Mapper 与 XML 的对应写法
Mapper 接口和 XML 必须一一对应,namespace 写接口全限定名,id 写方法名。这是新手最容易配错的地方。
<!-- DeviceMapper.xml 片段 --> <mapper namespace="com.lab.dao.DeviceMapper"> <select id="selectForUpdate" resultType="com.lab.entity.LabDevice"> SELECT * FROM lab_device WHERE id = #{id} FOR UPDATE </select> <update id="decreaseStock"> UPDATE lab_device SET stock = stock - 1 WHERE id = #{id} AND stock > 0 </update> </mapper>逻辑说明:decreaseStock的AND stock > 0是第二道保险,即使行锁失效,也不会把库存扣成负数。参数上#{id}是预编译占位符,能防 SQL 注入;如果要用动态排序字段,只能用${},但必须做白名单校验。
4. 小程序端落地:页面结构、请求封装与列表加载
4.1 请求封装与全局配置
小程序端不要在每个页面里裸写wx.request,封装一层统一处理域名、token 和错误码。
// utils/request.js const BASE_URL = 'https://your-domain.com'; function request(url, method, data) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method || 'GET', data: data || {}, header: { 'content-type': 'application/json' }, success(res) { if (res.data.code === 0) { resolve(res.data); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request };逻辑说明:BASE_URL必须是 HTTPS 且在小程序后台配置了合法域名,否则真机调试会报url not in domain list。参数上header里的content-type要和后端@RequestBody对应,如果后端用@RequestParam接收,则要改成application/x-www-form-urlencoded。
4.2 设备列表页与加载更多
列表页用onLoad拉第一页,onReachBottom拉下一页,这是原生小程序最稳的分页模式。
// pages/device/list.js Page({ data: { list: [], page: 1, hasMore: true }, onLoad() { this.loadData(); }, onReachBottom() { if (this.data.hasMore) this.loadData(); }, loadData() { const that = this; request('/api/device/list?page=' + this.data.page, 'GET').then(res => { const newList = that.data.list.concat(res.data.records); that.setData({ list: newList, page: that.data.page + 1, hasMore: res.data.records.length === 10 }); }); } });逻辑说明:hasMore判断依据是「本页返回条数是否等于页大小」,等于 10 说明可能还有下一页。参数上page从 1 开始,后端用LIMIT (page-1)*10, 10分页。注意setData不要传整个大数组,数据量大时会有性能问题,超过 200 条建议用虚拟列表。
4.3 单选框与表单提交
借还操作里常用单选框选设备状态,原生radio-group的写法要绑定bindchange。
<!-- pages/record/add.wxml --> <radio-group bindchange="onTypeChange"> <label><radio value="0" checked/>借出</label> <label><radio value="1"/>归还</label> </radio-group>onTypeChange(e) { this.setData({ type: e.detail.value }); }逻辑说明:e.detail.value是字符串,提交给后端前要parseInt转成数字,否则 MyBatis 的Integer参数会报类型不匹配。参数上checked只对第一个生效,不要给多个 radio 加 checked。
5. 避坑与排查:这套组合最容易翻车的五个地方
5.1 真机请求失败但开发者工具正常
现象:开发者工具里接口全通,真机预览全部超时。原因:开发者工具默认不校验域名,真机强制校验。解决:在小程序后台「开发设置」里配置 request 合法域名,必须是 HTTPS,且不能带端口号(443 除外)。本地调试可以勾选「不校验合法域名」,但上线前必须配好。
5.2 时间字段差 8 小时
现象:数据库存的create_time和实际时间差 8 小时。原因:MySQL 8 的时区默认是 UTC,JDBC 连接串没指定时区。解决:连接串加serverTimezone=Asia/Shanghai,同时确认 MySQL 服务器time_zone参数。如果还不对,检查实体类字段类型是不是java.util.Date,建议统一用java.time.LocalDateTime。
5.3 预约并发导致库存扣成负数
现象:压测时两台设备被三个人同时预约,库存变成 -1。原因:Service 方法没加事务,或者加了但没生效(同类内部调用不走代理)。解决:确认@Transactional加在 public 方法上,且调用方是通过 Spring 注入的 Bean 调用,不是new出来的对象。数据库层面给stock字段加UNSIGNED约束作为最后防线。
5.4 小程序包体积超 2MB 无法上传
现象:上传时提示source size exceed max limit 2mb。原因:图片、字体、第三方库没压缩。解决:图片全部走 CDN,不要放本地;uni-app项目要开分包;原生项目检查node_modules是否被打进包。实验室管理这类工具,主包控制在 1MB 以内完全可行。
5.5 MyBatis 返回字段为 null
现象:查询结果里某些字段是 null,但数据库明明有值。原因:实体类属性名和表字段名不一致,且没开驼峰映射。解决:在mybatis-config.xml里加<setting name="mapUnderscoreToCamelCase" value="true"/>,或者手写<resultMap>做映射。字段名create_time对应属性createTime,开了驼峰就自动匹配。
6. 进阶技巧:用拦截器做权限校验与操作日志
6.1 拦截器区分学生和管理员
实验室管理里,学生只能借还和查自己的记录,管理员才能增删设备和改库存。用 SpringMVC 拦截器统一拦截,比在每个 Controller 里写 if 判断干净得多。
// AuthInterceptor.java public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { String uri = req.getRequestURI(); // 管理端接口统一前缀 /api/admin if (uri.startsWith("/api/admin")) { String role = (String) req.getSession().getAttribute("role"); if (!"admin".equals(role)) { resp.setStatus(403); resp.getWriter().write("{\"code\":403,\"msg\":\"无权限\"}"); return false; } } return true; } }逻辑说明:preHandle返回 false 会中断请求,所以要把响应体写完整。参数上req.getSession()依赖服务端 session,小程序端要在登录后把JSESSIONID存到 storage,每次请求带上。如果不想用 session,可以换成 JWT,在 header 里传 token,拦截器解析 token 取 role。
6.2 操作日志表与 AOP 切面
借还记录本身就是一种日志,但管理员改库存、删设备这些操作也需要留痕。用 Spring AOP 切 Service 层,把关键操作写进lab_log表。
// LogAspect.java @Aspect @Component public class LogAspect { @Autowired private LogMapper logMapper; @AfterReturning("execution(* com.lab.service.*.update*(..)) || execution(* com.lab.service.*.delete*(..))") public void recordLog(JoinPoint jp) { String method = jp.getSignature().getName(); Object[] args = jp.getArgs(); LabLog log = new LabLog(); log.setAction(method); log.setParams(JSON.toJSONString(args)); log.setCreateTime(LocalDateTime.now()); logMapper.insert(log); } }逻辑说明:切点表达式匹配update和delete开头的方法,@AfterReturning保证只在方法成功返回后记录,抛异常不记。参数上JSON.toJSONString(args)可能包含敏感字段,实际项目里要做脱敏,比如把密码字段替换成***。
6.3 验证方法:用 Postman 和开发者工具交叉验证
后端接口写完,先用 Postman 跑一遍,确认返回格式和状态码。然后在开发者工具的 Network 面板看小程序发出的请求,对比两边的参数是否一致。最容易出问题的是content-type和参数位置:Postman 里用raw JSON对应@RequestBody,用form-data对应@RequestParam,两边必须一致。真机测试时打开 vConsole,能看到完整的请求日志,比盲猜快得多。
我自己的习惯是:每加一个接口,先在 Postman 里存一个用例,改完代码跑一遍,确认没破坏旧接口再往下写。这套 SSM + 小程序的组合,配置阶段最烦,但一旦跑通,后面加功能就是复制粘贴改字段。希望帮到你。
本文还有配套的精品资源,点击获取