news 2026/9/14 1:43:10

基于微信小程序的设备故障报修系统:SSM框架实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于微信小程序的设备故障报修系统:SSM框架实践

简介:一套基于微信小程序与SSM框架(Spring+Spring MVC+MyBatis)完成的设备故障报修管理系统项目源码,面向计算机相关专业毕业设计、课程设计以及需要快速搭建内部报修平台的技术开发者。系统覆盖设备台账管理、故障单提交、维修任务分派、进度实时跟踪、历史记录统计、消息提醒和角色权限控制等核心业务,采用微信小程序作为员工端、Vue后台作为管理端、SSM提供数据接口,从报修到维修再到结果反馈形成完整闭环。资源包共1286个文件,核心包括127个Java后端类、138个Vue后台页面、181个JS逻辑文件、100个JSON配置,以及WXML/WXSS小程序页面、SQL数据库脚本和PNG/JPG图片素材,压缩包约20.64MB,目录按后端、管理端、小程序、数据库与文档分隔,便于按模块阅读和二次开发。已有145人学习或下载。包内还包含可执行启动脚本与Eclipse工程配置,能帮助使用者快速复现项目,并深入理解SSM接口设计、小程序前后端联调、权限控制等关键知识点。

1. 基于微信小程序的设备故障报修管理系统为什么还要用SSM框架

设备坏了,报修信息如果不留痕,后勤管理员连“哪个设备在修、修了多久”都说不清。微信小程序适合做这种低频但刚需的内部工具:不用装App,扫码就能提交故障,还能看处理进度。后端选SSM框架,是因为大量存量系统的实际代码就是Spring+SpringMVC+MyBatis,这套框架在“表单+流程+统计”项目里非常好写:Spring管业务对象,SpringMVC收请求,MyBatis绑定SQL。下面围绕报修单从用户提交、维修工接单到消息通知的完整链路,把图片上传、状态机和分页操作写具体。适合做微信小程序毕业设计的人,也适合企业后勤想把维修流程线上化的工程师参考。

2. SSM工程结构、报修数据表与小程序端目录设计

2.1 为什么报修系统要拆成Spring+SpringMVC+MyBatis三层

我一般会把后端按controller、service、mapper三层分包,算是SSM框架最标准的写法。Controller负责接收小程序请求和做参数校验,Service负责报修单的创建、状态变更、通知发送,Mapper只写SQL和对象映射。拿“提交报修”这个功能来说,Controller收到POST /api/repair之后,要完成图片保存、订单号生成、报修单插入三个动作,这三个动作之间没有互相依赖的就不该在Controller里串行写,而是放到Service的一个事务方法里。Spring这里解决的是对象创建和事务管理,SpringMVC解决的是URL到Java方法的映射,MyBatis解决的是Java对象和数据库字段的映射。很多人在SSM项目里把SQL写在Service里,这是要避免的:报修单列表查询、按状态统计这类SQL,放在对应的Mapper.xml里更好维护。

2.2 报修订单表设计:设备、报修人、维修工、状态都要留快照

数据库设计决定了后面状态流转好不好写。我一般会建五张表:用户表、设备表、报修单表、处理记录表、附件表。其中核心是repair_order,字段设计如下表。注意要把device_name冗余到报修单里,否则设备改了名字或者被删除,历史报修单再查询时就要做表关联。status用tinyint不用varchar,因为后面要按状态做GROUP BY统计。

表2-1 报修单核心字段

字段类型说明
idbigint报修单主键
order_novarchar(32)业务编号,展示给用户
device_idbigint设备id
device_namevarchar(64)设备名称快照
reporter_idbigint报修人id
assignee_idbigint接单维修工id
descriptiontext故障描述
imagesvarchar(1000)图片URL,逗号分隔
prioritytinyint1低、2中、3高
statustinyint0待接单、1处理中、2已完成、3已评价
create_timedatetime报修时间
finish_timedatetime完成时间

建表SQL中要把order_no设置成唯一索引,status和reporter_id建普通索引。order_no生成方式常见是“BX+yyyyMMdd+三位序号”,在Service里用数据库查询当天最大编号再加一,注意加锁或者加上唯一索引防重。建表SQL如下:

