☰
SSM+微信小程序设备报修系统:从建表到状态机闭环实战
2026/10/1 5:32:49 网站建设 项目流程

简介:本资源是一套面向高校计算机相关专业学生的Java毕业设计完整项目,主题为基于SSM框架与微信小程序的设备故障报修管理系统,适合正在准备毕业设计、课程设计或需要全栈实战案例的学习者。项目已通过导师指导与答辩评审,获得97分的高分评价,并在Windows 10/11环境下完成严格调试,下载后可直接运行。压缩包共包含1216个文件,整体约46.16MB,涵盖Java后端源码、微信小程序端wxml与wxss页面、Vue前端组件、SQL数据库脚本、论文文档、使用说明及演示视频等,结构完整、层次清晰。资源中附有部署教程与运行脚本,便于快速搭建环境并理解前后端交互逻辑。目前已有184人学习关注,可作为毕业设计参考、课程设计模板或SSM与小程序联合开发的练手项目,帮助读者掌握设备报修流程中的用户管理、故障提交与处理等核心模块实现思路。

1. 从一份 SSM + 微信小程序的设备报修系统说起:它到底解决什么问题

实验室服务器半夜宕机、车间数控机床报警、机房空调漏水,这类设备故障最怕的不是修不好,而是没人知道、没人认领、修完没记录。传统做法是打电话、发微信群、填纸质单,结果就是责任链断裂、维修进度靠追问、历史故障查不到。这套基于 SSM + 微信小程序的设备故障报修管理系统,本质上是把「报修—派单—维修—验收—归档」这条链路搬到线上,让报修人用手机扫码就能提交,管理员在后台派单,维修工在小程序里接单并回填处理结果。它适合正在做 Java 毕业设计、课程设计的学生,也适合中小企业想低成本搭一套内部设备台账的开发者。SSM 负责后端业务与数据持久化,微信小程序负责移动端轻量入口,两者组合是当前 Java 毕设里检索量最高、资料最全的技术栈之一。下面我按「先立住原理、再动手复现、最后讲坑」的顺序,把整套系统拆开讲清楚。

2. SSM 后端骨架怎么搭:从建表到第一个接口跑通

SSM 是 Spring + SpringMVC + MyBatis 的缩写,这套组合在 Java Web 里属于「老但稳」的路线。Spring 管 Bean 和事务,SpringMVC 管请求路由,MyBatis 管 SQL 映射。设备报修系统的核心表不多,但关系要理清,否则后面派单和状态流转会写得很乱。

2.1 数据库表设计与字段取舍

先看核心表结构。设备表、报修单表、用户表、维修记录表是四张主表,派单和验收通过状态字段串联。下面是我一般会用的建表 SQL,字段名保持和后面实体类一致,避免映射时反复改。

-- 设备信息表:一台设备一条记录,设备编号唯一 CREATE TABLE `device` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `device_no` VARCHAR(32) NOT NULL UNIQUE COMMENT '设备编号,扫码用', `device_name` VARCHAR(64) NOT NULL COMMENT '设备名称', `location` VARCHAR(128) COMMENT '存放位置', `status` TINYINT DEFAULT 1 COMMENT '1正常 0停用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 报修单表:状态机是核心,0待派单 1维修中 2待验收 3已完成 CREATE TABLE `repair_order` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL UNIQUE COMMENT '报修单号', `device_id` INT NOT NULL COMMENT '关联设备', `reporter_id` INT NOT NULL COMMENT '报修人', `fault_desc` VARCHAR(500) COMMENT '故障描述', `img_url` VARCHAR(255) COMMENT '故障图片', `status` TINYINT DEFAULT 0 COMMENT '0待派单 1维修中 2待验收 3已完成', `handler_id` INT COMMENT '维修工ID', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `finish_time` DATETIME COMMENT '完成时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字段取舍上有两个点值得说。第一,status用 TINYINT 而不是字符串,因为状态流转判断在代码里做,数字比较快且省空间,前端展示时再映射成中文。第二,img_url只存路径不存二进制,图片走文件服务器或本地静态目录,数据库只做引用,这是血泪经验——把图片塞进 BLOB 字段,后期查询和备份都会很痛苦。设备编号device_no加唯一索引,是为了后面小程序扫码报修时能直接定位设备。

2.2 MyBatis 映射与分页查询配置

表建好后,实体类和 Mapper 要对应上。MyBatis 的 XML 映射文件里,报修单列表查询通常要带设备名和报修人姓名,所以用关联查询而不是单表查。

<!-- RepairOrderMapper.xml 片段:带设备名和报修人的分页列表 --> <select id="selectOrderPage" resultType="com.demo.vo.RepairOrderVO"> SELECT o.id, o.order_no, o.fault_desc, o.status, o.create_time, d.device_name, d.location, u.real_name AS reporterName FROM repair_order o LEFT JOIN device d ON o.device_id = d.id LEFT JOIN sys_user u ON o.reporter_id = u.id <where> <if test="status != null">AND o.status = #{status}</if> <if test="keyword != null and keyword != ''"> AND (o.order_no LIKE CONCAT('%',#{keyword},'%') OR d.device_name LIKE CONCAT('%',#{keyword},'%')) </if> </where> ORDER BY o.create_time DESC LIMIT #{offset}, #{pageSize} </select>

