简介:这套基于SSM与微信小程序的设备故障报修管理系统毕业设计项目,面向计算机相关专业毕业生和需要快速搭建同类业务的开发者,覆盖用户报修、管理员派单、工单状态跟踪、设备信息维护等典型场景。包内包含完整的Java后端源码、MySQL数据库初始化脚本、毕业论文、使用说明文档及演示视频,一站式支撑从环境搭建、功能演示到答辩汇报的完整流程。资源共1216个文件,压缩包46.16MB,主要文件类型涵盖Java业务代码、Vue后台管理页面、微信小程序端wxml/wxss/js逻辑、PNG/JPG界面素材、SQL脚本以及MP4演示录像,目录结构直观,适合按模块对照学习和二次改造。该项目为个人高分毕业设计,答辩评审97分,已在Windows10/11环境严格调试并附启动脚本与部署教程,下载即可运行,也可作为课程设计或新系统开发的基础原型。目前已有184人学习下载,对希望掌握SSM框架与小程序前后端联调的读者,能提供可直接复用的代码结构和论文写作范本。
1. 设备故障报修管理系统,难点不在表单而在状态流转
设备报修这个场景,表面上是“填一张故障单 + 后台有人处理”,很多同学一开始把精力全放在表单 UI 和增删改查上,做出来却总在答辩时被问住。问题不在页面不够好看,而在报修单从提交、受理、处理到评价的状态流转没有闭环:谁接单、处理到哪一步、报修人能看到什么、超时怎么办,这些才是系统真正要解的问题。这套基于 SSM + 微信小程序的设备故障报修管理系统,用户端是微信小程序,后端用 SSM 框架承接管理后台和接口服务,典型的数据流是“小程序提交报修 → SSM 写入数据库 → 管理员/维修员在后台处理 → 状态回写 → 小程序端展示进度”。适合两类读者:正在找毕业设计参考的应届生,以及想给单位做内部报修工具、又不想上重型 OA 的后端工程师。后面的内容我会从后端分层、表结构、接口设计、小程序端封装一直讲到答辩排错,全程是可复现的写法。
2. 先拆 SSM 后端三层:Controller 只做转发,业务留在 Service
2.1 为什么毕设场景仍多选 SSM 而非 Spring Boot
现在 Java 面试题里 Spring Boot 基本是默认技能,但高校课程和大量毕设参考资料仍然以 SSM 为主线。原因不难理解:SSM 的三层边界更明显,SpringMVC 的请求映射、Spring 的声明式事务、MyBatis 的 SQL 控制都是显式配置,能完整对应“表现层 - 业务层 - 持久层”的教学框架。用 SSM 做毕设不丢分,关键是能说清楚每一层在做什么。Spring Boot 把配置自动化的同时,也把很多边界藏了起来,对需要现场讲原理的答辩反而不利。
后端工程我习惯按下面的包结构拆,这个结构也方便后面写论文里的架构图:
com.example.repair ├── controller // 接收请求、参数校验、返回 JSON ├── service // 业务逻辑、事务边界 ├── dao // MyBatis Mapper 接口 ├── entity // 数据库实体 ├── common // 统一返回对象、异常、状态常量 └── interceptor // 登录拦截、Token 校验2.2 Controller-Service-Mapper 三层包结构,报修单实体字段一次定对
很多同学写 Service 时喜欢把所有逻辑堆到一个方法里,导致一个接口几百行。这里给出一个状态流转的标准拆分:Controller 只接收参数和调用 Service,把校验交给 Service;Service 内部先查旧状态,再判合法性,最后更新;Mapper 只处理单表写操作。以“维修员受理报修单”为例:
@Override @Transactional(rollbackFor = Exception.class) public void acceptOrder(Long orderId, Long handlerId) { RepairOrder update = new RepairOrder(); update.setId(orderId); update.setHandlerId(handlerId); update.setStatus(OrderStatus.PROCESSING); update.setHandleTime(new Date()); int rows = repairOrderMapper.updateStatusById(update, OrderStatus.WAITING); if (rows == 0) { throw new BizException("单据已被处理,请刷新后重试"); } repairLogMapper.insert(RepairLog.create(orderId, handlerId, "受理报修单")); }这段代码有三个关键点:一是@Transactional保证状态更新和日志写入同时成功或同时失败,避免出现“状态改了但没记录”的脏数据;二是 update 语句的 where 条件里带上“期望的当前状态”,相当于乐观锁;三是通过rows == 0判断是否冲突,如果有并发操作,第二个请求会拿到 0 行更新结果,直接提示用户刷新,不再往下走。对应的 Mapper XML 写法如下:
<update id="updateStatusById"> UPDATE repair_order SET status = #{param.newStatus}, handler_id = #{param.handlerId}, handle_time = #{param.handleTime} WHERE id = #{param.id} AND status = #{expectStatus} </update>expectStatus是调用方传入的期望前置状态,这个字段的命名和类型最好在 Mapper 接口里用@Param显式声明,避免多个参数时 MyBatis 用arg0、arg1导致 SQL 解析错误。下面用表格把报修单的状态机理清楚,后续所有接口都围绕这个表来设计:
| 操作 | 前置状态 | 目标状态 | 关联动作 | 操作人 |
|---|---|---|---|---|
| 提交报修 | 无 | 0 待受理 | 写 repair_log | 用户 |
| 受理工单 | 0 待受理 | 1 处理中 | 指派维修员 | 管理员 |
| 完成处理 | 1 处理中 | 2 已完成 | 写结单备注 | 维修员 |
| 用户确认 | 2 已完成 | 3 已评价 | 写评分与意见 | 用户 |
| 撤销报修 | 0 待受理 | 4 已撤销 | 记录原因 | 用户 |
状态字段用int比用varchar更省空间,配合后端常量类管理可读性也够。开发阶段为了调试方便,我给这个 int 字段加过@TableField注释,并在实体类上用@JsonFormat控制时间输出,避免前端拿到一串时间戳。
2.3 状态流转的 Service 实现与 MyBatis SQL 写法
上面的 acceptOrder 只是第一步,完整的“完成工单”接口会再带上图片和备注。我一般会给 repair_order 增加complete_remark和finish_time两个字段,更新时把备注写进去,同时往 repair_log 插记录。这里不新建一条报修单,而是更新现有记录,这是和“追加记录”最大的区别:报修单主表永远只保留一份当前状态,历史轨迹全部放日志表。如果后期要做“处理超时提醒”,也只查主表的handle_time和当前状态。
2.4 SpringMVC 接收小程序参数:3 个必踩的坑
小程序端的wx.request默认以 JSON 格式提交,后端如果写成下面这样,就必须保证请求头带Content-Type: application/json:
@PostMapping("/api/order/add") @ResponseBody public ResultVO addOrder(@RequestBody RepairOrderVO vo) { return ResultVO.success(repairService.create(vo)); }第一个坑是参数类型不匹配:小程序端传的handler_id如果是字符串,MyBatis 在写入数据库时可能报数字转换异常,解决方式是在前端提交前统一转数字,或者在 VO 里用 String 接收再手动校验转换。第二个坑是时间字段,前端传2025-01-01 10:00:00这种字符串,后端直接用 Date 接收会报 400,可以在 VO 里定义成 String,进入 Service 后用DateUtil.parse显式转换,错误信息更可控。第三个坑是字段命名,Java 的驼峰和数据库下划线对应关系要在mybatis-config.xml里开启map-underscore-to-camel-case,否则createTime永远映射不上create_time。
3. 数据库设计:设备、报修单、日志三张核心表
3.1 从数据库课程设计的角度拆三张核心表与建表 SQL
需要先明确一个基本判断:设备信息、报修单、处理日志必须分开建表,不能把设备信息冗余到报修单里。从数据库课程设计的角度来看,这对应的是“一对多”关系:一台设备可以有多条报修记录,一条报修记录又对应多条处理日志;并且设备的位置、编号变化不能影响历史报修单的展示,所以报修单里只存device_id,不直接冗余设备地址。三张核心表的职责见下表:
| 表名 | 职责 | 关键字段 | 关联关系 |
|---|---|---|---|
| device | 设备台账 | id, device_code, device_name, location | 被报修单引用 |
| repair_order | 报修单主表 | id, order_no, device_id, reporter_id, status | 关联 device、user |
| repair_log | 处理日志表 | id, order_id, operator_id, action, remark | 多对一关联 repair_order |
建表 SQL 用 InnoDB 引擎,字符集统一utf8mb4,因为用户在小程序端填写的故障描述可能包含 emoji 表情;直接用utf8会出现无法入库或乱码。下面是设备表和报修单表的精简版本:
CREATE TABLE device ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_code VARCHAR(32) NOT NULL UNIQUE, device_name VARCHAR(64) NOT NULL, location VARCHAR(128) DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT '1正常 0维修中', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE repair_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, device_id BIGINT NOT NULL, reporter_id BIGINT NOT NULL, handler_id BIGINT DEFAULT NULL, description VARCHAR(500) NOT NULL, images VARCHAR(1000) DEFAULT NULL COMMENT '逗号分隔图片URL', status TINYINT DEFAULT 0 COMMENT '0待受理 1处理中 2已完成 3已评价 4已撤销', priority TINYINT DEFAULT 1 COMMENT '1普通 2紧急', handle_time DATETIME DEFAULT NULL, finish_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_device_id (device_id), KEY idx_status (status) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;注意两个索引:idx_status是为了支撑“待受理列表”这种高频查询,order_no的唯一索引是配合后面的单号生成策略。如果后期报修数量大,status 区分度不够,可以改成idx_status_create_time复合索引,让列表页按状态和时间同时过滤。字段注释要写清楚状态枚举的含义,因为论文里需要贴表结构说明,注释直接复制过去就能用。
3.2 报修单号生成规则:日期序列与唯一索引兜底
不建议把自增主键 id 直接展示给用户,单号要用有业务含义的格式。常见做法是日期加当天序号:BX20250101001,含义是报修单、2025-01-01、当天第 1 单。生成逻辑先查当天已有单数,加一后拼接,然后插入时依赖order_no唯一索引兜底。
SELECT COUNT(*) FROM repair_order WHERE order_no LIKE CONCAT('BX', DATE_FORMAT(NOW(), '%Y%m%d'), '%')如果并发请求同时查到这个 count,后插入的那条会因唯一索引报错,此时重试一次即可。对于毕设量级,这样完全够用,不需要引入号段或雪花算法。把这段逻辑放在 Service 里,不要放在 Mapper 或 Controller,因为单号生成属于业务规则。
3.3 待受理列表的联表查询 SQL
维修员打开后台看到的第一屏是待受理列表,这个列表至少要显示单号、设备名称、位置、报修人、故障描述、提交时间。最直接的 SQL 是三表联查:
SELECT r.order_no, d.device_name, d.location, u.nickname AS reporter, r.description, r.create_time, r.priority FROM repair_order r LEFT JOIN device d ON r.device_id = d.id LEFT JOIN user u ON r.reporter_id = u.id WHERE r.status = 0 ORDER BY r.priority DESC, r.create_time ASC LIMIT #{offset}, #{pageSize};排序用priority DESC, create_time ASC,紧急单排前面,同优先级按提交时间从早到晚,正好对应“先来先处理”的业务规则。查询接口的两个分页参数需要做上限控制:pageSize 不超过 50,防止小程序端一次性拉太多数据导致渲染卡顿。另外轻量“我的报修”列表只需要按 reporter_id 过滤,加一个idx_reporter_id索引即可,这里不再单独贴 SQL。
3.4 微信小程序登录:code 换 openid 与 Token 鉴权
小程序的登录流程和传统账号密码不同:前端调用wx.login()获取临时 code,后端拿 code 换 openid 和 session_key。openid 是用户在小程序内的唯一标识,不能从小程序端直接获取,必须走后端交换接口:
String url = "https://api.weixin.qq.com/sns/jscode2session"; Map<String, String> params = new HashMap<>(); params.put("appid", APPID); params.put("secret", SECRET); params.put("js_code", code); params.put("grant_type", "authorization_code"); String result = HttpUtil.get(url, params); // 返回 openid 和 session_key这里有一个非常重要的边界:secret 只能存在后端,绝对不能写进小程序代码,否则任何人反编译源码就能拿到你的小程序密钥。获取 openid 之后,去 user 表查是否存在该用户,不存在就先按昵称“微信用户”自动注册,然后生成一个随机 token 返回给前端。后续请求都带这个 token,后端用拦截器校验,校验通过后再把 userId 放到 ThreadLocal 或 Request 属性里,Controller 随时可取。Token 的有效期建议设成 7 天,到期后小程序重新走一次 wx.login 就能续期。
4. 微信小程序端:报修入口、图片上传、状态可见
4.1 小程序目录结构与 app.json 基础配置
小程序端的页面建议按角色拆分:用户看到首页报修列表、提交报修、我的报修、详情四个页面;管理员和维修员尽量复用同一套页面,通过隐藏不同按钮来控制操作权限,避免页面数量膨胀。目录结构如下:
├── app.js // 全局登录逻辑 ├── app.json // 页面注册、窗口配置 ├── utils/request.js // 请求封装 ├── pages/ │ ├── login/ // 登录页 │ ├── index/ // 报修列表(首页) │ ├── report/ // 提交报修 │ ├── detail/ // 工单详情 │ └── mine/ // 我的报修 └── components/ // 空状态、工单卡片等组件app.json 里要重点配置两处:navigationBarTitleText 按页面设置,方便区分功能;如果打算自定义顶部导航,就把 navigationStyle 设为 custom,然后通过wx.getWindowInfo().statusBarHeight计算状态栏高度,做安全区适配。很多手机顶部有刘海,适配不对就会遮挡自定义标题,这部分是答辩时容易被点名的细节。如果对原生小程序的组件样式不熟,也可以把页面直接迁移到 uni-app 微信小程序,用标签化组件和条件编译处理,request 封装的思路一致,只是把wx.request换成uni.request。
4.2 封装 wx.request:统一 baseURL、token 与错误码处理
所有页面请求都应该走同一个封装函数,避免每个页面重复写wx.request和错误处理。下面是实际可用的 request.js 核心逻辑:
const BASE_URL = 'http://localhost:8080/repair' const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data) } else if (res.data.code === 401) { wx.removeStorageSync('token') wx.redirectTo({ url: '/pages/login/login' }) } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) reject(res.data) } }, fail: (err) => reject(err) }) }) } module.exports = { request, BASE_URL }这里做了三件事:把 baseURL 集中管理,改后端地址只需要动一处;把 token 统一加到请求头;对 401 做全局拦截,直接跳回登录页,而不是让每个页面自己判登录失效。开发调试时注意,微信开发者工具的“不校验合法域名”开关只影响模拟器,真机预览时 BASE_URL 必须换成局域网 IP 或者已备案的 HTTPS 域名,且需要在 mp 后台配置 request 合法域名,这是无法跳过的配置项。接口联调阶段如果发现提交的数据后端收不到,优先打开开发者工具的 Network 面板,看 Content-Type 是不是被小程序自动改成了表单格式。
4.3 报修表单、单选框与图片上传
提交报修页最核心的两个能力是故障类型选择和图片上传。故障类型用 radio-group 实现,可选项和设备类型联动的部分写死在页面 data 里即可;图片上传必须用wx.uploadFile而不是wx.request,因为前者专门处理 multipart/form-data 类型:
wx.chooseMedia({ count: 3, mediaType: ['image'], sourceType: ['camera', 'album'], success: (res) => { const filePath = res.tempFiles[0].tempFilePath wx.uploadFile({ url: BASE_URL + '/api/upload', filePath, name: 'file', header: { 'token': wx.getStorageSync('token') }, success: (uploadRes) => { const data = JSON.parse(uploadRes.data) // data.url 为后端返回的图片访问地址 this.setData({ imageUrls: [...this.data.imageUrls, data.url] }) } }) } })count 最多 3 张是为了控制后端存储压力和报修单展示长度。上传成功后再点“提交报修”,走普通的 request 请求,把 imageUrls 用逗号拼接成一个字符串存进repair_order.images字段。这里需要注意时序:必须等图片全部上传完成再提交表单,否则报修单里没有图片证据。上传失败时后端要返回明确的错误信息,小程序端用wx.showToast提示用户重新选择,而不是静默丢图。真机调试时如果 uploadFile 一直失败,先确认后端 upload 接口是否已经加入请求白名单,因为上传接口通常也不带 token。
4.4 工单列表渲染与状态轮询
报修单列表页的状态文案不能直接显示数字,需要在前端做映射。这里给出一个可复用的映射方案:
const STATUS_MAP = { 0: { text: '待受理', color: 'orange' }, 1: { text: '处理中', color: 'blue' }, 2: { text: '已完成', color: 'green' }, 3: { text: '已评价', color: 'gray' }, 4: { text: '已撤销', color: 'gray' } }同时要处理好工单卡片的数据来源。常见做法是首次加载用列表接口拉 20 条数据,下拉触发onPullDownRefresh重新拉第一页,触底用onReachBottom加载下一页。对于“处理中”的工单,我一般加一个 10 秒的轮询定时器,只刷新当前页数据;页面通过onHide清除定时器,避免切后台后继续发请求。轮询接口用status = 1过滤,比全量刷新的接口省流量,也让用户能尽快看到维修员更新后的状态。列表渲染时注意用wx:key绑定 orderNo 而不是 index,否则状态更新后列表重排会引起渲染异常。
5. 从跑通到高分:联调排错与答辩演示该怎么准备
5.1 联调排错:从 Network 面板看到 404/400 的处理
开发过程中遇到的接口问题,九成能从请求面板找到答案。拿到一个接口报错,按下面的顺序排查:先看请求 URL 里的 IP、端口、路径是否和后端一致,SSM 项目部署在 Tomcat 时默认端口是 8080,路径中通常要带项目名;再看请求方式是 GET 还是 POST,后端@RequestMapping没指定 method 时两者都接收,但@GetMapping和@PostMapping会严格校验;最后看请求体,后端用@RequestBody接收时必须传 JSON 字符串,用param接收时必须传表单格式。遇到 400 先检查字段名和前端 data 里的 key 是否一致,遇到 500 则直接看后端 Tomcat 控制台堆栈,MyBatis 的 SQL 报错一般都能直接看到具体字段。关键排错项可以记在下表里:
| 现象 | 检查点 | 解决方向 |
|---|---|---|
| 请求 404 | 路径、项目名、Tomcat 端口 | 核对 BASE_URL 与后端 context-path |
| 请求 400 | Content-Type 与字段类型 | 统一 application/json,检查时间字段 |
| 列表数据慢 | SQL 是否全表扫描 | 看 explain 结果,补联合索引 |
| 图片传不上 | uploadFile 路径与磁盘权限 | 确认上传目录存在且有写权限 |
5.2 答辩演示顺序与数据库设计问答点
演示的时候不要从登录页开始慢慢点,直接按三条路径切:第一条是用户提交带图片的报修单,展示提交成功后列表里出现“待受理”状态;第二条是管理员在后台点受理,回到小程序端展示状态变更为“处理中”,同时能看到处理时间被写入;第三条是维修员完成工单,用户在详情页看到“已完成”和评价入口。这条路径完整覆盖状态机的四个关键节点,比零散点按按钮更有说服力。数据库部分重点准备三块:表之间怎么保持关联,为什么状态历史要拆日志表而不是直接改主表,order_no 唯一索引起什么作用。打开演示视频录制时,hahaha 分别按这三条路径各录一段,配上简单字幕说明数据变化,论文中使用截图时直接截取管理后台和设备表结构。
5.3 一个低成本亮点功能:催单 + 日志表闭环
如果代码量和时间都还有富余,优先做一个“催单”功能。业务逻辑不复杂:当报修单状态为“处理中”且已超过两小时,用户端详情页显示催单按钮,点击后调用后端接口,在 repair_log 表插入一条“用户催单”记录,同时更新时间字段。这个功能代码量不大,但是能在答辩时展示你对业务细节的思考——它用到了日志表、状态判断和权限校验三个点,正好对应论文里的核心设计。实现时只需要在 RepairLog 表增加action字段,值为 remind 时前端展示特殊时间线图标。这个小功能比单纯多做一个 Excel 导出更能体现系统设计的完整性。
本文还有配套的精品资源,点击获取