CREATE TABLE repair_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, device_id BIGINT NOT NULL, device_name VARCHAR(64), reporter_id BIGINT NOT NULL, assignee_id BIGINT, description TEXT, images VARCHAR(1000), priority TINYINT DEFAULT 2, status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME, KEY idx_status (status), KEY idx_reporter (reporter_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里使用utf8mb4,是为了兼容小程序端提交的表情符号,尤其是报修备注里有人会打emoji。如果用了utf8,插入时报错“Incorrect string value”,那多半就是字符集问题。MyBatis的Mapper.xml里在插入时要注意useGeneratedKeys="true" keyProperty="id",这样插入后就能直接拿到自增id,不用再查一次。

2.3 小程序端目录与后端包结构对齐,避免逻辑散落

小程序端如果直接用原生微信小程序开发,目录可以分成pages/login、pages/repair、pages/list、pages/detail,再单独用utils/request.js封装wx.request。如果用uni-app开发,页面结构也是一样的,区别是wx.uploadFile要换成uni.uploadFile,wx.request换成uni.request。但无论哪种,业务校验都不能放到小程序端。有人为了省事,在提交前判断状态下拉框,在js里拼状态名,后端不受这一套约束,最后抓包一看数据早就发到后端了。正确做法是小程序端只做展示,状态流转必须由后端Service完成。Backend端包结构建议这样做:

repair-backend/ src/main/java/com/example/repair/ controller/ RepairController.java FileUploadController.java service/ RepairService.java OrderNumberGenerator.java mapper/ RepairOrderMapper.java RepairOrderMapper.xml src/main/resources/ spring-context.xml spring-mvc.xml mybatis-config.xml

SSM看似繁琐,但这个结构的好处是查错路径清楚。如果修改报修单分页条件,去RepairOrderMapper.xml找select;如果改接单后的通知逻辑,去RepairService找。Spring的配置文件要分清:spring-context.xml扫描service和mapper,spring-mvc.xml只扫描controller。如果两个文件都扫到了service,就会出现同一个Service被两个容器代理,事务注解有的时候生效,有的时候失效,非常难查。

2.4 MyBatis的Mapper绑定失败问题可以这样排查

“Invalid bound statement (not found)”是SSM项目最容易遇到的新手错误。常见原因有三种:Mapper接口的方法名和XML的id不一致;接口类被Spring扫描了但XML没有被打包;命名空间namespace写错。第一次部署时,先检查target/classes目录下有没有RepairOrderMapper.class对应路径的RepairOrderMapper.xml。还有一些项目使用MyBatis的@Select注解,这个在报修系统里不建议滥用,多表关联和动态条件写XML更直观。我在管理端列表里一般会用动态SQL:

<select id="selectPage" resultType="com.example.repair.vo.RepairOrderVO"> SELECT * FROM repair_order <where> <if test="status != null"> AND status = #{status} </if> <if test="reporterId != null"> AND reporter_id = #{reporterId} </if> </where> ORDER BY create_time DESC </select>

这里用where标签自动处理AND前缀,不需要在每条SQL前面手写1=1。查询参数用状态、报修人、时间范围,分别对应小程序端“我的报修”和管理端“按状态筛选”。如果参数再复杂,再考虑用condition对象接收。

3. 用户提交报修:小程序图片上传与SSM后端入库

3.1 用wx.chooseMedia选图并用wx.uploadFile一张一张传

微信小程序从基础库2.21.0开始推荐使用wx.chooseMedia替代wx.chooseImage,它在安卓和iOS上返回的tempFiles都包含tempFilePath和size,报修照片一般限制9张,每张压缩后再传。小程序端代码可以这么写:

chooseAndUpload() { wx.chooseMedia({ count: 9 - this.data.imageList.length, mediaType: ['image'], sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (res) => { const tasks = res.tempFiles.map(f => this.uploadOne(f.tempFilePath)); Promise.all(tasks).then(urls => { this.setData({ imageList: this.data.imageList.concat(urls) }); }); } }); } uploadOne(filePath) { return new Promise((resolve, reject) => { wx.uploadFile({ url: `${getApp().globalData.baseUrl}/api/file/upload`, filePath, name: 'file', success: (res) => { const data = JSON.parse(res.data); if (data.code === 0) { resolve(data.data.url); } else { reject(new Error(data.msg)); } }, fail: reject }); }); }

这段代码里count限制的是剩余可选数量,sizeType用compressed,既省流量也让上传更快。Promise.all并发上传不能超过微信的10个并发,9张是安全值。name字段“file”必须和后端@RequestParam("file")一致。选择报修类型要是用radio单选框,直接放在提交表单里,和后端字段对应即可,不要单独上传。上传成功返回的图片URL会展示在页面上,供用户确认再提交报修。

表3-1 上传参数建议值

参数建议值说明
count9 - imageList.length剩余可选照片数
mediaType['image']只选图片,不选视频
sizeType['compressed']用压缩图,减少上传时长
maxUploadSize5242880单张不超过5MB

3.2 后端FileUploadController接收文件并生成访问链接

后端用SpringMVC的MultipartFile接收上传文件,需要先在spring-mvc.xml里配置multipartResolver。我一般限制单张5MB,避免有大图把小程序内存拖垮。代码如下:

@RestController @RequestMapping("/api/file") public class FileUploadController { @Value("${upload.dir}") private String uploadDir; @Value("${upload.url-prefix}") private String urlPrefix; @PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file) throws IOException { if (file.isEmpty()) { return Result.error("文件为空"); } String original = file.getOriginalFilename(); String ext = ""; int dotIndex = original.lastIndexOf('.'); if (dotIndex >= 0) { ext = original.substring(dotIndex); } String filename = UUID.randomUUID().toString().replace("-", "") + ext; File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, filename)); return Result.ok(urlPrefix + "/uploads/" + filename); } }

