SSM+微信小程序设备报修系统:从状态流转到联调排错完整实战
2026/9/13 8:34:59 网站建设 项目流程

简介:这套基于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_remarkfinish_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
请求 400Content-Type 与字段类型统一 application/json,检查时间字段
列表数据慢SQL 是否全表扫描看 explain 结果,补联合索引
图片传不上uploadFile 路径与磁盘权限确认上传目录存在且有写权限

5.2 答辩演示顺序与数据库设计问答点

演示的时候不要从登录页开始慢慢点,直接按三条路径切:第一条是用户提交带图片的报修单,展示提交成功后列表里出现“待受理”状态;第二条是管理员在后台点受理,回到小程序端展示状态变更为“处理中”,同时能看到处理时间被写入;第三条是维修员完成工单,用户在详情页看到“已完成”和评价入口。这条路径完整覆盖状态机的四个关键节点,比零散点按按钮更有说服力。数据库部分重点准备三块:表之间怎么保持关联,为什么状态历史要拆日志表而不是直接改主表,order_no 唯一索引起什么作用。打开演示视频录制时,hahaha 分别按这三条路径各录一段,配上简单字幕说明数据变化,论文中使用截图时直接截取管理后台和设备表结构。

5.3 一个低成本亮点功能:催单 + 日志表闭环

如果代码量和时间都还有富余,优先做一个“催单”功能。业务逻辑不复杂:当报修单状态为“处理中”且已超过两小时,用户端详情页显示催单按钮,点击后调用后端接口,在 repair_log 表插入一条“用户催单”记录,同时更新时间字段。这个功能代码量不大,但是能在答辩时展示你对业务细节的思考——它用到了日志表、状态判断和权限校验三个点,正好对应论文里的核心设计。实现时只需要在 RepairLog 表增加action字段,值为 remind 时前端展示特殊时间线图标。这个小功能比单纯多做一个 Excel 导出更能体现系统设计的完整性。

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

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

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

立即咨询