这里resultType指向一个 VO 类而不是实体类,因为列表页需要展示设备名,实体类里没有这个字段。分页用LIMIT offset, pageSize,offset 由 PageHelper 或手动计算。参数说明:status为 null 时查全部,keyword支持单号和设备名模糊匹配。注意CONCAT里的%不要写成字符串拼接,否则有 SQL 注入风险,用#{}占位是底线。

2.3 Spring 事务与状态流转接口

报修单的状态流转必须放在事务里,否则派单成功但更新状态失败,数据就脏了。下面是一个派单接口的 Service 层写法。

@Service public class RepairOrderServiceImpl implements RepairOrderService { @Autowired private RepairOrderMapper orderMapper; // 派单:把待派单改为维修中,并绑定维修工 @Override @Transactional(rollbackFor = Exception.class) public void assignOrder(Integer orderId, Integer handlerId) { RepairOrder order = orderMapper.selectById(orderId); if (order == null) { throw new RuntimeException("报修单不存在"); } // 只有待派单状态才能派单,防止重复操作 if (order.getStatus() != 0) { throw new RuntimeException("当前状态不可派单"); } order.setHandlerId(handlerId); order.setStatus(1); orderMapper.updateById(order); } }

@Transactional(rollbackFor = Exception.class)是关键,默认只回滚运行时异常,加上rollbackFor后受检异常也回滚。状态判断放在更新前,是防止并发下重复派单。参数orderId和handlerId由 Controller 从请求里取,Controller 只做参数校验和调用,不写业务逻辑。这套分层在答辩时是加分项,因为能说清楚「为什么这么分」。

3. 微信小程序端怎么接:报修提交与列表渲染

小程序端不复杂,但有几个坑是新手必踩的:请求封装、图片上传、状态映射。这一章把这三件事讲透。

3.1 请求封装与全局配置

小程序原生wx.request回调写法容易回调地狱,我一般会封装一层 Promise,并统一处理 token 和错误码。

// utils/request.js 请求封装 const BASE_URL = 'https://your-domain.com/api'; function request(options) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'content-type': 'application/json', 'token': wx.getStorageSync('token') || '' }, success(res) { // 后端统一返回 {code, msg, data} if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { // token 失效,跳登录 wx.navigateTo({ url: '/pages/login/login' }); reject(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要换成自己后端地址,开发时可以在微信开发者工具里勾选「不校验合法域名」。token从缓存取,登录后写入。错误码 401 单独处理跳登录,其他错误统一 toast。这样每个页面调用时只关心业务数据,不用重复写错误处理。

3.2 报修提交页与图片上传

报修提交要选设备、填描述、传图片。图片上传用wx.uploadFile,注意它和wx.request不是一套,要单独封装。

// pages/report/report.js 提交报修 const { request } = require('../../utils/request'); Page({ data: { deviceId: null, faultDesc: '', imgUrl: '' }, // 选择并上传图片 chooseImage() { wx.chooseImage({ count: 1, success: (res) => { const tempPath = res.tempFilePaths[0]; wx.uploadFile({ url: 'https://your-domain.com/api/file/upload', filePath: tempPath, name: 'file', success: (uploadRes) => { // 后端返回图片访问路径 const data = JSON.parse(uploadRes.data); this.setData({ imgUrl: data.data }); } }); } }); }, // 提交报修单 submit() { if (!this.data.deviceId || !this.data.faultDesc) { wx.showToast({ title: '请填写完整', icon: 'none' }); return; } request({ url: '/repair/submit', method: 'POST', data: { deviceId: this.data.deviceId, faultDesc: this.data.faultDesc, imgUrl: this.data.imgUrl } }).then(() => { wx.showToast({ title: '提交成功' }); setTimeout(() => wx.navigateBack(), 1500); }); } });

wx.uploadFile返回的是字符串,要JSON.parse一次。图片路径存到imgUrl,提交时一起发给后端。参数说明:deviceId来自扫码或下拉选择,faultDesc限制 500 字以内,imgUrl可为空。注意上传接口的域名要和请求接口一致,否则开发者工具会拦截。

3.3 报修列表与状态映射渲染

列表页要把后端返回的数字状态转成中文标签,用 WXML 的三元或过滤器都行,我一般在前端做映射,后端只返回数字。

// pages/list/list.js const { request } = require('../../utils/request'); const STATUS_MAP = { 0: '待派单', 1: '维修中', 2: '待验收', 3: '已完成' }; Page({ data: { list: [] }, onShow() { request({ url: '/repair/myList' }).then((list) => { // 给每条数据加状态文本 list.forEach(item => { item.statusText = STATUS_MAP[item.status] || '未知'; }); this.setData({ list }); }); } });

STATUS_MAP放在页面外,避免每次渲染重复创建。onShow而不是onLoad,是为了从提交页返回时能刷新列表。状态映射放前端的好处是后端接口通用,坏处是新增状态要改前端,小项目可以接受。

4. 避坑与排查:这套系统最容易翻车的五个地方

做这套系统,代码写对只是一半,环境、配置、联调才是翻车重灾区。下面五条是我踩过或见别人踩过的,按「现象 → 原因 → 解决」写。

4.1 小程序请求报「不在以下 request 合法域名列表中」

现象:开发者工具里请求全部失败,控制台提示域名不合法。原因:小程序正式环境要求 HTTPS 且域名备案,开发时没勾选忽略校验。解决:开发阶段在「详情 → 本地设置」勾选「不校验合法域名、web-view、TLS 版本以及 HTTPS 证书」;上线前必须换成备案过的 HTTPS 域名,并在小程序后台配置 request 合法域名。

4.2 MyBatis 查询返回字段为 null

现象:列表里设备名、报修人姓名全是 null,但 SQL 单独执行有值。原因:VO 类字段名和 SQL 别名不一致,或没写resultType对应的 setter。解决:检查 SQL 别名和 VO 属性名是否完全一致,比如reporterName对应u.real_name AS reporterName;确认 VO 类有 getter/setter,用 Lombok 的话检查@Data是否生效。

4.3 图片上传后访问 404

现象:上传成功但图片打不开。原因:后端保存路径和静态资源映射路径不一致,或没配置静态资源映射。解决:SpringMVC 配置里加<mvc:resources mapping="/upload/**" location="file:/data/upload/"/>,上传时保存到/data/upload/,返回路径/upload/xxx.jpg。注意 location 结尾斜杠不能少。

4.4 状态流转出现「已完成」还能被派单

现象:已完成的单子还能被派单,数据错乱。原因:Service 层没做状态校验,或并发下两个请求同时通过校验。解决:更新前先查状态并判断,SQL 更新时加WHERE status = 0条件,用受影响行数判断是否成功,这样即使并发也只有一个能改成功。

4.5 数据库中文乱码

现象:故障描述存进去变成问号。原因:数据库、表、连接三处字符集不一致。解决:建库建表用utf8mb4,JDBC 连接串加?useUnicode=true&characterEncoding=utf8,MySQL 配置文件里character-set-server=utf8mb4。三处统一后基本不会再乱码。

5. 进阶技巧:用状态机 + 定时任务把报修闭环做扎实

基础功能跑通后,想让这套系统在答辩或实际使用中更稳,有两个进阶点值得做。第一个是把状态流转抽成状态机,第二个是用定时任务处理超时未处理的单子。

状态机的好处是避免散落各处的 if-else。我一般会定义一个枚举和允许的流转表:

public enum OrderStatus { WAIT_ASSIGN(0, "待派单"), REPAIRING(1, "维修中"), WAIT_CHECK(2, "待验收"), DONE(3, "已完成"); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } // 定义允许的流转:当前状态 -> 可流转到的状态 public static boolean canTransfer(int from, int to) { if (from == 0 && to == 1) return true; // 待派单 -> 维修中 if (from == 1 && to == 2) return true; // 维修中 -> 待验收 if (from == 2 && to == 3) return true; // 待验收 -> 已完成 return false; } }

Service 里更新状态前调用canTransfer,不合法直接抛异常。这样新增状态或调整流程只改一处,测试也好写。参数说明:from是当前状态码,to是目标状态码,返回布尔值。

第二个是定时任务扫超时单。用 Spring 的@Scheduled每小时扫一次,超过 24 小时还是「待派单」的,给管理员发提醒(站内信或小程序订阅消息)。

@Component public class OrderTimeoutTask { @Autowired private RepairOrderMapper orderMapper; // 每小时执行一次,cron 表达式按需调整 @Scheduled(cron = "0 0 * * * ?") public void checkTimeout() { // 查出创建超过24小时且状态为待派单的单子 List<RepairOrder> list = orderMapper.selectTimeoutOrders(24); for (RepairOrder order : list) { // 这里可以插入提醒记录或调用订阅消息 System.out.println("超时未派单:" + order.getOrderNo()); } } }

cron = "0 0 * * * ?"表示每小时整点执行。selectTimeoutOrders(24)的 SQL 用create_time < DATE_SUB(NOW(), INTERVAL #{hours} HOUR)。注意定时任务要开启@EnableScheduling,否则不生效。

验证这套系统是否真的闭环,我一般会走一遍完整流程:小程序提交报修 → 后台看到待派单 → 派单给维修工 → 维修工小程序接单并回填 → 状态变待验收 → 报修人验收 → 状态变已完成 → 历史记录可查。每一步都看数据库状态字段是否按预期变化,图片是否能打开,列表是否实时刷新。这套流程走通,答辩演示基本不会翻车。

最后说个我自己的习惯:每次改完状态流转逻辑,我都会手动构造几条脏数据去测,比如直接改数据库把状态改成已完成再调派单接口,看能不能拦住。能拦住,说明校验是真的生效,而不是只靠前端隐藏按钮。希望帮到你。

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

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

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

立即咨询