对应的multipartResolver配置:

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="5242880"/> <property name="defaultEncoding" value="UTF-8"/> </bean>

maxUploadSize这里是5MB。注意:CommonsMultipartResolver的bean id必须是multipartResolver,不能改名字,否则SpringMVC不认。如果出现“Required request part 'file' is not present”,优先检查name是否对应,再检查是否引入了commons-fileupload依赖。文件保存路径不要放在WEB-INF目录下面,否则通过URL访问不到,建议配一个独立的uploads目录,再用静态资源映射把/uploads/映射到磁盘目录。图片地址返回给小程序后还需要拼接完整域名,这就是urlPrefix的作用。

3.3 创建报修单的Service方法要为一个事务

图片上传成功只是第一步,点“提交报修”时,小程序会POST一个包含设备名称、故障描述、priority、images的JSON到后端。Controller要做的事很少,真正的逻辑在Service:

@Service public class RepairService { @Autowired private RepairOrderMapper repairOrderMapper; @Autowired private OrderNumberGenerator orderNumberGenerator; @Transactional(rollbackFor = Exception.class) public Long createRepairOrder(RepairCreateDTO dto, Long reporterId) { RepairOrder order = new RepairOrder(); order.setOrderNo(orderNumberGenerator.next()); order.setDeviceId(dto.getDeviceId()); order.setDeviceName(dto.getDeviceName()); order.setReporterId(reporterId); order.setDescription(dto.getDescription()); order.setImages(String.join(",", dto.getImages())); order.setPriority(dto.getPriority()); order.setStatus(0); repairOrderMapper.insert(order); return order.getId(); } }

这里把@Transactional放在Service层,不能放在Controller,原因是SpringMVC的Controller是单例包装的,事务代理不同,容易出现“没生效”。生成订单号和插入报修单在一个事务里,保证不会出现order_no一样的两条数据。如果服务器异常,订单号生成器里的计数和数据库插入都回滚,下次重新生成就好。还有,reporterId不要信前端传的,应该从拦截器设置在ThreadLocal或request attribute里的登录用户id。登录态用微信登录后自己颁发的token,每次请求带在header里。

3.4 真机预览和抓包时的请求排查方法

小程序开发者工具里能直接看到请求,但真机上出问题就要抓包。常见排查路径是先看“开发设置-服务器域名”是否把后端地址加入request合法域名和uploadFile合法域名。有人改了域名后不生效,就要清理缓存重新编译;也可以用charles抓包微信小程序,观察HTTP请求是否发出、响应状态码是多少。如果遇到上传图片时返回401,多半是token没有加到uploadFile的header里,wx.uploadFile不能用默认header,需要这样写:

header: { 'Authorization': getApp().globalData.token }

如果返回的是JSON但小程序的data字段解析失败,检查后端是否返回了纯HTML错误页,比如404或500页面,JSON.parse自然失败。这个时侯先把后端日志打开,看有没有NullPointerException或MapperException,比反复改小程序代码更有用。在SSM里给所有异常加一个全局@ControllerAdvice,统一返回{code: 500, msg: "系统异常"},小程序端拿到code非0就提示用户,避免白屏。

4. 工单状态机与微信订阅消息通知设计

4.1 报修单状态机:哪些角色可以把单子变成什么状态

