☰
Spring Boot+微信小程序设备报修系统:毕设级工单状态机与数据库设计实战
2026/10/21 6:25:21 网站建设 项目流程

1. 这个选题为什么值得做:从毕设撞车到有序交付

每年到了毕业季,计算机专业的同学都在忙同一件事:找毕设题目。你会发现一个很有意思的现象,凡是带"微信小程序"和"Spring Boot"两个词的项目,几乎每个班都有七八个人在做,为什么?因为这两个技术栈确实覆盖面广、就业认可度高、做出来又能跑得动,用来当毕业设计刚刚好。

但这个题目做一个能演示的Demo不难,难的是把业务逻辑讲清楚、把实现细节写扎实、在答辩时有话可讲。很多同学做完课程设计就扔了,到毕设答辩时老师一追问"你这个工单状态是怎么流转的""多个人同时接单怎么办""文件上传除了图片还能传什么",就答不上来了。

学校设备报修这个小程序,核心价值就四个字:流程闭环。学生扫码或打开小程序就能提交报修单,后勤维修工单按状态流转,管理员能看数据、做统计、催办、评价。听上去简单,但麻雀虽小五脏俱全,它涵盖了RBAC权限模型、工单状态机、微信登录鉴权、富文本/图片上传、消息通知提醒、数据报表聚合等一整套常见后端业务场景。

本文内容基于我在实际指导毕设项目X时的经验整理:技术方案、表结构设计、关键代码实现、踩坑记录,全部来自真实的开发过程,适配Spring Boot 2.x/3.x + 微信小程序云开发或本地后端联调的常见组合。前后端分离、模块化编排,方便你按需裁剪。

顺便说一句,如果你正在为毕设选题发愁,或者已经被"设备报修"这个题目选中但毫无头绪,这篇文章可以直接当作一份备查手册来用。我尽量用口语化的方式讲清楚每一步是怎么想的、为什么这样做,而不是给你扔一堆代码让你自己猜。

2. 整体设计与技术选型:先把"地基"打对

2.1 功能模块拆解:四个角色各管一段

设备报修系统看起来小,但角色划分非常清楚。通常一个完整的校园报修场景里,有学生、维修师傅、院系管理员、超级管理员四类人。有人可能会说"我做个简单点的,只分学生和管理员行不行",理论上可以,但答辩时功能性不足,老师大概率会问"维修师傅这个角色怎么体现师傅接单、完工、评价的流程",到时候再补就很被动。

我建议最小实现做到三角色:学生(提交人)、维修人员(接单人)、管理员(分配/监管/统计)。如果时间和精力富余,再加上院系维度进行数据隔离,但最初版本不必过度设计。

从功能形态上看,小程序端要有的页面包括:登录授权页、报修单填写页(选设备类别、填位置、描述问题、上传图片)、报修记录列表(按状态筛选)、报修详情页(进度时间线、评价入口)、个人中心(我的报修、我的接单、个人信息)。

管理端我建议直接用网页,也就是Spring Boot的Thymeleaf或者Vue做成的前台管理页,尽量不要在小程序里做管理功能,工具属性太弱。管理端要覆盖:用户管理、设备类别管理、报修单管理(列表、详情、指派)、维修人员接单、工单状态变更、统计报表。

这个CRUD看着多,实际梳理后在数据库层面也就是五六张核心表,后面我会把建表SQL直接给你。

2.2 技术选型的真实考量:别只图"流行"

Spring Boot作为后端框架,最大的优势是生态成熟、资料多、出问题随手一搜就有答案。版本选2.7.x是一个保守但稳定的选择,支持JDK8/11/17,很多老教程都还能用;如果学校要求较新环境,也可以上Spring Boot 3.x + JDK17,但要注意JDK8跑的旧代码可能有兼容问题。

小程序端用原生语言开发 + 微信开发者工具调试,这是最稳妥的方案。为什么要强调原生而不是选uni-app或Taro?不是说不可以用跨端框架,而是对于毕设而言,原生小程序框架在微信生态中的登录、订阅消息、上传文件等能力最直接,代码量也不大,调试排错时资料最容易找。

数据库用MySQL 8.0,ORM框架用MyBatis-Plus。MyBatis-Plus在毕设项目里太好用了,内置分页、条件构造器、逻辑删除,能省掉大量CRUD样板代码,而且它继承了MyBatis的灵活SQL,复杂统计时可以写原生SQL。这套组合几乎是当前Java毕设项目的事实标准。

