简介:这是基于微信小程序设计的宿舍报修系统毕业源码案例,后端采用SSM架构,Java语言编码,Mysql创建数据表,面向计算机相关专业学生及小程序开发者,可用于毕业设计、课程实践或项目参考。压缩包共664个文件,大小约36MB,类型覆盖java后端逻辑、vue页面、小程序wxml/wxss/js界面、SQL数据库脚本及mp4演示视频,另有bat一键安装脚本、png/svg图标、json/css等配置样式文件,整体目录清晰,便于按模块查阅和代码定位。当前已有391人学习下载,可作为项目实战参考。资源提供完整前后端代码和数据库初始化数据,从学生报修端到管理员处理端均有对应实现,覆盖报修提交、进度跟踪、后台派单、信息统计等典型功能,搭配演示视频可直观了解运行效果,适合需要快速上手SSM与微信小程序整合开发的学习者。 做宿舍报修系统这个选题,编号是 weixin183,后端走 SSM(Spring + SpringMVC + MyBatis),前端是微信小程序。乍一看和普通管理系统没什么区别,但真正动手后发现,这个小项目几乎把毕业设计常见的坑都踩了一遍:小程序登录、接口设计、OpenId 获取、图片上传、状态流转、部署上线。今天把这套系统的完整设计思路、核心代码逻辑和排错经验整理出来,给正在做同类课题、或者想用 SSM 做后端接口练手的同学一个可以直接复现的参考。
这套系统能解决什么问题?很简单:学生不用再跑到宿管办公室填纸质报修单,直接在微信小程序里拍照、描述问题、提交,维修进度随时可查;宿管和维修工通过后台管理页面分配工单、更新状态;最后学生还能对维修结果评价。对毕设来说,它包含了完整的用户体系、业务流、权限和状态流转,复杂度刚好,不会让人觉得水,又不会做到一半失控。
1. 项目整体设计思路:先想清楚要做成什么形态
1.1 需求端到端拆解:谁在用,每个角色关心什么
做任何系统前,先把角色和诉求列清楚。宿舍报修系统里我最后划出了三类角色:学生、维修工、管理员(宿管)。
学生侧的诉求很直观:提交报修时最好 30 秒内搞定,不用填一堆字段;提交后能看进度,知道师傅到底接没接单、修完没有。维修工侧关心的是手头还有多少单、单子在哪栋楼哪个房间、问题描述是否清晰。管理员则要处理派单、监督维修时效、抽查学生评价。
所以功能模块我拆成了:
- 学生端:个人信息维护、报修单提交(选宿舍、选问题分类、填描述、上传图片)、报修进度查询、待评价列表、历史记录。
- 维修工端:待接单列表、接单/完成操作、查看报修详情。
- 管理后台:用户管理、维修工分配、报修单审核与派单、统计报表。
这个拆分本身就是给数据库和接口设计打底。很多同学一上来就写代码,写到一半发现字段缺了或者接口对不上,根子就在需求没有拆够细。
1.2 技术选型为什么是 SSM + 微信小程序
后端选 SSM,说实话不是因为它最新,而是因为它在毕业设计语境里足够“标准”:Spring 管对象和事务,SpringMVC 负责接收前端请求,MyBatis 操作数据库。这套组合分层清晰,写起来比 Spring Boot 更能理解请求从进入控制器到数据库再返回 JSON 的完整链路。
很多同学问过:“接口是啥?”用这个项目举例最合适。接口就相当于后端对外提供的一个 URL,小程序通过 HTTP 请求这个地址,带上参数,后端处理完返回 JSON 数据。比如小程序提交报修单,就是 POST /api/repair/add,请求体里带上 dormId、description、imageUrl,后端返回{code: 200, msg: "提交成功", data: null}。前端拿到这个 JSON 再做页面跳转或提示。项目中我统一定义了 Result 对象,保证所有接口都返回一致结构,调试起来非常省事。
小程序端选择原生框架而不是 uniapp 或 Taro,主要是为了降低调试成本。原生框架里调试登录、上传文件、订阅消息都直接用微信开发者工具就能搞定,不需要额外编译链路。对毕设来说,稳定跑通比炫技更重要。
2. 数据库设计:一张报修单是怎么“活”起来的
2.1 核心表结构与字段设计
我数据库用的 MySQL 5.7,建了 6 张核心表:用户表、维修工表、报修分类表、报修单表、报修进度日志表、评价表。下面这张是报修单表的核心字段,也是整条业务流的主干。
CREATE TABLE `repair_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '报修单号', `student_id` int(11) NOT NULL COMMENT '学生用户ID', `worker_id` int(11) DEFAULT NULL COMMENT '维修工ID', `type_id` int(11) NOT NULL COMMENT '报修分类ID', `dorm_building` varchar(20) NOT NULL COMMENT '宿舍楼', `dorm_room` varchar(20) NOT NULL COMMENT '宿舍门牌', `description` text NOT NULL COMMENT '问题描述', `image_url` varchar(255) DEFAULT NULL COMMENT '现场图片', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待受理 1已派单 2维修中 3已完成 4已评价 -1已取消', `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_student` (`student_id`), KEY `idx_worker` (`worker_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字段设计时有几个细节值得说。第一,订单号不能用自增 ID 暴露给用户,容易被人遍历抓数据,我用时间戳加随机数生成order_no。第二,学生姓名、手机号不要冗余在报修单里,通过student_id关联用户表去查,避免用户改手机号后历史单子出现旧数据。第三,status用数字枚举而不是字符串,省空间、查询快,Java 枚举和前端展示时再映射成中文。
2.2 状态机设计:报修单生命周期
状态流转是这类业务系统最容易写乱的地方。如果不在数据库和接口层统一约定,学生端、维修工端、管理后台各维护一套状态判断,最后一定出 bug。我用一个枚举类把状态定义清楚:
待受理(0) -> 已派单(1) -> 维修中(2) -> 已完成(3) -> 已评价(4) | | | v v v 已取消(-1) 已取消(-1) 已取消(-1)规定只有待受理和已派单状态下的单子可以取消;维修中之后不能再取消。这个约束后端接口里要校验,不能只靠前端按钮隐藏。写接口时我习惯把所有允许的状态转换放在一张 Map 里,每次更新先判断当前状态是否允许跳到目标状态,不合法直接返回错误码:状态不允许变更。这个方法比到处写 if-else 清晰很多。
进度日志表也是容易被忽略的设计。修改状态时同时插入一条日志,记录操作人、操作时间、从什么状态变成什么状态。哪怕系统上线后不需要,毕设答辩时也能展示你对业务完整性的考虑。学生端“进度查询”直接查日志表按时间倒序返回即可。
3. 后端 SSM 核心实现:从登录鉴权到报修接口
3.1 微信登录与 token 鉴权(含踩坑)
小程序端登录流程有三步:前端调用wx.login()拿到临时 code,传到后端;后端拿着 code 到微信接口换 openid;后端生成自定义 token 返回给前端,前端存储 token 并在后续请求中带上。
后端代码简化后大概是:
public String wxLogin(String code) { // 1. 用 code 换取 openid String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code"; String result = restTemplate.getForObject(url, String.class); JSONObject json = JSONObject.parseObject(result); String openid = json.getString("openid"); // 2. 根据 openid 查用户,不存在则注册 User user = userMapper.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); // 默认角色为学生 user.setRole(1); userMapper.insert(user); } // 3. 生成 token 并存储到 redis,这里用项目里简单的 token 表 String token = UUID.randomUUID().toString().replace("-", ""); tokenMapper.save(user.getId(), token); return token; }这个环节踩过的坑主要有三个。第一,小程序的 appid 和 secret 不要写在前端代码里,凡是需要 secret 的操作一律放后端;不然别人反编译代码就能拿你的 secret 调接口。第二,wx.login返回的 code 只能使用一次,并且有效期通常只有 5 分钟,后端处理完要及时调用 code2session,不能把 code 存库后异步处理。第三是内容安全相关,获取 openid 是登录的基础,但登录后不能把所有接口都裸露,即使小程序没有传统 Cookie 概念,也要用自定义 token 做拦截器校验。
我自定义了LoginInterceptor实现 SpringMVC 的HandlerInterceptor,在preHandle里读取请求头Authorization,从 token 表查用户,查不到直接返回 401。配置拦截时注意排除登录接口/api/user/login,否则会把登录请求也拦掉。
3.2 报修业务接口与统一返回格式
接口设计我遵循一个原则:登录接口用小程序 code 换 token,业务接口统一走 JSON,文件上传单独走wx.uploadFile。这样做前后端联调时不用纠结参数格式混用的问题。
以新增报修单为例,控制层代码:
@RestController @RequestMapping("/api/repair") public class RepairOrderController { @Autowired private RepairOrderService repairOrderService; @PostMapping("/add") public Result add(@RequestBody RepairCreateRequest request, @RequestAttribute("userId") Integer userId) { if (request.getTypeId() == null || StringUtils.isEmpty(request.getDormRoom()) || StringUtils.isEmpty(request.getDescription())) { return Result.error("必填项不能为空"); } Long orderId = repairOrderService.addOrder(userId, request); return Result.success(orderId); } }所有接口返回统一Result { code, msg, data }。201 表示业务成功,500 表示系统异常,401 表示登录失效,业务参数错误用业务码 1001、1002 等。这样做的好处是前端可以统一封装request方法,通过code判断成败,不用为每个接口写异常处理。
Service 层处理业务时要注意事务。一个完整的报修单创建涉及生成单号、插入报修单、写入进度日志三个操作,必须加上@Transactional,否则中途报错会出现只有日志没有单子的情况。这是很多同学容易忽略的地方。
3.3 MyBatis 与 SpringMVC 的配合要点
SSM 项目最烦的一类问题就是 mapper 扫描不到、XML 里 SQL 写错、字段映射不对。我建议在项目结构上直接按 controller / service / mapper 分层,applicationContext.xml 中配置好MapperScannerConfigurer,扫描路径和 XML 资源路径保持一致。这个项目里我用注解方式实现 mapper 接口,XML 放在 resources 下同名目录,SQL 用动态标签写法,例如where条件中根据分类和状态动态拼接,方便后续扩展查询条件。
SpringMVC 部分需要特意说一下子@RestController与@Controller的区别。如果用了@Controller再配合@ResponseBody,返回的才是 JSON;直接写@RestController默认对象会走 JSON 序列化。这个项目统一用@RestController,控制层不再返回视图,天然适合前后端分离的接口场景。
数据库连接这块,MySQL 8.0 和 5.7 的驱动配置不一样。我用的是 MySQL 5.7,驱动类com.mysql.jdbc.Driver,连接 URL 上加了characterEncoding=utf8和useSSL=false,解决中文乱码和 SSL 警告。如果是 MySQL 8.0,要换成com.mysql.cj.jdbc.Driver,还要指定serverTimezone=Asia/Shanghai,否则时间字段查询会报错。
4. 微信小程序端:登录、提交、进度展示落地
4.1 新版用户信息获取方式(不可跳过)
很多同学在网上搜到老版本代码,用wx.getUserProfile或者<button open-type="getUserInfo">获取昵称头像,2022 年之后这套基本拿不到真实数据了,返回的要么是默认头像,要么是灰色昵称“微信用户”。这个项目里我直接按 2023 年后的实现方案处理:头像通过<button open-type="chooseAvatar">让用户主动选择,昵称通过input类型为nickname的输入框让用户填写,保存时再调后端更新用户资料。
小程序端获取登录状态的核心代码:
onLoad() { wx.login({ success: (res) => { wx.request({ url: getApp().globalData.baseUrl + '/api/user/login', method: 'POST', data: { code: res.code }, success: (resp) => { if (resp.data.code === 200) { wx.setStorageSync('token', resp.data.data); } } }); } }); }这里有个常见报错,很多同学会遇到:小程序获取登录后的微信用户失败,报错信息包含一串 wx 开头的数字。多数情况下不是登录接口本身的问题,而是开发者工具中“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”这个选项没勾,或者后端 session 接口返回延迟。排查顺序一般是:先开后端日志看请求有没有进来,再看 code 是否过期,最后看网络请求里的完整返回信息。
4.2 报修表单提交与图片上传
报修表单我设计得尽量轻:宿舍楼用 picker 选择、门牌号用 input、问题分类用 picker 绑定后台分类数据、描述用 textarea、图片用wx.chooseMedia选择后上传。图片处理有个容易踩的坑:如果直接把图片转 base64 塞进 JSON,请求体很容易超限,所以图片必须单独通过wx.uploadFile上传。
上传部分参考实现:
uploadImage(tempFilePath) { return new Promise((resolve, reject) => { wx.uploadFile({ url: getApp().globalData.baseUrl + '/api/file/upload', filePath: tempFilePath, name: 'file', header: { 'Authorization': wx.getStorageSync('token') }, success: (res) => { let data = JSON.parse(res.data); if (data.code === 200) { resolve(data.data); } else { reject(data.msg); } }, fail: reject }); }); }后端接收上传文件时注意两点:一是限制单个文件大小,比如 5MB,避免有人传大图把内存打满;二是把文件用 UUID 重命名后存储到指定目录,不要把用户原始文件名直接当存储名,否则会出现重名覆盖或中文文件名乱码问题。上传成功后保存一个相对路径到数据库,访问时再拼上服务器地址。
4.3 列表状态展示与下拉刷新
学生提交报修后,最关心的就是“现在什么状态了”。前端列表页我做成卡片式,每个卡片显示单号、宿舍房间、状态标签、报修内容和时间。状态标签的颜色和文字通过一个映射函数统一处理:
const STATUS_MAP = { 0: { text: '待受理', color: 'orange' }, 1: { text: '已派单', color: 'blue' }, 2: { text: '维修中', color: 'purple' }, 3: { text: '已完成', color: 'green' }, 4: { text: '已评价', color: 'gray' }, '-1': { text: '已取消', color: 'red' } };这里要和后端状态枚举严格保持一致,前后端各维护一份很容易出现错位。我实际开发时是在后端生成一个/api/repair/statusList接口,前端启动时拉取一次状态映射,而不是在前端硬编码,避免改状态时两端不同步。
列表页我加了enablePullDownRefresh,也就是下拉刷新。学生提交完报修单后,从详情页返回列表页也会在onShow生命周期里重新加载列表。微信小程序页面栈里onLoad只触发一次,很多同学发现提交后回到列表页数据没更新,就是因为只在onLoad里请求了数据。
5. 本地部署与常见问题排查
5.1 前后端分离部署步骤速查
整个项目是前后端分离的,后端是 Java Web 项目,前端是微信小程序。本地联调我推荐这样操作:后端启动在 8080 端口,小程序开发者工具中把 baseUrl 指向局域网 IP 的 8080,并且勾选“不校验合法域名”。这样调试速度快,不需要每次改动都上传服务器。
部署到生产环境时,后端打包成 war 包放到 Tomcat 的 webapps 目录,小程序端把运维域名配置成 HTTPS,然后在微信公众平台后台把 request 合法域名加白名单。需要注意:这个域名必须是备案过的,并且要支持 HTTPS,小程序里不能直接访问 http 地址。
数据库导入也很关键。我项目里附带了一个db_sql文件,导入前确认 MySQL 版本和字符集。如果导入时报“Unknown collation: utf8mb4_0900_ai_ci”,说明你用的是 MySQL 5.7 但 SQL 文件是从 MySQL 8.0 导出的,需要全局替换掉这个排序规则,或者直接用 5.7 对应的utf8mb4_general_ci。
5.2 高频报错与解决方案
这个项目从开发到跑通,我遇到频率最高的几个问题整理成了下面的表格,基本覆盖了 SSM 后端 + 小程序的典型坑:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 小程序请求一直 404 | 后端路径不对或项目没部署 | 先看后端控制台日志,再用浏览器直接访问接口测试 |
| 后端返回 401 | 请求头没带 token 或 token 过期 | 检查拦截器排除路径、前端 header 是否传了 Authorization |
| 登录时报“获取登录用户失败” | code 已过期、secret 配置错误、域名不合法 | 重新调 wx.login,确认 appid 和 secret 与小程序一致 |
| 数据库查询中文乱码 | 数据库字符集不是 utf8mb4 | 建库时指定DEFAULT CHARSET=utf8mb4,连接 URL 加 characterEncoding |
| MyBatis 报 “Invalid bound statement” | mapper 接口和 XML 没绑定 | XML 的 namespace 必须等于接口全限定名,接口方法名要和 XML id 一致 |
| 上传图片后无法访问 | 文件保存路径和静态资源映射冲突 | SpringMVC 配置<mvc:resources>映射上传目录,或用虚拟路径访问 |
权限拦截器是新手特别容易踩的坑。如果拦截器写了excludePathPatterns("/api/user/login")但登录接口还是被拦,大概率是路径模糊匹配写法有误,比如写成了/api/user/*,而实际请求是/api/user/login。SpringMVC 的路径匹配规则/*只匹配一层路径,/**才匹配多级路径。这类小问题看后端日志比看前端报错更直接。
6. 个人经验与可扩展方向
整个系统做完,我最大的体会是:毕业设计不一定非要多高级的技术栈,而是要把最基础的技术用完整、用规范。SSM 虽然已经是老框架,但它能让你真正理解 Spring 注入、事务、MyBatis 映射这些原理。代码结构上,我强烈建议给每个 service、mapper 都写清楚,不要把所有逻辑堆在 controller 里,否则后期改一个状态字段就要动三四个文件。
这个项目还能继续扩展的方向有几个。第一,引入 Spring Boot 版本,把 XML 配置替换成注解和 yml,技术栈向企业级靠拢。第二,接微信订阅消息,学生提交报修后,管理员派单时通过 subscribeMessage 给用户推送进度通知,体验会提升很多。第三,加一个简单的数据可视化,比如统计这周各楼栋报修数量、常用维修类型排行,答辩时展示效果很加分。
最后再分享一个小技巧:后端接口统一打印请求耗时日志,用 SpringMVC 拦截器实现。每次学生端反馈“小程序很卡”,你不用猜是前端脚本慢还是后端慢,看日志里每个接口的耗时就能定位。这个习惯放到任何项目里都适用。
本文还有配套的精品资源,点击获取