如果报修系统只有一个上报和查看列表,那和问卷调查没区别。真正的核心在状态流转:用户报修后维修工接单,处理完改成已完成,用户再评价。把状态机定义清楚,后面不会因为需求加一个“加急撤回”就把代码改乱。以最常见的流程定义为例:

当前状态动作目标状态允许角色
0 待接单接单1 处理中维修工、管理员
1 处理中完成2 已完成维修工
2 已完成评价3 已评价报修人
0 待接单撤回-1 已取消报修人

我一般会在Service里提供一个统一的状态变更方法,先判断当前状态能不能执行目标动作,再执行update。禁止在各个Controller里直接写update set status,否则有人绕过校验把状态从0改成2。实现上可以用一个枚举类,只做校验用:

public enum StatusTransition { ACCEPT(0, 1), FINISH(1, 2), EVALUATE(2, 3), CANCEL(0, -1); private final int from; private final int to; StatusTransition(int from, int to) { this.from = from; this.to = to; } }

然后在Service里写changeStatus方法时,先遍历枚举看是否有匹配的from/to组合,再检查操作人角色,最后执行update。这样即使以后加了一个“重新打开”状态,也只需要在枚举里加一项,不需要在所有if分支里补逻辑。

4.2 用状态CAS替换“先查再做”,防住并发抢单

维修工列表页在待接单状态下会有多个维修工同时看到一条记录。如果Service里先select,判断status == 0,再执行update,那两个人都会查到status==0,最后都会执行成功,单被接两次。常见做法是把update语句本身作为校验条件:

<update id="accept"> UPDATE repair_order SET status = 1, assignee_id = #{assigneeId} WHERE id = #{id} AND status = 0 </update>

Service里这样调用:

int rows = repairOrderMapper.accept(orderId, operatorId); if (rows == 0) { throw new BizException("该报修单已被接单"); }

update影响行数为0时,说明当前状态已经不是0,直接拒绝。这种CAS思路比给表加锁更轻量,因为在Spring的Transactional里,你select到的旧值可能被其他事务隔离,而update的where条件能保证最终一致性。评价操作同理,把where改成status=2,防止用户重复评价。还有一点,接单时要把assignee_id和status一起更新,不要在Service里分两次update,否则两条SQL之间会有窗口期。

4.3 微信订阅消息:用什么模板、在哪里触发

进度提醒是用微信订阅消息,不是模板消息。用户在提交报修后,需要在小程序里主动调用wx.requestSubscribeMessage,用户同意一次,后端才有权限发送一条订阅消息。我一般把订阅动作放在提交报修成功后的回调里,这样用户刚完成操作,转换率比较高。

wx.requestSubscribeMessage({ tmplIds: ['模板ID'], success(res) { if (res['模板ID'] === 'accept') { console.log('订阅成功'); } } });

后端在状态变为“处理中”和“已完成”时调用subscribeMessage.send接口。这里需要注意:access_token要从微信接口获取,缓存7200秒,不能每次发送都去拿;推送接口的数据格式对字段长度非常敏感,特别是thing类型的value不能超过20个字。报修的device_name如果太长要截断,不然接口会报invalid message data。

4.4 处理记录表:状态变化的审计是详情页的底牌

光有报修单状态还不够,如果用户问“为什么修了这么久”,需要有人能回答单子什么时候被接走、维修工有没有更新过备注。所以状态变化一定要写repair_record表,字段包括id、order_id、from_status、to_status、operator_id、remark、create_time。Service里的统一变更方法这样实现:

@Transactional public void changeStatus(Long orderId, Long operatorId, int fromStatus, int toStatus, String remark) { int rows = repairOrderMapper.updateStatus(orderId, fromStatus, toStatus); if (rows == 0) { throw new BizException("状态变更失败"); } RepairRecord record = new RepairRecord(); record.setOrderId(orderId); record.setOperatorId(operatorId); record.setFromStatus(fromStatus); record.setToStatus(toStatus); record.setRemark(remark); repairRecordMapper.insert(record); }

这样状态变更记录和报修单更新在同一个事务里,不会出现库里的状态和日志对不上的情况。小程序详情页只要查repair_record列表,就能按时间正序显示“提交报修/维修工接单/维修完成/用户评价”。同时,管理端可以基于这张表做“平均接单时长、平均维修时长”的统计,比用报修单表更准。唯一的代价是多一次insert,对报修这种低频系统来说完全可以接受。

5. 管理端分页查询与SSM配置的几个必调参数

5.1 PageHelper的startPage只能作用于下一条SQL