另一件值得提及的事是:很多人纠结要不要用Redis。就报修场景来说,Redis不是必需的。用户量不大、并发不高、缓存热点少,硬上Redis只会增加部署复杂度,答辩时如果讲不清楚淘汰策略和缓存一致性,反而容易被追问。如果你实在想加一个"亮点",我觉得可以把Spring Security换成Sa-Token或JWT做登录鉴权,在小程序无状态接口鉴权场景里更贴切,也容易讲清楚。

2.3 部署与核心流程:一张图看懂数据怎么跑

我习惯先画清楚业务的流转路径,再把代码往里填。整个系统一次完整的报修是这么走的:

学生打开小程序,通过微信登录换取身份标识,提交报修单(包含设备名称、位置、故障描述、照片),后端写入数据库并把状态置为"待受理"。管理员在管理后台看到新工单,将其指派给某个空闲的维修师傅(也可以让师傅自行抢单),状态变为"维修中"。维修师傅通过小程序接单、维修、完成后上传维修结果照片并标记"待验收"。学生最后确认报修完成,打分评价,整个工单状态变为"已完结"。

这里有一个非常关键的环节:状态机设计。不要把"待受理、维修中、待验收、已完结"这四态简单做成字段存字符串,用状态机驱动流转,每一步都记录操作人、操作时间、操作备注,这在答辩时可以当作亮点讲出来。

如果觉得状态机这个概念过于抽象,想象成一条流水线:只有上一步做完贴好标,下一步的工人才能开始动工,中途谁也不能跳过某个工序。这种"不做越级操作"的约束,代码层面可以做成枚举 + 前置状态校验。

3. 数据库设计:五张核心表把业务锁死

3.1 实体关系梳理

实体关系不要画得太复杂,如果不是特殊需要,不建议引入中间关联表去做多对多。我最终落地的方案是:

  • 用户表:统一存储学生和维修师傅,用角色字段区分,避免多表继承的麻烦
  • 设备类别表:管理报修设备类型,比如"空调""投影仪""桌椅""网络设备",一个类别可以被多个报修单引用
  • 报修单表:核心业务表,记录位置、描述、状态、图片URL、分配人、处理人、时间线
  • 评价表:一对一关联报修单,存评分和文字评价
  • 操作日志表:记录报修单状态变更的历史轨迹

用户表里把学生和维修师傅统一到一个表,是很多同学容易纠结的地方。一开始会想"要不要分成学生表、职工表",但多表查询和权限分配的复杂度会上升,分成一张表加role字段是更优雅的处理方式。

3.2 建表SQL实战(含字段说明)

以下是我在项目X中实际用的一套表结构,为了便于阅读稍微精简了冗余字段:

CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', openid VARCHAR(64) NOT NULL UNIQUE COMMENT '微信openid', name VARCHAR(32) COMMENT '昵称', avatar VARCHAR(255) COMMENT '头像URL', role TINYINT NOT NULL DEFAULT 0 COMMENT '角色 0-学生 1-维修员 2-管理员', phone VARCHAR(20) COMMENT '联系电话', status TINYINT DEFAULT 1 COMMENT '状态 1-正常 0-禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE t_device_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '类别名称', icon VARCHAR(255) COMMENT '图标路径', sort_order INT DEFAULT 0 COMMENT '排序', status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备类别表'; CREATE TABLE t_repair_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '工单编号', category_id BIGINT COMMENT '设备类别ID', device_name VARCHAR(100) COMMENT '设备名称/编号', location VARCHAR(100) COMMENT '故障位置,如XX楼XXX室', description VARCHAR(500) COMMENT '问题描述', image_urls TEXT COMMENT '图片URL,多个用逗号分隔', status TINYINT DEFAULT 0 COMMENT '状态 0待受理 1维修中 2待验收 3已完结 4已取消', reporter_id BIGINT COMMENT '报修人ID', assignee_id BIGINT COMMENT '维修人ID', assign_time DATETIME COMMENT '接单时间', finish_time DATETIME COMMENT '完成时间', remark VARCHAR(255) COMMENT '管理员备注', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_status (status), KEY idx_reporter (reporter_id), KEY idx_assignee (assignee_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报修单表'; CREATE TABLE t_repair_evaluation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT '报修单ID', score TINYINT COMMENT '评分 1-5', content VARCHAR(300) COMMENT '评价内容', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评价表'; CREATE TABLE t_operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, operator_id BIGINT COMMENT '操作人ID', operator_name VARCHAR(32) COMMENT '操作人姓名', from_status TINYINT COMMENT '变化前状态', to_status TINYINT COMMENT '变化后状态', action_desc VARCHAR(255) COMMENT '操作描述', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';

