简介:本资源是一套面向高校计算机相关专业学生的Java毕业设计完整项目包,主题为基于SSM框架与微信小程序的设备故障报修管理系统,适合正在准备毕业设计、课程设计或需要SSM与小程序实战案例的开发者参考学习。压缩包共1216个文件,约46.16MB,涵盖117个Java后端源码、123个Vue前端组件、170个JavaScript脚本、81个WXML与83个WXSS小程序页面文件,以及2个SQL数据库脚本、论文文档、使用说明与演示视频等,前后端与小程序端结构完整。项目已通过导师指导与答辩评审,得分97分,并在Windows 10/11环境下严格调试,下载后可直接运行部署。资源同时提供数据库文件、部署教程与操作演示,便于读者快速理解报修工单流转、设备信息管理与用户权限控制等核心模块的实现思路,也可作为二次开发与功能扩展的基础模板。目前已有184人学习关注。
1. 从一份 SSM + 微信小程序的设备故障报修系统说起
实验室的服务器半夜报警,值班同学在群里发一句「三号机又挂了」,然后就没有然后了——没人记录、没人跟进、修没修好全靠缘分。这个场景几乎每个高校实验室、每栋写字楼、每个工厂车间都在重复。设备故障报修管理系统要解决的就是这件事:把「口头报修」变成「有单号、有状态、有记录」的闭环流程。这套基于 SSM(Spring + SpringMVC + MyBatis)加微信小程序的方案,后端跑 Java,前端跑小程序,是毕业设计里非常典型的一套组合。它适合三类人:正在做 Java 毕业设计、需要一套能跑通全流程的参考实现的同学;想搞清楚 SSM 三层架构怎么落地到真实业务的后端新手;以及需要给单位内部做一套轻量报修工具、又不想上重型平台的开发者。下面我按「先讲清架构选型,再拆开每一步怎么做,最后说坑在哪」的顺序,把整套东西讲透。
2. SSM 后端骨架怎么搭:从建表到第一个接口跑通
2.1 为什么是 SSM 而不是 Spring Boot
很多人第一反应是「都什么年代了还用 SSM,直接 Spring Boot 不香吗」。这话对生产项目成立,但对毕业设计不一定。SSM 的价值在于它把 Spring 的 IoC/AOP、SpringMVC 的请求分发、MyBatis 的 SQL 映射这三层拆得清清楚楚,答辩时老师问「你的 Controller 到 Service 到 Mapper 是怎么串起来的」,你能一层层指出来。Spring Boot 把这些都自动配置了,反而说不清底层。所以选 SSM 不是技术落后,是教学场景下「可解释性」优先。
这套系统的后端分层是这样的:Controller 层接收小程序发来的 HTTP 请求,Service 层处理业务逻辑(比如报修单状态流转),Mapper 层通过 MyBatis 操作 MySQL。实体类对应数据库表,DTO 用于前后端数据传输。核心表一般有四张:用户表(user)、设备表(device)、报修单表(repair_order)、维修记录表(repair_log)。报修单表是核心,字段包括单号、设备 ID、报修人、故障描述、图片路径、状态(待受理/维修中/已完成/已关闭)、创建时间、受理人。
2.2 建表 SQL 与关键字段设计
先看报修单表,这是整个系统的核心:
CREATE TABLE `repair_order` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `order_no` VARCHAR(32) NOT NULL COMMENT '报修单号,如 RO20240501001', `device_id` BIGINT NOT NULL COMMENT '关联设备ID', `reporter_id` BIGINT NOT NULL COMMENT '报修人用户ID', `fault_desc` VARCHAR(500) DEFAULT NULL COMMENT '故障描述', `fault_images` VARCHAR(1000) DEFAULT NULL COMMENT '故障图片,多张用逗号分隔', `status` TINYINT DEFAULT 0 COMMENT '0待受理 1维修中 2已完成 3已关闭', `handler_id` BIGINT DEFAULT NULL COMMENT '受理维修人ID', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `finish_time` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_reporter` (`reporter_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有几个设计点值得说。order_no加了唯一索引,因为报修单号是业务主键,重复了会出大问题。status用 TINYINT 而不是字符串,查询效率高,前端做状态映射也方便。fault_images用逗号分隔存多张图片路径,这是轻量方案,如果图片多建议单独建一张图片表。idx_status索引很关键,因为维修工查询「待受理」列表是高频操作,没索引数据量一上来就慢。
2.3 MyBatis 映射与分页查询
Mapper 接口和 XML 是 SSM 里最容易翻车的地方。先看接口:
public interface RepairOrderMapper { // 插入报修单 int insertOrder(RepairOrder order); // 按状态分页查询,pageNum从1开始 List<RepairOrder> selectByStatus(@Param("status") Integer status, @Param("offset") Integer offset, @Param("limit") Integer limit); // 统计某状态总数,用于分页计算 int countByStatus(@Param("status") Integer status); // 更新状态,带乐观锁版本号可选 int updateStatus(@Param("id") Long id, @Param("status") Integer status, @Param("handlerId") Long handlerId); }对应的 XML:
<select id="selectByStatus" resultType="com.example.entity.RepairOrder"> SELECT * FROM repair_order WHERE status = #{status} ORDER BY create_time DESC LIMIT #{offset}, #{limit} </select> <update id="updateStatus"> UPDATE repair_order SET status = #{status}, handler_id = #{handlerId}, finish_time = CASE WHEN #{status} = 2 THEN NOW() ELSE finish_time END WHERE id = #{id} </update>分页这里用LIMIT offset, limit,offset 由 Service 层算:(pageNum - 1) * pageSize。注意 MyBatis 的#{}是预编译占位符,能防 SQL 注入,${}是字符串拼接,千万别用在用户输入上。updateStatus 里用 CASE WHEN 处理完成时间,避免在 Java 里写一堆 if-else,SQL 一次搞定。
2.4 用 Postman 验证第一个接口
后端写完先别急着连小程序,用 Postman 或 curl 把接口跑通。假设查询待受理列表:
curl -X GET "http://localhost:8080/repair/order/list?status=0&pageNum=1&pageSize=10" \ -H "Content-Type: application/json"返回结构建议统一成{code, msg, data},data 里放{list, total, pageNum}。这样小程序端解析逻辑统一,不用每个接口写一套。如果返回 500,先看控制台异常栈,八成是 MyBatis 的resultType路径写错或者数据库连接配置不对。如果返回空列表但数据库有数据,检查status传参类型,前端传字符串 "0" 后端用 Integer 接,MyBatis 有时会匹配不上。
3. 微信小程序端怎么接:登录、列表、图片上传三件事
3.1 微信登录换 openid 的完整链路
小程序不能直接用账号密码登录,标准做法是wx.login拿 code,传给后端换 openid。前端:
wx.login({ success: (res) => { if (res.code) { wx.request({ url: 'http://localhost:8080/user/login', method: 'POST', data: { code: res.code }, success: (r) => { // 后端返回自定义 token,存本地 wx.setStorageSync('token', r.data.data.token); wx.setStorageSync('userId', r.data.data.userId); } }); } } });后端拿到 code 后,调用微信的jscode2session接口换 openid 和 session_key。这里注意:appid和secret必须放后端,绝不能写在小程序代码里,否则被人抓包就泄露了。换到 openid 后,查用户表是否存在,不存在就自动注册一条,然后生成一个自定义 token(可以用 JWT 或简单的 UUID 存 Redis)返回给前端。后续所有请求在 header 里带这个 token,后端拦截器校验。
3.2 报修列表的分页加载与下拉刷新
小程序列表加载更多是高频需求,也是热搜词里反复出现的点。核心逻辑是维护pageNum和hasMore两个变量:
Page({ data: { list: [], pageNum: 1, hasMore: true, loading: false }, onLoad() { this.loadList(); }, onReachBottom() { if (this.data.hasMore && !this.data.loading) this.loadList(); }, loadList() { this.setData({ loading: true }); wx.request({ url: 'http://localhost:8080/repair/order/list', data: { status: 0, pageNum: this.data.pageNum, pageSize: 10 }, header: { token: wx.getStorageSync('token') }, success: (res) => { const newList = res.data.data.list; this.setData({ list: this.data.list.concat(newList), pageNum: this.data.pageNum + 1, hasMore: newList.length === 10, // 不足一页说明到底了 loading: false }); } }); } });onReachBottom是小程序自带的上拉触底回调,不用自己监听滚动。hasMore判断用newList.length === 10,如果后端返回不足 10 条,说明没有下一页了。这里有个坑:如果用户快速上拉,可能触发多次请求导致数据重复,所以加了loading锁。下拉刷新用onPullDownRefresh,把pageNum重置为 1、list清空再加载。
3.3 故障图片上传与后端接收
报修时拍故障照片是刚需。小程序用wx.chooseImage选图,wx.uploadFile上传:
wx.chooseImage({ count: 3, success: (res) => { const tempPaths = res.tempFilePaths; tempPaths.forEach((path) => { wx.uploadFile({ url: 'http://localhost:8080/file/upload', filePath: path, name: 'file', header: { token: wx.getStorageSync('token') }, success: (r) => { const imgUrl = JSON.parse(r.data).data; // 收集 imgUrl,提交报修单时一起传 } }); }); } });后端用 SpringMVC 的MultipartFile接收,存到服务器本地目录或对象存储,返回可访问的 URL。注意两点:一是上传目录要有写权限,Linux 下经常因为权限问题静默失败;二是文件名要重命名(用 UUID),否则同名文件会覆盖。返回的 URL 拼上域名前缀,小程序里<image src="{{url}}">才能显示。
4. 状态流转与权限:报修单从提交到关闭的完整链路
4.1 四种状态与流转规则
报修单的状态机是整个系统的业务核心。常见设计是四态:待受理(0)、维修中(1)、已完成(2)、已关闭(3)。流转规则:用户提交生成待受理;维修工接单变维修中;维修工填完维修记录变已完成;用户确认或超时未确认变已关闭。每次状态变更都要写一条 repair_log 记录,谁在什么时间改的、改前改后是什么状态,这是追责和统计的依据。
状态流转必须在 Service 层做校验,不能信任前端传什么就改什么。比如「已完成」只能从「维修中」变过来,不能从「待受理」直接跳。写个简单的校验:
public void changeStatus(Long orderId, Integer targetStatus, Long operatorId) { RepairOrder order = orderMapper.selectById(orderId); if (order == null) throw new BizException("报修单不存在"); Integer current = order.getStatus(); // 合法流转:0->1, 1->2, 2->3 if (!isValidTransition(current, targetStatus)) { throw new BizException("非法的状态流转"); } orderMapper.updateStatus(orderId, targetStatus, operatorId); logMapper.insert(new RepairLog(orderId, current, targetStatus, operatorId)); }isValidTransition用 switch 或 Map 定义合法路径。这样即使有人绕过前端直接调接口,也改不了不该改的状态。
4.2 角色权限:普通用户、维修工、管理员
系统一般有三种角色。普通用户只能看自己的报修单、提交新报修;维修工能看所有待受理单、接单、填维修记录;管理员能管设备、管用户、看统计。权限控制放在拦截器或 AOP 里,根据 token 解析出的 userId 查角色,再判断当前接口是否允许。
一个容易忽略的点:普通用户查列表时,SQL 必须带reporter_id = 当前用户条件,否则会看到别人的报修单。这个条件不能靠前端传,必须后端从 token 里取。维修工查列表则不带这个限制,但要按状态过滤。所以同一个/repair/order/list接口,不同角色走的查询逻辑不同,Service 层要根据角色分支。
4.3 维修记录与统计报表
维修工完成维修后要填维修记录:故障原因、更换配件、维修耗时、备注。这些数据存 repair_log 表。基于这些数据可以做统计:每月报修量、平均维修时长、设备故障率排名。统计 SQL 用 GROUP BY 和聚合函数:
SELECT device_id, COUNT(*) AS fault_count FROM repair_order WHERE create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY device_id ORDER BY fault_count DESC LIMIT 10;这条查最近 30 天故障最多的 10 台设备。管理员页面用 ECharts 或小程序的图表组件展示。注意时间范围查询要加索引,create_time上建索引,否则数据量大了全表扫描很慢。
5. 避坑与排查:这套系统最容易翻车的五个地方
5.1 小程序请求本地后端报「不在以下 request 合法域名列表」
现象:开发者工具里能跑,真机预览时报域名不合法。原因:小程序正式环境要求 HTTPS 且域名备案,本地localhost或 IP 不在白名单。解决:开发阶段在开发者工具「详情-本地设置」勾选「不校验合法域名」,真机调试时用同一局域网 IP 并勾选调试模式。上线前必须换成备案域名加 HTTPS。
5.2 MyBatis 查询返回字段为 null
现象:SQL 在数据库能查出数据,但 Java 对象字段全是 null。原因:数据库字段名order_no和实体属性orderNo没映射上,MyBatis 默认不开驼峰转换。解决:在mybatis-config.xml里加<setting name="mapUnderscoreToCamelCase" value="true"/>,或者手写 resultMap。这个坑几乎每个新手都踩过,血泪经验。
5.3 图片上传后小程序显示不出来
现象:上传成功返回了 URL,但<image>标签空白。原因:返回的是相对路径如/upload/xxx.jpg,小程序需要完整 URL;或者后端静态资源映射没配,访问 404。解决:后端返回时拼上http://域名:端口,并在 SpringMVC 配置<mvc:resources mapping="/upload/**" location="file:/data/upload/"/>把上传目录暴露成静态资源。
5.4 分页查询第二页数据重复或丢失
现象:上拉加载时,第二页出现了第一页的数据,或者中间少了几条。原因:排序字段不唯一,ORDER BY create_time DESC时如果多条记录时间相同,MySQL 返回顺序不稳定。解决:排序加第二字段ORDER BY create_time DESC, id DESC,保证顺序确定。这个坑很隐蔽,数据量小的时候看不出来。
5.5 并发接单导致同一单被两个维修工抢到
现象:两个维修工同时点「接单」,都提示成功,但只有一个应该成功。原因:查询和更新之间有时间窗口,两个请求都查到状态是「待受理」。解决:用乐观锁,update 时加WHERE status = 0,根据 affected rows 判断是否抢到:
UPDATE repair_order SET status = 1, handler_id = #{uid} WHERE id = #{id} AND status = 0;返回影响行数为 1 才算接单成功,为 0 说明被别人抢先了。这是并发场景的标准解法,比加锁轻量。
6. 把这套系统用出价值:二次开发与答辩加分技巧
如果你只是把源码跑起来交差,那这套系统的价值发挥不到三成。真正能拉开差距的是二次开发和讲清楚设计取舍。先说几个投入产出比高的扩展方向。第一是加消息通知:报修单状态变更时,通过小程序的订阅消息推送给用户,用户不用反复打开小程序查进度。订阅消息需要在微信公众平台申请模板,后端调subscribeMessage.send接口,注意每次推送需要用户之前授权过。第二是加设备二维码:每台设备贴一个二维码,扫码直接跳到该设备的报修页,省去手动选设备的步骤,这个在实验室和工厂场景特别实用。第三是加数据看板:用报修量趋势、维修工绩效、设备故障 TOP10 三个维度做可视化,答辩时这一页最能打动老师,因为它证明你不仅会写 CRUD,还懂业务分析。
再说答辩。老师最常问的三个问题:为什么用 SSM 不用 Spring Boot、状态流转怎么保证合法性、并发怎么处理。前两个前面已经讲过,第三个就用乐观锁那段回答。还有一个加分点是主动说清楚系统的边界:这套方案适合中小规模内部使用,如果设备上万台、报修量每天上千,需要考虑分库分表、消息队列削峰、图片存对象存储。能主动说出「什么场景下这套方案不够用」,比一味吹嘘功能全更能体现工程思维。
最后给一个我自己的习惯:每次改完一个模块,先用 Postman 把接口全跑一遍,再连小程序验证,最后真机预览。三层都过了才算完成,别等答辩前一天才发现真机跑不起来。这套系统我前后帮人调过好几版,最常见的翻车不是代码写错,而是环境配置和域名校验这种「非技术」问题。希望帮到你。
本文还有配套的精品资源,点击获取