做这类课设项目的学生应该不少,宿舍报修系统是个非常典型的选题。表面上看就是个"提交工单、处理工单"的小功能,但真正把前后端完整跑通、让小程序端能正常展示报修进度、让维修人员能接单派单,涉及的技术点相当密集:小程序页面生命周期、SSM框架的分层协作、微信登录换取用户身份、图片上传、数据状态流转,还有联调时的域名校验和接口排查。这套基于SSM的宿舍报修系统,我用微信小程序做用户端,后端用Spring + SpringMVC + MyBatis,跑通之后既能直接交作业,也能作为简历上的完整项目经历。下面把这套系统的需求拆解、数据库设计、核心接口实现、小程序端踩坑记录,以及源码怎么快速跑起来,一次性说清楚。
1. 项目概述与需求拆解
1.1 这个系统解决的核心痛点
宿舍报修这个场景,传统流程一般是:学生发现灯坏了、门锁坏了、水龙头漏水,去楼下值班室填一张纸质单,或者口头跟宿管说一句,然后等维修师傅有空了再上门。这个模式最大的问题是信息断层:报修单到底有没有提交上去?维修师傅有没有看到?修到哪一步了?学生完全不知道。宿管那边单子多了也容易丢,尤其是期末高峰期,几张纸弄丢了,学生等一周都没人来修,然后就开始投诉。
小程序版本的报修系统,核心就是把这条线变成可视化的闭环。学生在小程序里选故障类型、填位置、拍照上传,提交后就能看到工单状态从"待接单"变成"维修中",再到"已完成"。维修师傅在自己的小程序端或者后台能看到待处理工单,按紧急程度排序处理。管理员则能看整体数据,比如哪栋楼报修最多、哪个维修师傅处理速度慢、哪些报修超时了。信息一旦在线化,责任就清楚了,扯皮自然就少。
这个项目适合谁?如果你是刚学完Java Web、想做个完整项目练手的人,或者正在选毕业设计题目,再或者想拿来当作简历里的"全栈实践",这套系统都很合适。它的复杂度刚好卡在"该有的都有、但不至于失控"的位置:表数量不多,接口逻辑清晰,小程序端也不算花哨,可扩展空间又很大。
1.2 用户角色与功能清单
整个系统的用户角色在需求分析阶段就要定清楚,不然后面接口权限会乱。我把角色拆成三类:
- 学生/报修人:微信授权登录后,能提交报修单(选类型、写描述、传图片)、查看自己的报修记录、查看工单状态、对完成的工单进行评价和确认关闭。
- 维修师傅:能看到自己被分配的工单,或从待接单池里抢单,支持调整状态(已接单、维修中、已完成),填写维修反馈。
- 管理员/宿管:维护楼栋、故障类型、维修师傅账号,有权重新分单、查看全部工单、处理超时工单、看统计报表。
功能梳理清楚之后,前后端的页面和接口就有了依据。我把功能清单压缩成一张表,可以直接用来检查自己做的系统有没有漏掉核心功能:
| 模块 | 学生端 | 维修端 | 管理端 |
|---|---|---|---|
| 登录 | 微信授权登录 | 账号密码登录 | 账号密码登录 |
| 报修 | 提交工单、拍照上传、催单 | 查看待接工单、接单 | 强制分单 |
| 处理 | 查看进度、确认完工 | 更新状态、填写反馈 | 查看进展、超时提醒 |
| 管理 | 消息通知 | 个人工作量 | 用户管理、类型管理、报表 |
| 评价 | 打分、评价内容 | 查看评价 | 查看汇总评价 |
有一点值得提醒:很多第一次做这类系统的同学,容易忽略角色权限的控制,所有用户登录后都能调所有接口。虽然演示起来没问题,但如果要做成真正能看的项目,建议在拦截器里按照角色做接口访问控制,这一步在答辩时很加分。
2. 技术选型思路与数据库设计
2.1 为什么用SSM而不是直接上Spring Boot
现在很多新项目上来就是Spring Boot,确实configurations省心很多。但我这里还是坚持用SSM,理由很实际:SSM(Spring + SpringMVC + MyBatis)的结构更显式,Controller、Service、Mapper分层清清楚楚,每个注解的作用都摆在你面前。对于学生和刚入门的人来说,Spring Boot把太多东西自动配置好了,你反而不清楚项目是怎么跑起来的。面试或答辩时,老师问一句"SpringMVC的请求流程是怎么走的",如果你的项目是SSM,你很容易接住;如果是Spring Boot,往往就只能答个"自动配置"然后卡住。
用SSM做这套系统还有一个好处:WEB-INF下的文件结构、web.xml配置、applicationContext.xml和spring-mvc.xml,每个文件都有存在的意义。你可以真实感受到一个Java Web项目怎么从Servlet演进到框架整合。以后转Spring Boot,你理解起来也会更快。
配置层面,典型的三件套:
- spring-mvc.xml:开启注解驱动、配置视图解析器、静态资源放行。
- applicationContext.xml:扫描Service和Dao、配置数据源和事务管理器。
- mybatis-config.xml:别名、驼峰映射、Mapper扫描。
这三个文件之间的分工,其实就是一个项目的"骨架":MVC管请求分发,Spring管业务对象生命周期,MyBatis管SQL映射。
2.2 数据表设计:从业务模型到建表语句
系统核心涉及用户、报修工单、故障类型、工单日志、评价几张表。我实际建表时的核心字段如下,这也是整个系统最需要想清楚的部分。
用户表(sys_user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| open_id | varchar | 小程序微信openid,学生登录专用 |
| role | int | 角色:1学生 2维修工 3管理员 |
| username | varchar | 登录名,维修和管理员用 |
| password | varchar | 密码 |
| phone | varchar | 联系电话 |
| building | varchar | 楼栋信息,学生填写 |
| room | varchar | 宿舍号 |
| status | int | 是否启用 |
报修工单表(repair_order)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| order_no | varchar | 工单编号,如BX20250601001 |
| user_id | int | 报修人 |
| type_id | int | 故障类型 |
| description | text | 故障描述 |
| images | varchar | 图片路径,逗号分隔 |
| address | varchar | 具体位置 |
| status | tinyint | 0待接单 1已接单 2维修中 3已完成 4已取消 |
| assignee_id | int | 接单人 |
| create_time | datetime | 提交时间 |
| finish_time | datetime | 完成时间 |
| level | tinyint | 紧急程度 |
故障类型表(repair_type):id、name、sort。这个表很简单,但别忘了,因为小程序端的报修表单要动态加载故障类型下拉框。
工单日志表(order_log):记录状态变更的历史,比如谁在什么时间把工单从待接单改成了维修中。这个表是答辩时的亮点,因为它体现了"可追溯"的设计思想。
建表时最容易踩的坑是两个:一是状态字段没有默认值,导致插入时程序报错;二是没有任何时间字段,后面想看超时工单完全无从查起。建议create_time、update_time、finish_time这些字段从一开始就加上。
SQL示例可以这样写:
CREATE TABLE repair_order ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, type_id INT, description VARCHAR(500), images VARCHAR(1000), address VARCHAR(255), status TINYINT DEFAULT 0 COMMENT '0待接单 1已接单 2维修中 3已完成 4已取消', assignee_id INT, level TINYINT DEFAULT 1, create_time DATETIME, finish_time DATETIME, KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3. 后端核心接口设计与实现
3.1 SSM分层:Controller-Service-Mapper怎么配合
这套系统的后端接口,我习惯按模块拆:登录模块、报修模块、工单处理模块、统计模块。以"提交报修"为例,完整走一遍SSM三层的代码组织方式。
Controller层只负责接收参数、调Service、返回JSON,不在里面写业务逻辑:
@RestController @RequestMapping("/api/repair") public class RepairController { @Autowired private RepairService repairService; @PostMapping("/submit") @ResponseBody public Result submit(@RequestBody RepairOrder order, HttpSession session) { Integer userId = (Integer) session.getAttribute("userId"); if (userId == null) { return Result.error("未登录"); } order.setUserId(userId); int id = repairService.submitOrder(order); return Result.success(id); } }Service层承载核心业务,比如生成工单编号、校验用户、初始化日志:
@Service public class RepairServiceImpl implements RepairService { @Autowired private RepairOrderMapper orderMapper; @Autowired private OrderLogMapper logMapper; @Override @Transactional public int submitOrder(RepairOrder order) { order.setOrderNo("BX" + System.currentTimeMillis()); order.setStatus(0); order.setCreateTime(new Date()); orderMapper.insert(order); logMapper.insert(new OrderLog(order.getId(), 0, "提交报修")); return order.getId(); } }Mapper层就是MyBatis的接口加XML。注意这里我加了@Transactional,为什么?因为submitOrder要同时写工单表和日志表,任何一个失败都要回滚,绝不能让工单创建成功但日志没写进去,那样后面状态追踪就废了。
Controller返回的统一Result对象也很关键。我建议统一成{code: 0, msg: "success", data: {...}}的结构。这样小程序端解析数据就只需要关注code值,判断成功失败,不用每个接口都调整解析逻辑。这个习惯在联调时能省大量时间。
3.2 工单状态流转:从一个状态机说起
报修工单不是简单的新增、查询,它有一个明确的生命周期。我在设计的时候就规定死了状态的流转路径:
- 待接单(0)→ 已接单(1)
- 已接单(1)→ 维修中(2)
- 维修中(2)→ 已完成(3)
- 待接单(0)→ 已取消(4,仅管理员操作或学生提交后2小时内可取消)
这个状态机为什么要提前定清楚?因为如果你不在后端做状态校验,就会出现"学生直接调接口把待接单改成已完成"这种离谱操作。我在Service层加了一个状态校验方法:
private static final Map<Integer, List<Integer>> TRANSITIONS = new HashMap<>(); static { TRANSITIONS.put(0, Arrays.asList(1, 4)); TRANSITIONS.put(1, Arrays.asList(2)); TRANSITIONS.put(2, Arrays.asList(3)); } public void changeStatus(int orderId, int targetStatus) { RepairOrder order = orderMapper.selectById(orderId); List<Integer> allowed = TRANSITIONS.get(order.getStatus()); if (allowed == null || !allowed.contains(targetStatus)) { throw new BusinessException("非法的状态流转"); } // 执行更新并写日志 }状态机是我强烈建议每个做这类系统的人都写的部分。它把模糊的业务规则变成可执行的代码,也让系统显得专业。答辩时你拿出这个设计来讲,老师基本不会在业务层面挑刺。
3.3 微信登录与Token处理
小程序端没有传统的账号密码,而是通过wx.login()获取临时code,然后传到后端,后端再调用微信的jscode2session接口换取openid。这里的核心问题是:openid就是用户的唯一下标,你要用它去查本地用户表,如果查不到就自动注册一个新用户。
需要特别注意的是,微信登录接口的调用需要配置小程序的AppID和AppSecret。我在第一次调试时,因为这个Secret配置错误导致一直报错,排查了半天。建议在applicationContext.xml配置文件中把这两个值用占位符替代,部署时再改成正式值,避免源码泄露后被人恶意调用。
拿到openid后,我建议在后端维护一个Token,存到Redis或者数据库,返回给小程序端。小程序后续请求都带上这个Token,后端拦截器校验通过后再放行。这里有一个高频问题:直接用微信的session_key当登录态可以吗?可以但不推荐,因为session_key有自己的生命周期和更新机制,业务登录态应该自己控制。
4. 小程序端实现与踩坑记录
4.1 页面结构与列表加载更多
小程序端的页面结构不会太复杂,我分成这几块:首页(推荐+待办统计)、报修提交页、报修记录列表页、工单详情页、个人中心页。
报修记录列表用的是小程序最常见的"下拉刷新 + 上拉加载更多"模式。这是网络热词里经常被搜到的点,因为很多人第一次写列表分页时,直接一次性把数据全查出来,导致页面卡顿。正确做法是后端接口接收pageNum和pageSize两个参数,返回总条数和当前页数据:
@GetMapping("/list") public Result list(@RequestParam int pageNum, @RequestParam int pageSize, @RequestParam(required = false) Integer status) { PageHelper.startPage(pageNum, pageSize); List<RepairOrderVO> list = orderMapper.selectList(status); PageInfo<RepairOrderVO> info = new PageInfo<>(list); return Result.success(info); }小程序端配合onReachBottom触底加载:
onReachBottom() { if (this.data.pageNum * this.data.pageSize >= this.data.total) { wx.showToast({ title: '没有更多了', icon: 'none' }); return; } this.setData({ pageNum: this.data.pageNum + 1 }); this.loadList(); }有个坑必须说:小程序请求接口时,如果后端返回的JSON里用了Long类型的id(比如雪花ID),小程序端的number类型精度不够,超过16位就会丢精度。我遇到时页面数据能查到,但是点进去详情显示不出来。解决方案很简单,后端返回时把id转成String。这个坑在真实项目里经常遇到,值得记住。
4.2 报修表单与图片上传
报修表单里最有技术含量的是图片上传。用户可能选多张照片,我们需要调用wx.chooseMedia选择图片,然后一张张传给后端。上传时要注意,不能直接用wx.uploadFile把文件传到服务端就算了,后端还要处理文件保存路径、生成访问URL、把URL存到工单记录里。
我这里给一个可以复用的处理思路:后端创建一个独立的上传接口/api/common/upload,用MultipartFile接收文件,将文件保存到本地指定目录,同时返回可访问的完整URL:
@PostMapping("/upload") @ResponseBody public Result upload(@RequestParam("file") MultipartFile file, HttpServletRequest request) { if (file.isEmpty()) { return Result.error("文件为空"); } String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = UUID.randomUUID().toString().replace("-", "") + suffix; String realPath = request.getServletContext().getRealPath("/uploads/"); File dir = new File(realPath); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(dir, fileName)); String url = "/uploads/" + fileName; return Result.success(url); }这里需要处理两个细节:一是对上传文件做类型校验,不能什么文件都收;二是当图片数量多时,前端要控制并发,小程序端最常见的问题是用户一次性选了9张图,然后前端用for循环并发上传,把后端的Tomcat默认线程池打满,导致请求超时。稳妥做法是做一个队列,同时最多上传3个,完成一批再传下一批。
4.3 顶部导航栏与动态标题
小程序端的顶部导航栏高度,是一个容易被忽略了但实际很影响体验的参数。不同手机的胶囊按钮位置不一样,如果你用自定义导航栏,就需要通过wx.getMenuButtonBoundingClientRect()拿到胶囊按钮的位置,再动态计算导航栏高度。这个在很多机型适配时相当关键。比如我一开始直接写了个固定高度,结果在全面屏手机上,自定义标题被状态栏挡住了,按钮点不到。后来统一用下面的公式计算:
const menu = wx.getMenuButtonBoundingClientRect(); const system = wx.getSystemInfoSync(); const navBarHeight = (menu.top - system.statusBarHeight) * 2 + menu.height;还有个实用小技巧:报修工单详情页可以动态设置标题,比如用wx.setNavigationBarTitle({ title: '工单号' + orderNo }),这样用户分享页面时,别人一眼就能看到这是哪个工单。做这个功能时别忘了在页面配置文件或者js里设置enableShareAppMessage,不然分享按钮出不来。
4.4 联调阶段怎么用抓包工具排查问题
前后端联调是大学生做课设最痛苦的一步,尤其是后端API报错但小程序端只显示"request:fail"。这种情况下,我强烈建议学会用抓包工具(比如Charles)查看小程序发出的请求。注意这只是本地调试手段,完全不涉及任何特殊网络配置,纯粹是电脑上的调试工具。
用法很简单:开启抓包后,你操作小程序,就能看到它发出的每个请求的URL、请求头、参数、返回的JSON数据,快速定位问题是出在前端参数传错了、后端报异常了,还是根本没有发起请求。排查思路:
- 看请求有没有发出,没发出就是前端代码逻辑问题。
- 看请求URL对不对,有没有404或者405。
- 看请求参数,传参是中文字符串还是JSON,格式是否和后端接口匹配。
- 看响应内容,如果是后端500错误,再去看后端控制台的具体异常栈。
这里我碰到最高频的一个问题,是请求头里没带Content-Type: application/json,导致后端用@RequestBody接参数时一直报HttpMessageNotReadableException。用小程序的wx.request提交JSON时,必须显式设置header:
wx.request({ url: BASE_URL + '/api/repair/submit', method: 'POST', header: { 'Content-Type': 'application/json' }, data: orderData, success(res) { ... } });如果不加这一行,小程序默认发的是application/x-www-form-urlencoded,后端解析不到JSON对象。
5. 源码跑通指南与二次开发扩展
5.1 拿到源码后从零跑起来的五个步骤
很多人拿到一套源码,最头疼的是不知道从哪里开始。我这套系统的运行步骤,其实也是所有SSM项目和小程序项目通用的流程,照着做就能跑通:
第一,准备环境。装好JDK 1.8、Maven 3.6、MySQL 5.7或8.0、Tomcat 8.5,以及微信开发者工具。JDK版本不要装太新,比如JDK 17配Tomcat 8.5会有兼容问题,建议JDK 8最稳。
第二,导入数据库。用Navicat或者命令行执行项目根目录下的sql/dormitory_repair.sql脚本,它会自动建库建表并插入初始数据。初始数据里自带了一个管理员账号、两个维修师傅账号,登录密码一般是123456,具体看项目说明文件。
第三,改配置文件。打开jdbc.properties,把数据库地址、用户名、密码改成你自己的。再把wx-config里的AppID和AppSecret换成你的小程序账号的。如果你暂时没有小程序AppID,可以先不用微信登录功能,直接用测试账号登录接口,但建议还是去微信公众平台注册一个个人开发者账号,免费的。
第四,启动后端。用IDEA打开项目,配置Tomcat,把项目部署到Tomcat并启动。启动后浏览器访问http://localhost:8080/api/health,如果能返回正常JSON说明后端起来了。
第五,导入小程序项目。在微信开发者工具中导入前端源码目录,填写你的AppID,然后在app.js里把BASE_URL改成你的后端地址。注意本地开发时,小程序默认不允许请求http接口,需要在开发者工具"详情-本地设置"里勾选"不校验合法域名"。不过这只是开发环境,上线时域名必须是HTTPS且在小程序后台配置过。
5.2 可以继续扩展的方向
这套源码跑通后,我不建议直接交差就完事,哪怕只是为了学习,也可以试着扩展几个方向,给自己加加分:
- 消息通知:工单状态变化时通过小程序订阅消息推送给学生,这是很实用且常见的功能,能在"用户粘性"方面讲故事。
- 统计报表:基于订单表按楼栋、按维修类型做聚合统计,用ECharts在小程序里画出饼图和柱状图。这个扩展能直接体现数据分析能力。
- 超时自动处理:用定时任务扫出超过48小时未完成的工单,自动转给管理员,避免工单卡死。
- Redis缓存:把高频访问的故障类型列表和待接单列表缓存起来,减少数据库压力。虽然宿舍规模下用不上,但思路可以写进项目文档作为亮点。
我自己实际做这套系统时,最后悔的一件事是没有从一开始就留出order_log这个扩展点,后面加上去要改一堆代码。你现在在做的话,建议把日志表从一开始就建好,这会让系统显得完整很多。
还有一个经验,就是代码和数据库字段的命名一定要有统一规范。我早期是userName和user_name混着用,后来排坑排到崩溃,最后还是全改成驼峰命名并在MyBatis里开启驼峰映射才彻底解决。建议你从一开始就统一用驼峰,并且每次改表结构,马上同步更新XML映射文件,不要拖,一拖就出问题。
最后再分享一个小技巧:如果调试时频繁改动后端代码,不要每次都手动重启Tomcat,用IDEA的DevTools或者装一个JRebel插件,能自动热部署,开发效率直接翻倍。这套系统的源码结构不复杂,理清前后端数据流之后,你完全可以在此基础上加入自己的功能,把它变成真正属于自己的实战项目。