字段细节有三个容易踩坑的位置需要提一下。

第一个是order_no生成规则,别用自增ID当工单号给学生看。建议生成方式:日期 + 随机数,如20250612001,或者"BX" + 时间戳 + 三位随机数。这样视觉上有"正式工单"的感觉,也方便按时间排序。

第二个是image_urls用逗号分隔存多图URL。正规做法应该用子表,但毕设场景多图需求不高,逗号分隔更简便。真正注意的点是后端要限制上传图片的大小和数量,微信小程序上传时wx.chooseMedia一次最多9张,而后端接口要加校验,防止单次请求过大导致超时。

第三个是状态字段使用TINYINT而不是VARCHAR。不要存"待受理"这种汉字字符串,虽然直观但对数据库不友好,排序、索引、变更都会遇到编码问题。颜色、标签这类展示层的东西,交给前端根据状态值去映射。

3.3 为什么要做操作日志表

操作日志表相当于"复读机",每次状态变更插一条记录。这件事在初版设计时容易被忽略,但做到后面你会感谢这张表。

一方面,从业务上看,老师和学生最关心的是"我的报修单到底经历了什么"。操作日志能让前端渲染出时间线:你提交了报修单、管理员分配给了某师傅、师傅已接单、师傅已完成维修。没有日志表,前端就只能根据几个时间字段拼凑,一旦中间有人改过状态或重新指派,时间线就会错乱。

另一方面,从答辩层面看,日志表是"数据可追溯"的典型体现。面试官或毕设评审老师问"如果师傅说没收到单,你怎么查",日志表就是最好的证据。代码上也不要手动去写日志,而是用Spring AOP或者状态变更的Service方法统一插入日志,这样不会出现"漏记"的情况。

4. 后端Spring Boot核心实现:把接口讲透

4.1 项目结构划分与依赖配置

不少同学一上来就喜欢按"Controller/Service/Mapper"三层建包,然后所有类全堆里面。真实项目建议加一层:在Controller之上用"facade"或者"strategy"去应对复杂业务变换。当然,毕设代码不追求过度设计,关键是把目录划分为:

com.example.repair ├── config // 全局配置:跨域、拦截器、上传配置 ├── controller // 接口层:小程序端和管理端 ├── service // 业务层:接口 + 实现类 ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 实体类 ├── dto // 请求/响应对象 ├── common // 通用响应体、异常处理、常量 └── utils // 工具类:JWT、日期等

依赖上,核心的pom依赖清单如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.37.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency>

Hutool是一个很推荐引入的工具包,生成随机数、日期处理、Excel导出都要用到它。在毕设代码里使用成熟工具类完全不丢人,相反能体现工程意识。

4.2 微信登录与鉴权:别被"登录"两个字吓住

小程序登录的标准流程是:wx.login拿到临时code,发送到后端,后端调用微信官方API的jscode2session接口换取openid。这个openid就是用户在小程序生态里的唯一身份证。

很多同学第一次做时会在"换取openid"这步卡住,其实核心代码就这么几行:

