首页 / 资讯中心 / 文章详情

微信小程序+SSM宿舍报修系统全栈实战解析

微信小程序+SSM宿舍报修系统全栈实战解析 ★ FEATURED ARTICLE
宿舍报修这个场景在学生时代几乎是每周都会遇到的刚需灯管坏了、水龙头漏水、空调不制冷每次都得跑下楼找宿管填纸质单子然后等师傅上门中间完全不知道进度到哪一步了。我做这套基于微信小程序SSM的宿舍报修系统就是为了把这整个流程搬到线上。学生手机上报修、拍照上传维修工接单处理管理员全程可见每一步都有状态反馈不用再打电话催了。这个项目既适合做毕业设计也适合作为学习SSM框架和小程序开发的完整实战案例整条链路从前端界面到后端接口再到数据库设计全都覆盖属于短小精悍但五脏俱全的综合项目。先说下我的整体判断这类管理系统的技术难点不在于某个单一技术而在于把业务状态流转理顺。报修单不是简单的增删改查它要经历提交、待接单、维修中、待确认、已完成、已评价这一整套生命周期不同角色在不同节点做不同操作后端接口得围绕状态机来设计。把这条主线路想清楚剩下的都是体力活。下面我按照从设计到实现、再到排坑的顺序把整个项目的关键内容拆开讲希望能帮到正在做类似项目或者准备上手的朋友。1. 项目整体设计与技术选型拆解1.1 需求场景分析谁在用解决什么问题开发之前先要搞清楚系统服务的三类人分别需要什么。学生最关心的是提交方便、能看进度不想为了一个报修跑上跑下维修工最关心的是任务列表清晰、能快速知道去哪儿修、修什么管理员最关心的是全局统计、哪些宿舍报修多、哪个维修工处理慢、有没有超时未处理的工单。三类角色的诉求完全不同这决定了系统必须有明确的角色区分和对应的功能权限不能把所有功能堆在同一个页面上。我最初设计时把系统分成了三个端小程序端给学生用后台管理端给管理员用维修工则是单独一个角色视图嵌入小程序里。为什么维修工也放在小程序而不是做PC后台因为维修工大部分时间在移动中背着工具箱来回跑不可能坐在电脑前面操作。维修工直接用小程序查看任务、点击接单、拍照上传维修结果比PC端方便得多。这个决策也是实际调研了真实维修场景后确定的算是这个项目里比较关键的产品判断。实际开发中我还加了一个角色宿管。宿管不需要参与维修但需要确认学生是否真的在宿舍、以及协助核对报修单的真实性。不过后来发现宿管角色会让流程变长学生报修后还要等宿管确认才能到维修工那边增加了等待时间。最后我把宿管角色精简掉报修单由系统根据宿舍楼号自动路由到对应片区的维修工流程短了体验反而更好。1.2 技术选型为什么选小程序SSM很多人问过我为什么不用Spring Boot非要用SSM说实话选SSM不是因为它还流行而是因为这个项目本身定位是教学和毕设场景SSM三件套Spring、SpringMVC、MyBatis能让学习者更清楚地看到分层结构。Spring管Bean、SpringMVC管路由、MyBatis管数据库访问每一层各司其职对理解Web后端的基本原理特别有帮助。Spring Boot虽然省事但很多东西都被封装得太隐蔽反而不适合用来学习框架本质。再说小程序端。小程序相比安卓和iOS原生开发最大的优势是跨平台——安卓和苹果用户都能用不需要下载安装扫个码就能打开对学生来说几乎没有使用门槛。学校这种场景下让学生下载一个App专门报修根本不现实小程序用完即走没有任何安装成本。另一个好处是开发调试方便微信开发者工具直接改代码就能看到效果不用打包签名那一套对开发者的效率提升很明显。数据交互方面小程序通过HTTP请求跟后端对接后端返回JSON数据。这里有个要注意的点小程序对域名有白名单限制正式上线必须用HTTPS协议而且域名要备案。我自己第一次部署时在这个上面卡了两天开发环境关了域名校验感觉一切正常一到真机预览就请求失败最后才意识到是域名配置问题。后面我专门整理了一遍上线前的配置清单这个在第四节我会详细说。1.3 项目目录结构与代码分层后端代码我严格按三层架构来组织Controller层负责接收请求和响应数据Service层负责业务逻辑Mapper层DAO层负责数据库操作。以报修单模块为例报文从微信小程序发到RepairControllerController不写任何业务代码直接调用RepairService的方法Service内部处理完状态校验、数据组装等逻辑后通过RepairMapper操作数据库表。这样分层的好处是每一层都能独立测试出了问题能快速定位在哪一层将来如果要重构界面层业务代码完全不用动。前端的目录结构也要提前规划好。小程序这边我把页面分成两个主包student包和worker包分别放学生端和维修工端的页面。公共组件比如报修单卡片、状态标签、图片上传预览这些放在components目录里。公共工具类request.js封装了所有网络请求的入口统一处理token注入和错误码拦截这样每个页面不需要重复写请求逻辑直接调用封装好的方法就行。有的页面业务逻辑复杂比如报修单详情页要同时展示基本信息、处理记录、评价区域我会拆成多个组件的组合让每个组件只负责一个独立区块。2. 核心功能模块与数据模型设计2.1 三个角色的功能划分与权限设计权限设计是这类系统的地基。我用一张user表和role字段来区分三种身份学生role0维修工role1管理员role2。不用单独做权限表因为角色数量少、权限差异大用枚举字段判断足够。但我做了两件事来保证安全性一是小程序端的所有请求后端都要校验token且token里包含用户id和角色后端拦截器会解析token并验证角色是否有权限操作对应接口二是维修工只能看到自己负责片区的工单学生只能看到自己提交的工单这个归属关系在后端SQL查询时就做了限制不能只靠前端隐藏按钮来防越权。学生端的功能包括提交报修单填宿舍号、选择故障类型、写描述、拍照上传、查看我的报修列表、查看报修详情和进度、确认完成、对完成的工单进行评价。维修工端包括查看待接单列表、接单、查看分配给自己的工单、上传维修结果填写维修说明和图片、标记完成。管理员端的功能最多宿舍楼管理、维修工分配、工单管理改派、催办、关闭、数据统计报修量趋势、维修完成率、平均响应时间。这里我想提醒一个容易忽略的地方不同角色看到同一份工单数据展示的字段应该不一样。学生不用看到维修工的接单时间线维修工不用看到学生的评价内容避免影响接单积极性管理员需要看到全量数据但不能修改维修工填写的维修内容。所以后端在返回数据时要根据当前用户的角色对字段做裁剪而不是把整张表记录直接返回给前端。一个简单的做法是写一个RepairVO视图对象在Service层根据角色组装好不同的字段结构再返回。2.2 数据库表结构设计的关键思路数据库我设计了6张核心表用户表、报修单表、维修单表处理记录、评价表、通知表、宿舍楼表。用户表user是基础表字段包括id、微信openid、昵称、头像、手机号、角色、所属宿舍楼、创建时间。这里有个小细节学生的宿舍楼和宿舍号是分开存储的宿舍楼关联到楼栋表宿舍号存在用户表里。为什么不开一张宿舍表专门存所有宿舍因为学生换宿的频率不高而且报警修时需要把宿舍号冗余到报修单里防止学生修改宿舍号后旧工单查不到位置所以用户表存一个默认宿舍号报修单再存一份报修时的宿舍号两条信息在创建工单时保持一致后续学生改了默认宿舍也不会影响历史工单的准确性。报修单表repair_order是最核心的表字段如下字段类型说明idbigint主键order_novarchar工单编号自定义生成规则student_idbigint报修学生dorm_buildingvarchar宿舍楼dorm_roomvarchar宿舍号categoryvarchar故障类型水电/门窗/电器等descriptiontext问题描述image_urlsvarchar报修图片多张逗号分隔statustinyint当前状态0待接单/1维修中/2待确认/3已完成/4已取消assignee_idbigint接单维修工create_timedatetime提交时间accept_timedatetime接单时间finish_timedatetime完成时间工单编号我用了自定义生成规则前缀BX加日期再加随机数比如BX202506111023481234即使同一秒有多条并发提交也不会重复。生成的逻辑放在Service层用时间戳加计数器来保证唯一性避免直接依赖数据库自增主键作为业务编号暴露给用户。维修单表repair_record存处理过程一条报修单可以有多条处理记录维修工接单算一条上传维修结果算一条学生确认算一条。每一条记录都包含操作人、操作类型、操作时间、备注内容、图片。这样设计的好处是形成完整的操作日志链将来如果学生和维修工之间发生纠纷可以直接调出时间线上的每一步来核对事实。2.3 状态机设计报修单的生命周期状态管理是整个系统的灵魂。我把报修单的流转定义成5个状态待接单、维修中、待确认、已完成、已取消每条状态流转都有明确的操作方和触发条件。学生提交报修 → 状态变为待接单维修工点击接单 → 状态变为维修中系统记录接单时间维修工提交维修结果 → 状态变为待确认学生确认维修完成 → 状态变为已完成学生在待接单状态下可以取消或者管理员可以关闭异常工单 → 状态变为已取消我在代码里定义了一个状态转移表用if/else布尔矩阵来校验每个状态可以到达哪些目标状态。比如待接单状态只能到维修中或已取消不能直接跳到已完成。这个校验逻辑必须放在Service层统一处理而不能散落在各个请求方法里否则会出现前端调接口跳过中间步骤把工单状态改没了的问题。在实现状态流转时我还要求每次状态变更都往repair_record表插入一条操作记录同时更新报修单表的状态字段。这两个操作必须放到一个数据库事务里要么都成功要么都失败。如果不加事务可能出现记录表已经插入但主表状态没更新的脏数据这种Bug排查起来很费劲。我建议所有同学在做这类设计时都养成主表更新记录表插入同一个事务方法的习惯后面省心很多。还有一个细节是超时提醒当维修工超过2小时未接单时系统应该自动通知管理员人员介入处理。这个逻辑我最初想用定时任务实现后来发现对于这种量级的管理系统不需要上额外的调度框架直接用数据库查询加上定时扫描就够了。我写了一个简单的后台任务每隔10分钟扫描一次报修单表中状态为待接单且创建时间超过2小时的记录把消息推送到管理端弹窗列表。这个设计简单可靠但又非常实用。3. 实操过程与核心环节实现3.1 小程序端登录态管理与报修单提交微信小程序开发的第一步永远是登录。我采用的方案是前端调用wx.login()获取临时code把code发给后端后端拿着code去微信接口换取用户openid然后查数据库判断这个用户是否已注册。已注册就直接生成自定义token返回给前端未注册就先创建一个默认账号同时返回一个isNewUser标记前端拿到这个标记就跳转到完善资料页让学生填写姓名、学号和宿舍楼号。token我这里用了UUID生成随机字符串存到用户表的一个token字段里有效期是7天。没有用JWT原因是小程序场景下的token不需要携带用户信息去解析后端每次请求都从数据库读取用户信息虽然多一次查询但对于学生宿舍这种几十万级别的数据量完全没有性能压力反而实现更简单可靠。报修单提交是整个前端最核心的页面这里面有两个比较复杂的控件故障类型选择和图片上传。故障类型我用一个scroll-view横向滚动的按钮组分成了水电维修、门窗家具、空调照明、网络设备和其他每项对应一个图标。图片上传用的是wx.chooseMedia选择图片然后调用wx.uploadFile逐个上传到后端后端返回图片URL后把URL数组拼在一起随报修表单一起提交。这里有一个我踩过的坑wx.chooseMedia在上传前要先压缩图片一张3MB的照片直接上传在校园网环境下特别慢而且后端接收时也容易超时。我在前端循环上传前加了wx.compressImage压缩把图片压到800KB以内再传体感好了很多。这里给个代码思路前端提交报修单的核心逻辑submitRepairOrder() { if (!this.data.dormRoom || !this.data.category || !this.data.description) { wx.showToast({ title: 请填写完整信息, icon: none }); return; } const app getApp(); const params { dormBuilding: this.data.dormBuilding, dormRoom: this.data.dormRoom, category: this.data.category, description: this.data.description, imageUrls: this.data.uploadedImages.join(,) }; wx.request({ url: app.globalData.baseUrl /repair/submit, method: POST, data: params, header: { token: wx.getStorageSync(token) }, success: (res) { if (res.data.code 200) { wx.showToast({ title: 提交成功, icon: success }); setTimeout(() { wx.navigateBack(); }, 1500); } else { wx.showToast({ title: res.data.msg, icon: none }); } } }); }这个前端代码不复杂但有一个要点提交前要在前端把必填项校验好尤其是宿舍号格式我用正则校验了必须是类似3-502的格式避免用户乱填导致维修工找不到地方。3.2 图片上传从小程序到服务器的完整链路图片上传环节我单独拿出来讲因为它是报修系统里最容易出问题的一环。整条链路是这样的小程序端选择图片 → 压缩 → 调用wx.uploadFile将文件流POST到后端接口 → 后端接收MultipartFile → 保存到服务器磁盘目录 → 返回可访问的URL → 前端拿到URL后再随表单提交。后端接收图片的代码用SpringMVC的MultipartFile来处理核心方法RequestMapping(value /upload, method RequestMethod.POST) ResponseBody public Result uploadImage(RequestParam(file) MultipartFile file) { // 校验文件大小不能超过5MB if (file.getSize() 5 * 1024 * 1024) { return Result.error(图片大小不能超过5MB); } // 校验文件类型只允许图片格式 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); if (!Arrays.asList(.jpg, .jpeg, .png, .webp).contains(ext.toLowerCase())) { return Result.error(不支持的图片格式); } // 按日期分目录存储避免单目录文件过多 String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); String fileName UUID.randomUUID().toString().replace(-, ) ext; String dirPath UPLOAD_BASE_PATH / datePath /; // 创建目录 File dir new File(dirPath); if (!dir.exists()) { dir.mkdirs(); } try { file.transferTo(new File(dirPath fileName)); String url /upload/ datePath / fileName; return Result.success(url); } catch (IOException e) { e.printStackTrace(); return Result.error(上传失败); } }这里有几个关键点文件名不能用用户原文件名要用UUID重新生成否则两个用户上传了相同名字的图片会互相覆盖目录按日期分文件夹存放方便后期排查和清理过期文件图片的访问URL需要做静态资源映射在SpringMVC配置里把/upload/**路径映射到磁盘目录否则浏览器无法直接打开返回的URL。内存溢出问题也要提前预防。我一开始把临时文件直接存在Tomcat的临时目录里结果运行几天后磁盘快满了排查半天发现是文件上传后transferTo没成功但仍然残留了临时文件。后来我调整了策略在配置文件里指定了上传大小限制和临时目录位置并且定时清理超过30天且没有关联到任何报修单的孤儿图片。3.3 后端接口SSM三层的落地写法后端的核心接口主要有以下几个接口路径方法功能角色/repair/submitPOST提交报修单学生/repair/myListGET我的报修列表学生/repair/detailGET报修单详情所有人/repair/cancelPOST取消报修学生/管理员/repair/pendingListGET待接单列表维修工/repair/acceptPOST接单维修工/repair/finishPOST提交维修结果维修工/repair/confirmPOST确认完成学生/repair/evaluatePOST评价学生/repair/statisticsGET统计数据管理员我以提交维修结果接口为例展示一下Service层的核心逻辑这是整个项目里业务最重的接口Service public class RepairServiceImpl implements RepairService { Autowired private RepairOrderMapper repairOrderMapper; Autowired private RepairRecordMapper repairRecordMapper; Override Transactional(rollbackFor Exception.class) public Result finishRepair(Long orderId, Long workerId, String handleResult, String handleImages) { // 1. 查询订单检查是否存在 RepairOrder order repairOrderMapper.selectById(orderId); if (order null) { return Result.error(报修单不存在); } // 2. 校验当前状态必须是维修中 if (order.getStatus() ! 1) { return Result.error(当前状态不允许执行此操作); } // 3. 校验操作人必须是该订单的接单维修工 if (!order.getAssigneeId().equals(workerId)) { return Result.error(无权操作该报修单); } // 4. 更新主表状态维修中 - 待确认 RepairOrder updateOrder new RepairOrder(); updateOrder.setId(orderId); updateOrder.setStatus(2); updateOrder.setFinishTime(new Date()); repairOrderMapper.updateById(updateOrder); // 5. 插入处理记录 RepairRecord record new RepairRecord(); record.setOrderId(orderId); record.setOperatorId(workerId); record.setOperatorRole(1); record.setActionType(2); record.setRemark(handleResult); record.setImages(handleImages); record.setCreateTime(new Date()); repairRecordMapper.insert(record); return Result.success(维修结果提交成功); } }这段代码核心体现了两个原则一是所有接口都要做状态校验和权限校验不能只从前端约束二是状态更新和记录插入之间加了Transactional事务注解确保数据一致性。实际操作中我发现很多人在写这类接口时容易漏掉第3步的归属校验如果一个维修工知道订单ID就能操作别人的订单这在生产环境是不可接受的安全漏洞。3.4 轮询与通知让各方被动收到消息状态更新之后怎么让相关人员及时知道是这类系统体验好坏的分水岭。学生提交报修后维修工需要收到新单提醒维修工接单后学生需要看到进度变化维修完成后管理员需要收到完成通知。小程序没法像App那样常驻后台收推送用微信订阅消息需要用户授权且还有模板限制最关键的是个人主体小程序无法申请订阅消息接口权限。所以我的方案是前端在几个关键页面做轻量轮询报修详情页每隔5秒拉一次订单详情检测状态变化后自动刷新界面并把状态变化以Toast方式提示出来。这个方案很朴素但实际体验并不差而且完全不需要额外成本。为了减少无意义的请求我只在报修详情页做轮询列表页靠下拉刷新来更新数据这样请求量控制在一个合理的范围内。管理端和维修工端的新工单提醒我则是做了个简单的站内消息表当新报修单产生时系统自动往对应维修工和管辖管理员的notice表插入一条未读消息前端在tabBar的角标上显示未读数量。维修工打开小程序时先请求未读消息数这个不用实时推送只要进入小程序时拉取一次就够了。这套机制虽然不如WebSocket即时但在宿舍报修场景里完全够用而且开发量小得多。4. 常见问题与排查技巧实录4.1 高频问题速查表我在开发和调试过程中遇到了一批高频问题整理成表格分享给大家这些都是网上很难直接搜到答案的细节问题现象根本原因解决方案真机预览请求失败没配置合法域名或没开HTTPS开发工具勾选不校验上线前配置HTTPS域名并备案图片上传后无法访问静态资源映射没配SpringMVC配置addResourceHandlers映射磁盘路径登录后token不生效拦截器没放行登录接口拦截器白名单配置/login、/upload等公共接口维修工列表能看到别人的工单SQL查询没有加归属条件所有工单查询都要带worker_id或building条件提交报修按钮点了没反应前端没做防重复提交提交时加loading状态请求期间禁用按钮时间字段显示成数字数据库datetime被序列化成时间戳配置Jackson格式化全局date格式小程序自动退出登录token过期后没做自动刷新后端返回401时前端自动跳转登录页并发提交报修订单号重复使用时间戳随机数碰撞增加数据库唯一索引并发重试机制这里重点展开说第一个域名配置。微信小程序在开发模式下可以勾选不校验合法域名本地联调时后端接口跑在http://localhost:8080也能通。但一旦用真机预览或者发布体验版所有请求都必须走HTTPS域名而且域名必须在小程序后台配置为request合法域名。如果前端请求的URL是IP地址加端口小程序会直接拦截报url not in domain list。我建议在项目一开始就把baseUrl抽成全局变量开发环境用一个值、生产环境用另一个值切换环境只需要改一个地方不要在代码里硬编码。4.2 项目上线前要做的几件事项目开发完成到上线中间还有一些事情必须做。第一是数据库备份策略学生报修数据虽然不涉及支付但属于服务流程数据丢了会出大问题。我写了一个简单的shell脚本每天凌晨用crontab自动mysqldump备份到指定目录保留最近30天的备份文件。这个投入产出比极高有一次我手动误操作删了一条测试数据直接从昨天的备份里恢复一点损失都没有。第二是接口安全性。除了token校验我在上线前加了一个简单的请求防刷策略同一个token在1分钟内对同一个接口的请求次数超过30次就返回错误。这个小功能用拦截器加Redis计数器就能实现但如果没有Redis也可以用数据库计数数据量小的时候无所谓。第三是日志记录。我用Log4j按天输出日志文件记录了每个关键接口的入参、出参、耗时。排查问题时看日志是最高效的手段特别是状态流转类的Bug靠猜永远找不到症结必须看日志中接口被调用的真实顺序和参数。我给所有重要的接口都加了一个统一的接口耗时日志切面在SpringMVC拦截器的afterCompletion方法里计算处理时间打印到日志中哪个接口慢了或者报错了一眼就能在日志里定位。第四是权限数据初始化。管理员账号不能是写死在代码里的我用了数据库种子数据的方式在项目启动时通过Spring的InitializingBean检测管理员表是否为空为空就自动插入默认管理员账号密码用MD5加盐存储。这样项目在别人机器上部署时不用手动往数据库里插数据直接启动就能跑起来降低了复现成本。这个细节在毕业设计答辩和项目演示时也是一个加分项。4.3 一套实用的前后端联调技巧联调阶段最容易消耗时间我总结了一套自己的调试方法。第一步是所有接口先在后端写好接口文档我用的是原生方式在代码里使用Swagger注解启动项目后自动生成API文档页面前端同学直接看文档就知道接口入参出参不用反复问后端。第二步是规范错误码我定义了一套统一的返回体code为200表示成功400表示参数错误401表示未登录403表示无权限500表示服务器异常。前端统一在request封装里对错误码做拦截处理401直接跳登录页其他错误码弹Toast提示。前端不用在每一个请求里都写一套错误处理逻辑这也是整体开发提速的关键。第三步是养成看Network面板的习惯。微信开发者工具的Network面板跟浏览器的DevTools很像可以看到每个请求的请求头、请求参数、响应数据、耗时。前端排查问题时第一件事不是看代码而是看请求到底成功没有、返回了什么内容、状态码是多少。很多所谓的前后端联调问题其实就是请求没带token或者参数格式不对导致的看Network面板几十秒就能定位。我见过太多人遇到问题先乱改代码其实认真看一遍请求和响应问题的原因往往非常明显。还有一个联调的小技巧是mock数据。后端接口还没写好时前端可以用wx.setStorageSync把假数据写到本地缓存页面开发时优先从缓存读取mock数据等后端接口调通后切换成真实请求。这样前端和后端可以并行开发不用等接口完成后再动手写页面。我在项目初期用这个方式把核心页面的前端界面全部先做完了后来只花了很少的时间对接真实接口整体进度快了不少。结尾一点实操后的个人体会做完这套宿舍报修系统我最大的感受是这类管理系统的难点真的不在技术而在对业务流程的理解和细节的打磨。状态流转、角色权限、数据一致性这些才是项目里真正需要动脑子的地方。代码本身只是把你想清楚的逻辑翻译成机器能执行的语言如果你在写代码之前没有把报修单什么时候能取消、维修工能不能改派、学生确认后还能不能反悔这些问题想明白代码写得再花哨也没用。最后再分享一个小技巧一定要在项目里加上全局异常处理。SpringMVC提供了ControllerAdvice注解可以用一个类统一捕获所有Controller层抛出的异常把未预期的异常转成统一的错误返回格式避免用户看到裸的堆栈信息。我在这个项目里就用了这个机制再加上自定义的业务异常类Service层所有校验失败直接throw new BizException(当前状态不允许此操作)统一由全局异常处理器转成JSON返回给前端代码干净利落排查错误也方便。这套做法我从这个项目之后就一直沿用算是踩过几次坑后留下来的最有价值的经验之一。这套系统后续还想扩展的方向也有不少比如对接企业微信通知、增加维修耗材库存管理、用图表展示月度维修趋势等等。做这类项目最有趣的地方就是它像搭积木基础框架搭好了想加什么功能都很顺畅。如果这篇文章能帮你顺利完成这个项目那我这半天字就没白敲。遇到具体问题欢迎在评论区交流。
阅读完成 · 觉得有帮助?
咨询建站