管理端报修单列表要分页,SSM项目最常见的是用PageHelper插件。配置好依赖后,在spring的SqlSessionFactoryBean里加拦截器:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="plugins"> <array> <bean class="com.github.pagehelper.PageInterceptor"> <property name="properties"> <props> <prop key="helperDialect">mysql</prop> <prop key="reasonable">true</prop> </props> </property> </bean> </array> </property> </bean>

Service里这样写:

PageHelper.startPage(pageNum, pageSize); List<RepairOrderVO> list = repairOrderMapper.selectPage(query); PageInfo<RepairOrderVO> pageInfo = new PageInfo<>(list);

特别要注意:startPage之后如果紧跟着的不是你要分页的查询,而是另一个不同Mapper的查询,那分页就作用到错误查询上了。所以startPage和查询之间不要有任何别的MyBatis操作。reasonable=true这个参数建议打开:pageNum小于1时自动按1处理,pageNum超过总页数时按最后一页处理,能避免管理端手动输入page=999时返回空页面还报错。

表5-1 PageHelper参数调整建议

参数推荐值说明
helperDialectmysql指定数据库方言
reasonabletrue页码越界自动纠正
pageSizeZerofalse设为true会让pageSize=0查全部,除非有导出需求

5.2 导出Excel时先用GROUP BY做统计,再交给POI

管理端首页和导出报表都需要报修单统计。按状态统计可以用一条SQL:

SELECT status, COUNT(*) cnt FROM repair_order WHERE create_time BETWEEN #{start} AND #{end} GROUP BY status;

按天统计近7天报修趋势:

SELECT DATE_FORMAT(create_time, '%Y-%m-%d') as day, COUNT(*) cnt FROM repair_order WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY day ORDER BY day;

这两条SQL结果可以直接填充到前端图表,或者导出Excel。导出Excel建议后端用Apache POI生成xlsx,小程序端不要前端导出,因为微信环境生成文件再打开链路太绕。

5.3 分页慢时先看explain,再看索引

分页查询在前台是越查越慢,主要原因是MySQL在大offset下会扫描大量不需要的行。先跑一次EXPLAIN确认索引情况:

EXPLAIN SELECT * FROM repair_order WHERE status = 0 ORDER BY create_time DESC LIMIT 10, 10;

如果type是ALL,给(status, create_time)加联合索引。如果查询里还有设备ID过滤,就把device_id放到联合索引第一列。对于报修系统这种数据量,不需要做复杂的分页优化,但千万别在status字段上建一个单独的索引,再用status IN (0,1,2)查询,那索引基本不会走。另外insert语句一定要用useGeneratedKeys="true" keyProperty="id",这样插入报修单后order对象里就有id,后续要关联repair_record时不用再查一次。配置好这些,SSM这套结构在几千条报修单量级下完全够用,不需要折腾分布式事务。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 1:41:56

Hive拉链表设计与实现:数据仓库历史追踪方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 1:41:31

C语言实战项目解析:吃豆豆游戏开发与工程结构拆解

简介&#xff1a;这是一套用C语言编写的经典“吃豆豆”游戏源码&#xff0c;对应CSDN资源helios-4.1g压缩包&#xff0c;主要面向希望学习C语言游戏开发、复习数据结构&#xff0c;或者寻找课程设计参考的初学者。资源包内是完整工程&#xff0c;共42个文件&#xff0c;以8个c源…

作者头像 李华
网站建设 2026/9/14 1:40:20

斐波那契回调直线:技术分析中的黄金分割应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 1:34:56

基于YOLOv5的人群密度检测全流程实践

简介&#xff1a;这套基于YOLOv5的人群密度检测系统源码项目&#xff0c;面向目标检测、深度学习的开发者与研究人员&#xff0c;适合用来解决真实场景下的人群计数与密度评估问题。项目对标准YOLOv5做了多处改进&#xff1a;采用FasterNet主干网络替换原Backbone&#xff0c;结…

作者头像 李华
网站建设 2026/9/14 1:33:49

线下给监狱寄信效率太低怎么解决?白马信件小程序足不出户寄家书

频繁跑邮局寄信效率低怎么办&#xff1f;白马信件发信快&#xff0c;信件每日发出。很多家属每次寄信都要专程抽空前往邮局排队&#xff0c;来回耗费不少时间&#xff0c;想要简化寄信流程&#xff0c;可以选择白马信件小程序&#xff0c;在家线上完成家书代寄。线下到邮局寄信…

作者头像 李华