@PostMapping("/login") public Result login(@RequestBody LoginRequest req) { String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + req.getCode() + "&grant_type=authorization_code"; String result = HttpUtil.get(url); JSONObject json = JSONUtil.parseObj(result); String openid = json.getStr("openid"); if (StrUtil.isBlank(openid)) { return Result.fail("登录失败"); } // 查库,不存在则自动注册 User user = userService.getByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setRole(0); userService.save(user); } // 签发token String token = StpUtil.login(user.getId()); return Result.ok(token); }

这里有两个细节需要提醒。第一个是jscode2session接口对appid和secret的对应关系很严格,必须是小程序账号下的,千万别把公众号的secret填进来。第二个是不要把整个openid返回给前端存储,返回token就够了,前端每次请求带上token,后端通过token去查当前用户。

鉴权做三层就够了:Sa-Token拦截器统一校验登录态;基于角色的注解(如@SaCheckRole("admin"))限制管理端接口;细节业务判断放在Service层,比如"维修师傅只能操作分给自己的工单"。

4.3 提交报修单:状态机与防重是重点

学生提交报修单是后端写的第一个完整业务,也是最能体现代码习惯的地方。完整的处理链路是:接收参数、校验必填、生成工单号、设置状态、插入日志、返回单号。

实现的关键代码:

@PostMapping("/repair/submit") public Result submit(@RequestBody RepairSubmitDTO dto) { // 1. 基础校验 if (StrUtil.isBlank(dto.getDescription())) { return Result.fail("请描述问题"); } // 2. 读取当前登录用户 User current = (User) StpUtil.getSession().get("user"); // 3. 生成工单号:BX + 时间戳 + 随机三位 String orderNo = "BX" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + RandomUtil.randomNumbers(3); RepairOrder order = new RepairOrder(); order.setOrderNo(orderNo); order.setCategoryId(dto.getCategoryId()); order.setDeviceName(dto.getDeviceName()); order.setLocation(dto.getLocation()); order.setDescription(dto.getDescription()); order.setImageUrls(dto.getImageUrls()); order.setStatus(0); order.setReporterId(current.getId()); repairOrderMapper.insert(order); // 4. 写入操作日志 operationLogService.log(order.getId(), current.getId(), null, 0, "用户提交报修单"); return Result.ok(orderNo); }

有人会问"防重"怎么防?比如学生手抖连点两次提交,产生两条重复工单。最直接的办法是用order_no唯一索引兜底,加上前端按钮loading时禁用二次点击。更完善的做法是做一个"用户最近五分钟内是否对同一设备同一位置提交过未完结工单"的校验,代码也不复杂:

LambdaQueryWrapper<RepairOrder> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(RepairOrder::getReporterId, current.getId()) .eq(RepairOrder::getLocation, dto.getLocation()) .eq(RepairOrder::getStatus, 0); long count = repairOrderMapper.selectCount(wrapper); if (count > 0) { return Result.fail("您已有一个待处理的报修单,请勿重复提交"); }

4.4 状态流转:用枚举约束非法变更

维修状态流转的代码是答辩时的核心亮点。一个严谨的做法是定义状态枚举和合法流转表,避免代码里到处写魔法数字。

public enum OrderStatus { PENDING(0, "待受理"), PROCESSING(1, "维修中"), PENDING_ACCEPT(2, "待验收"), COMPLETED(3, "已完结"), CANCELLED(4, "已取消"); private final int value; private final String desc; OrderStatus(int value, String desc) { this.value = value; this.desc = desc; } }

合法的流转目标可以做成一个Map:

// 状态机规则:当前状态 -> 允许的下一状态集合 private static final Map<Integer, Set<Integer>> TRANSITIONS = new HashMap<>(); static { TRANSITIONS.put(0, Set.of(1, 4)); // 待受理 -> 维修中 / 已取消 TRANSITIONS.put(1, Set.of(2, 4)); // 维修中 -> 待验收 / 已取消 TRANSITIONS.put(2, Set.of(3)); // 待验收 -> 已完结 }

然后在统一的更新状态方法里做校验:

public boolean changeStatus(Long orderId, int targetStatus, Long operatorId) { RepairOrder order = repairOrderMapper.selectById(orderId); if (order == null) { throw new BizException("工单不存在"); } int current = order.getStatus(); Set<Integer> allowed = TRANSITIONS.get(current); if (allowed == null || !allowed.contains(targetStatus)) { throw new BizException("非法的状态变更:" + current + " -> " + targetStatus); } // 更新并记录日志 order.setStatus(targetStatus); if (targetStatus == 3) { order.setFinishTime(new Date()); } repairOrderMapper.updateById(order); operationLogService.log(orderId, operatorId, current, targetStatus, "状态变更"); return true; }

把状态机的代码写出来,绝不仅仅是代码量的问题。它反映的是你对业务流程的抽象能力,这是很多培训速成班教不出来的东西。状态机这套写法在后面的工单管理、退款流程里都能复用。

4.5 图片上传:路径规划与大小限制

微信小程序上传文件的接口是wx.uploadFile,对应后端需要提供一个接收MultipartFile的接口。Spring Boot原生支持,但需要做好三件事:静态资源映射、大小限制、文件名重写。

上传接口核心代码:

@PostMapping("/file/upload") public Result upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.fail("请选择文件"); } // 限制大小:5MB if (file.getSize() > 5 * 1024 * 1024) { return Result.fail("图片不能超过5MB"); } // 检查后缀 String originalName = file.getOriginalFilename(); String ext = StrUtil.subAfter(originalName, ".", true); if (!ListUtil.toList("jpg", "jpeg", "png", "webp", "gif").contains(ext.toLowerCase())) { return Result.fail("仅支持图片格式"); } // 文件名:日期目录 + UUID String datePath = DateUtil.format(new Date(), "yyyyMMdd"); String fileName = IdUtil.fastSimpleUUID() + "." + ext; String fullPath = "upload/" + datePath + "/" + fileName; File dest = new File(System.getProperty("user.dir") + "/" + fullPath); FileUtil.mkParentDirs(dest); file.transferTo(dest); // 返回可访问的URL return Result.ok("/" + fullPath); }

对应的,在application.yml里配置静态资源映射:

spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB web: resources: static-locations: file:upload/,classpath:/static/

上传文件路径不要使用绝对路径写死,用System.getProperty("user.dir")获取当前项目目录更通用。部署到服务器时,上传目录要单独挂在磁盘上,否则容器重建后文件就丢了。还有一点,file.transferTo(dest)之前必须确保父目录存在,否则会报NoSuchFileException,这是很多新手第一次传文件必踩的坑。

5. 小程序端开发要点:别让人看出这是"半成品"

5.1 页面结构规划

小程序端我推荐的页面结构是4个Tab:首页、报修、消息、我的。首页放常用功能入口和通知公告,报修页是核心功能,消息页展示工单状态更新通知,我的页面展示用户信息和历史记录。

为了不让"首页"空荡荡,可以在首页加一个设备分类九宫格,点击分类直接跳转到报修填写页并带过去类别ID。这样学生操作流程最短,维护起来也只是两个页面的简单跳转。

页面结构如下:

pages/ ├── index/index // 首页 ├── repair/addRepair // 提交报修 ├── repair/list // 报修记录列表 ├── repair/detail // 报修详情 + 时间线 + 评价 ├── user/profile // 个人中心 └── login/login // 授权登录页

5.2 请求封装与登录态维护

小程序的请求封装直接用wx.request包一层Promise,统一处理token和错误提示。封装核心逻辑:

const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token') wx.request({ url: getApp().globalData.baseUrl + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': token ? token : '' }, success: (res) => { if (res.statusCode === 401) { wx.reLaunch({ url: '/pages/login/login' }) reject(res) } else if (res.data.code === 200) { resolve(res.data.data) } else { wx.showToast({ title: res.data.msg, icon: 'none' }) reject(res) } }, fail: (err) => reject(err) }) }) }

封装的好处是,如果后续要切换成云开发或者改新接口,只需要改一个文件。token过期怎么办?在全局拦截器里统一跳转登录页,不要在每个页面单独写判断,否则代码重复率极高,答辩时读代码的人也头疼。

5.3 报修单填写页:表单校验别偷懒

报修单填写页是小程序端最核心的页面,各种细节直接决定体验。从用户角度思考,学生报修时最烦"描述不清楚"和"不知道设备叫什么"。所以表单设计上要做好引导:

位置字段用picker选择楼栋,再手动输入具体房间号,或者干脆用textarea自由填写并加上"示例:XX楼301教室"的占位提示。设备类别用radio或picker张列表,并支持"其他"选项,避免学生找不到具体类别时无法提交。

表单校验也不要只依赖后端,前端提前做一层校验能大幅提升体验。判断逻辑写清楚:位置不能空、描述不能小于10个字、至少上传一张图片。

图片上传那里,真正做到"拍完就能传"需要三步:wx.chooseMedia选择图片、wx.uploadFile传到底层后端、把返回的URL存到表单数据里。注意图片数量限制,最多9张,最后用逗号拼接提交。

5.4 工单详情页:时间线渲染带来的"高级感"

工单详情页是最能展示"系统不只是一个CRUD"的地方。后端已经存储了操作日志,前端要做的事情就是拉取日志列表并按时间排序渲染成时间线。时间线组件可用原生view+样式实现,不必引第三方UI库:

<view class="timeline-item" wx:for="{{logs}}" wx:key="id"> <view class="timeline-dot"></view> <view class="timeline-content"> <view class="timeline-title">{{item.actionDesc}}</view> <view class="timeline-time">{{item.createTime}}</view> </view> </view>

配合一些CSS的圆点和竖线,视觉上顿时高级不少。在答辩演示时,这条时间线既能证明流程闭环,又能展示你处理"过程性数据"的思路。

5.5 消息通知选型:订阅消息的"一次订阅一次发送"原则

小程序向用户发消息,官方推荐的是订阅消息。核心特点是"一次订阅一次发送":用户必须在提交报修时点击允许订阅,后端才能给该用户发送一次模板消息。

实现要点是:用户在提交表单时用wx.requestSubscribeMessage订阅"工单状态更新"模板;后端在状态变更时调用subscribeMessage.send发送。

wx.requestSubscribeMessage({ tmplIds: ['模板ID'], success: (res) => { // 用户点击了"允许",后端后续才能发一次 } })

这个功能的重要意义在于:学生报修后不用反复进小程序查状态,工单变成"维修中"或"已完成"时,微信会自动弹通知。虽然一次订阅只能有一次发送机会,但对毕设演示来说已经完全够了。

需要提醒的是,模板ID要到微信公众平台申请,模板内容要跟类目匹配,比如"设备维修进度通知"这种。个人主体的小程序不能申请太多接口权限,如果遇到审核问题,可以在答辩演示时降级为轮询查询或说明实现思路。

6. 管理端Web实现与统计报表

6.1 管理端页面搭建

管理端用Thymeleaf + Bootstrap做最简单直接,整套东西Spring Boot原生态支持,不需要额外引前端框架。如果觉得Thymeleaf过时,可以考虑把Vue单独拆出来跑,但部署时就多了一个Nginx或静态文件服务器,复杂度会上升。

管理端页面清单:

  • 登录页:管理员账号密码登录
  • 工作台/首页:今日报修数、处理中、今日完成、工单总数等指标卡片 + 最近工单列表
  • 报修单管理:表格/列表页面,支持按状态和日期筛选、指派维修员、查看详情
  • 用户管理:管理员对用户查询与状态禁用
  • 类别管理:设备类别添加、编辑、排序
  • 评价管理:查看评价明细

报修单列表那一页一定要做分页和筛选。后端用MyBatis-Plus分页插件,前端表格支持下拉筛选,界面看起来就更完善,也更接近企业实际后台的样子。

6.2 报表统计:答辩讲解时的加分项

统计报表是很多毕设项目的薄弱点。很多同学做完"增删改查"就觉得任务完成了,一张报表都不做,这是很可惜的。报修系统的报表做简单一点的有两张:

第一张是"本周每日报修量趋势"。按日期分组统计:

SELECT DATE(create_time) AS date, COUNT(*) AS cnt FROM t_repair_order WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY date

展示成一个折线图,就能看出周一到周五报修高峰的规律。前端用ECharts或是简单的HTML图表都行,后端只要提供数据接口。

第二张是"设备类别报修占比"。按类别统计数量:

SELECT c.name, COUNT(o.id) AS cnt FROM t_device_category c LEFT JOIN t_repair_order o ON c.id = o.category_id GROUP BY c.id

渲染成饼图,直观看出哪类设备问题最多,对后勤采购和维修排班有参考价值。

答辩时不要只说"我能展示图表",要说得出"这些数据能帮助后勤管理员做资源调度决策"这种有落点的话,老师对应用价值的感知完全不同。

6.3 管理员指派与监控:把权限边界划清楚

管理员指派维修师傅是管理端的核心动作。指派本质上是更新工单的assignee_id字段并把状态从"待受理"变成"维修中"。但从系统设计角度看,接口必须有权限校验——学生用户不能调用指派的API,维修师傅只能查看自己的工单。

用Sa-Token角色校验的方式:

@SaCheckRole("admin") @PostMapping("/admin/assign") public Result assign(@RequestParam Long orderId, @RequestParam Long assigneeId) { RepairOrder order = repairOrderMapper.selectById(orderId); if (order == null || order.getStatus() != 0) { return Result.fail("当前工单不可分配"); } order.setAssigneeId(assigneeId); order.setAssignTime(new Date()); order.setStatus(1); repairOrderMapper.updateById(order); operationLogService.log(orderId, StpUtil.getLoginIdAsLong(), 0, 1, "管理员指派给维修人员#"+assigneeId); return Result.ok(); }

还有一个容易忽略的点:维修师傅列表在指派下拉框里展示。如果维修师傅多,下拉框很长,可以加一个"闲忙状态"标记:统计每个维修师傅当前处理中工单数,超过阈值就不允许再分配。这个逻辑做成后端校验,而不是只靠前端提示,防止并发指派造成"一个师傅同时接到10单"的尴尬局面。

7. 常见问题与排查技巧实录

7.1 微信登录报错 errcode 40029

这个错误码是典型的"code无效"。排查思路依次是:检查前端wx.login是否拿到了code、检查appid和secret是否正确、检查后端请求微信接口时是否把url拼接传参的方式写错。很多同学把参数从POST体里传,但官方接口是GET + query参数,Hutool的HttpUtil.get(url)默认UTF-8没问题,关键是不要把参数写在body里。

7.2 图片上传成功后访问404

404基本都是静态资源映射问题。Spring Boot默认只映射classpath:/static/、classpath:/public/等目录,你手工创建的upload/目录不在这个范围内。解决方式在application.yml里加spring.web.resources.static-locations: file:upload/,classpath:/static/。还有一个小概率问题:IDEA里项目重启后upload目录被清空,这是IDE工作目录切换导致的,检查System.getProperty("user.dir")是否变成了新目录。

7.3 小程序请求被拦截"不在以下 request 合法域名列表中"

本地调试阶段,在微信开发者工具右上角勾选"不校验合法域名",点击详情-本地设置里打开。真机预览时一定要在微信公众平台配置合法域名,而且要HTTPS。如果没条件部署HTTPS,真机预览就会特别痛苦,建议用内网穿透工具提供一个HTTPS的临时域名,或者直接使用云主机+备案域名部署。

7.4 数据库连接乱码

MySQL 8.0连接串加useUnicode=true&characterEncoding=utf8mb4,表结构使用utf8mb4。如果前端展示的emoji变成了问号,一定是MySQL连接使用了旧版驱动或连接串没带utf8编码。MyBatis-Plus连接配置里还有一个需要注意的点:serverTimezone=Asia/Shanghai,否则Java 8的时间类型会报时区异常。

7.5 工单状态被跳转:非法流转没拦住

如果你发现工单状态异常跳跃,十有八九是状态流转方法里没有做前置状态校验。比如学生取消时直接update status=4,没有判断当前状态是不是0;明明已经进入"维修中"的工单,还能被再次"受理",数据就乱套了。解决方式就是前面写到的状态机校验,不能只靠前端按钮控制。

8. 踩坑之后的几点实战心得

整个项目从零到一做完大概需要三到四周时间,其中数据库设计占三天左右,后端接口开发占两周左右,小程序页面开发五天,联调和修Bug再占几天。节奏上,先把后端接口全部跑通拿Postman测试,再开始写小程序页面,否则联调阶段两边一起出Bug,排查效率会很低。

我在实际指导过程中发现,大部分同学出问题都出在同一个地方:太着急写页面和代码,表结构设计草草了事。一旦业务涉及状态流转和多角色数据隔离,后面改表结构就非常痛苦。所以我的建议是,开工第一周别碰代码,先画好ER图、列清楚角色权限、明确所有状态和流转边界,把所有接口签名先列出来,等设计文档自己觉得没有明显漏洞,再动手写代码。

还有一点,代码提交时要养成分模块提交的习惯。报修单接口、用户登录、文件上传分别提交,一个功能验证完再提交另一个。万一改出Bug还能回滚,答辩时看Git提交记录也是加分项。

最后的最后总结一个或许不那么起眼、但在实际演示中非常重要的细节:记得在小程序里做"空状态"处理。报修记录列表为空时,显示"暂无报修记录"插画和一个"去报修"按钮;工单详情页如果没有日志,显示"等待管理员受理"。这个细节花的代码量很小,但答辩演示时如果列表空白一片,观感就会很差。小而美的细节往往最容易被评审老师记住。

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

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

立即咨询