ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序+SSM宿舍报修系统设计与实现:状态机与接口实战

微信小程序+SSM宿舍报修系统设计与实现:状态机与接口实战 简介这是面向高校毕业设计场景的微信小程序宿舍报修系统完整源码基于SSM后端框架与MySQL数据库实现。系统针对宿舍报修信息登记、处理状态跟踪、管理效率提升等需求提供小程序端与后台管理端适合计算机相关专业学生参考学习或直接作为毕设案例二次开发。压缩包共664个文件、约36.06MB主要包含98个Java后端源码、89个Vue前端页面、60个JS逻辑文件、20个WXML与21个WXSS小程序页面样式以及JSON配置、SQL数据库脚本、BAT一键启动脚本、MP4操作演示视频和大量SVG/PNG图标素材目录结构较完整便于按模块检索。目前已有392人学习下载资源附带环境安装与启动脚本、数据库初始化脚本和演示视频可帮助快速跑通项目理解SSM与微信小程序前后端交互流程同时提供页面样式与交互实现示例对毕业设计写作和答辩展示均有参考价值。1. 宿舍报修系统为什么用「微信小程序 SSM」先看清这套毕设值不值得做很多在校生看到「基于微信小程序宿舍报修系统的设计与实现 ssm 后端」这个选题第一反应是又要做界面又要做接口工作量不小。实际上这套组合最典型的价值不在页面而是它把微信小程序登录、SSM 后端接口、MySQL 数据表和角色状态机串成了一个完整闭环非常适合拿来练手或做毕业设计。做之前先想清楚你的时间应该花在定义接口和状态流转上而不是一味挑样式框架。下面从数据库、后端接口、小程序前端到联调排查按我实际做这类项目的顺序展开。2. 宿舍报修系统数据模型设计四张表撑起学生、维修工与管理员三大角色报修系统的业务本质是“谁提交、谁处理、处理到什么程度”所以数据模型不需要追求表多而是要把角色和状态表达清楚。我一般会先画三张主表加一张字典表用户表、宿舍楼表、报修单表、报修跟踪表。这样的好处是后端写查询时不需要在报修单表里冗余一大堆用户字段状态变化也有地方留痕。先定这四张表的字段再写 Mapper 会非常顺。2.1 角色与权限划分三个角色各自的报修闭环宿舍报修系统至少要撑起三个角色学生、维修工、管理员。学生只能提交报修、查看自己的单子、在待受理时取消维修工只能被分配后处理单子并把状态从处理中改到已完成管理员负责分派、驳回和统计。权限字段用 TINYINT 的 role 表示1 学生、2 维修工、3 管理员而不是用 Spring Security 这类重框架因为这套系统后端核心是 SSM 三个框架的协作毕设里把权限留给拦截器做更直观也更容易在答辩时讲清楚。为什么不在用户表里直接存宿舍楼名和房间号因为宿舍楼会有维修工默认归属拆成 t_building 后学生通过 building_id 关联管理员统计报修单时也能按楼栋聚合。房间号仍然存在用户表加上唯一约束避免同一学生开多个账号。这些细节在答辩时都是加分项。2.2 核心表结构与建表 SQL报修单的状态字段是灵魂下面给出一组能直接跑起来的 MySQL 建表语句使用 utf8mb4 字符集避免小程序端传进来的 emoji 表情把插入弄崩。注意密码字段建议存 MD5 而不是明文openid 单独一列因为微信小程序登录换取的用户标识要和学号账号解耦。CREATE DATABASE IF NOT EXISTS dorm_repair DEFAULT CHARACTER SET utf8mb4; USE dorm_repair; CREATE TABLE t_building ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 宿舍楼ID, name VARCHAR(32) NOT NULL COMMENT 楼栋名如 1栋, repairer_id INT DEFAULT NULL COMMENT 默认维修工ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_name (name) ) ENGINEInnoDB COMMENT宿舍楼表; CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID, username VARCHAR(32) NOT NULL COMMENT 学号/工号, password VARCHAR(64) NOT NULL COMMENT 密码(MD5), nickname VARCHAR(32) DEFAULT COMMENT 姓名, role TINYINT NOT NULL DEFAULT 1 COMMENT 1学生 2维修工 3管理员, openid VARCHAR(64) DEFAULT COMMENT 微信openid, phone VARCHAR(20) DEFAULT COMMENT 手机号, building_id INT DEFAULT NULL COMMENT 宿舍楼ID, room_no VARCHAR(16) DEFAULT COMMENT 房间号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB COMMENT用户表; CREATE TABLE t_repair_order ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 单ID, order_no VARCHAR(32) NOT NULL COMMENT 报修单号业务编号, user_id INT NOT NULL COMMENT 报修学生ID, repairer_id INT DEFAULT NULL COMMENT 维修工ID, title VARCHAR(64) NOT NULL COMMENT 报修标题, description TEXT COMMENT 问题描述, images VARCHAR(1000) DEFAULT COMMENT 图片路径逗号分隔, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待受理 1处理中 2已完成 3已驳回, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, finish_time DATETIME DEFAULT NULL COMMENT 完成时间, PRIMARY KEY (id), KEY idx_status (status), KEY idx_user (user_id) ) ENGINEInnoDB COMMENT报修单表; CREATE TABLE t_repair_track ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 记录ID, order_id INT NOT NULL COMMENT 报修单ID, oper_type TINYINT NOT NULL COMMENT 操作类型1提交 2分派 3开始 4完成 5驳回, operator_id INT NOT NULL COMMENT 操作人用户ID, remark VARCHAR(255) DEFAULT COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB COMMENT报修跟踪表;建表语句里有几个字段需要特别说明。t_user 的 uk_openid 唯一约束很关键同一个微信号只能绑定一个账号否则小程序端每次登录可能会生成重复用户。t_repair_order 的 status 字段用 0 到 3 的整数配合 idx_status 索引管理员后台按状态筛选时不会全表扫。t_repair_track 的 oper_type 和表名本身承担了审计功能当维修工说“我没改过状态”时你把跟踪记录拉出来就能看到每一步是谁操作的。校验逻辑上外键这里我故意不建因为毕业设计里用外键会让插入变得很啰嗦实际项目也常常用应用层保证引用关系。但必须在 Mapper 的插入语句里校验 user_id、repairer_id 是否真实存在。你在答辩时可以说这是“用应用层替代外键换取并发插入时的性能”。2.3 状态机与前端的映射待受理、处理中、已完成和驳回怎么写报修单的状态不要用中文 varchar 直接存原因有两个一是中文值可能被前端传错比如“处理中”和“处理中 ”带空格二是数据库排序和筛选整数比字符串快得多。所以统一用 TINYINT 编码在小程序端用一个状态映射表翻译成中文展示。下面这个表就是你后端 Service 和前端页面都要遵守的契约后端状态机校验、前端按钮显隐、管理员页面的筛选选项都按它来避免前后端各写一套逻辑最后对不上。实际开发时我会把这个表直接贴到接口文档顶部让小程序端不用追着后端问状态码含义。状态码含义可操作角色下一状态0待受理管理员1 处理中 或 3 已驳回1处理中维修工2 已完成2已完成管理员无可关闭3已驳回学生0 重新提交新增单子状态机设计成单向向前避免了“已完成的单子又被改回处理中”这种数据打架。学生端能看到的按钮根据 status 动态显示0 和 3 显示“催办”1 显示“等待师傅完成”2 显示“已完成”。后端 Service 在 updateStatus 之前先查当前状态只允许表中列出的转移路径否则直接抛业务异常这样比前端拦截可靠得多。这里注意重新提交的语义被驳回的单子不要在原记录上改而是把原单保持 3 状态不可见另新建一条 0 状态的单子同时保留原单号用于追溯。这个设计在答辩现场演示时能很清楚地展示你对业务流程的理解。3. 用 SSM 把后端接口搭起来从 Maven 依赖到报修单状态流转数据库定稿后后端反而好写因为 SSM 每一层的职责很清楚。这个项目我会选择 Maven 工程遵循 SSM 经典三层Controller 接收参数、Service 写业务、Mapper 操作数据库。Spring 负责 Bean 管理和事务SpringMVC 负责路由和 JSON 转换MyBatis 负责 SQL 和结果映射。对毕业设计来说SSM 比 Spring Boot 的加分点在你能讲清楚“配置和容器”这两个概念这也是评委会问的地方。3.1 项目结构分层Controller-Service-Mapper 哪层该干什么常见做法是建一个多模块 Maven 工程但单模块也完全够用关键是包结构清晰。下面是我常用的目录controller 和 service 分开后你写代码不用在整个工程里翻文件。src/main/java/com/dorm/repair/ ├── controller/ # 接收参数校验基础格式 ├── service/ # 业务接口 │ └── impl/ # 接口实现加 Transactional ├── mapper/ # MyBatis Mapper 接口 │ └── xml/ # 对应 XML 文件 ├── entity/ # 数据库实体 ├── common/ │ ├── Result.java │ └── interceptor/ # 登录拦截器 └── config/ # SpringMVC 配置类如 WebMvcConfig分层理由要能说出来Controller 里不出现 SQL、不出现业务 if elseService 里不出现 HttpServletRequestMapper 里只做单表增删改查多表查询交给 XML 的 join。这样的好处是替换数据库方言时只需要动 Mapper XML不需要动上层。学生用户提交报修时Controller 从拦截器里拿 userId避免前端传一个假的 userId 来冒充别人。Maven 依赖方面SSM 至少要引入 spring-webmvc、mybatis、mybatis-spring、mysql-connector-java以及 jackson-databind。版本号用你本地 Spring 5 系列兼容的即可不要盲目追最新SSM 搭配的很多坑恰恰是版本兼容引起的。用 Servlet 3.0 以上的容器时你还需要 spring-webmvc 自带的 JsonConverter这部分在 Spring 配置里打开。3.2 三个核心接口发起报修、查询列表、更新状态接口设计遵循一个原则所有返回统一走 Result 包装前端只解析 data、code、msg 三个字段不需要每个接口单独猜字段名。这样小程序端处理异常会非常省事错误提示直接取 msg弹窗、日志、埋点都复用同一套结构。先看统一返回和发起报修的 Controller// common/Result.java public class ResultT { private int code; // 0 成功1xx 业务错误500 系统错误 private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 0; r.msg success; r.data data; return r; } public static T ResultT error(int code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } }// controller/RepairOrderController.java RestController RequestMapping(/api/repair) public class RepairOrderController { Autowired private RepairOrderService repairOrderService; PostMapping public ResultLong create(RequestBody RepairOrder order, RequestAttribute(userId) Integer userId) { if (StringUtils.isBlank(order.getTitle())) { return Result.error(1001, 报修标题不能为空); } Long orderId repairOrderService.createOrder(userId, order); return Result.success(orderId); } GetMapping(/list) public ResultPageVORepairOrder list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) Integer status, RequestAttribute(userId) Integer userId) { return Result.success(repairOrderService.pageByUser(userId, page, size, status)); } PostMapping(/{id}/status) public ResultVoid updateStatus(PathVariable(id) Long id, RequestParam(status) Integer status, RequestAttribute(userId) Integer userId) { repairOrderService.updateStatus(id, status, userId); return Result.success(null); } }这里的 RequestAttribute(userId) 值是拦截器里写入的你不需要信任前端传来的任何身份信息。create 接口直接绑定 RepairOrder 实体前端只要传 title、description、images 三个字段order_no 和 user_id 都由后端生成避免学生传一个伪造的学号或订单号。list 接口的 status 可空不传就查全部传 0 就只查待受理。接着看 Service 实现里创建报修单的逻辑事务边界是关键// service/impl/RepairOrderServiceImpl.java Override Transactional(rollbackFor Exception.class) public Long createOrder(Integer userId, RepairOrder order) { RepairOrder entity new RepairOrder(); entity.setOrderNo(BX System.currentTimeMillis() (int)(Math.random() * 90 10)); entity.setUserId(userId); entity.setTitle(order.getTitle()); entity.setDescription(order.getDescription()); entity.setImages(order.getImages()); entity.setStatus(0); repairOrderMapper.insert(entity); RepairTrack track new RepairTrack(); track.setOrderId(entity.getId()); track.setOperType(1); track.setOperatorId(userId); track.setRemark(学生提交报修); repairTrackMapper.insert(track); return entity.getId(); }事务为什么放在 Service 而不是 Mapper因为一次创建报修需要同时插入报修单和跟踪记录任何一步失败都必须回滚。Transactional 加上 rollbackFor Exception.class避免只对 RuntimeException 生效而漏掉检查异常。订单号生成这里用了时间戳加随机数生产环境建议换成数据库序列或雪花算法毕设里这种生成方式足够但答辩时要能说出它的并发局限性。更新状态的方法要体现状态机约束否则很快会被答辩老师一句话问住“如果学生反复点击完成怎么办”Override Transactional(rollbackFor Exception.class) public void updateStatus(Long orderId, Integer targetStatus, Integer userId) { RepairOrder order repairOrderMapper.selectById(orderId); if (order null) { throw new BizException(报修单不存在); } int current order.getStatus(); if (!canTransit(current, targetStatus)) { throw new BizException(当前状态不允许变更为目标状态); } RepairOrder update new RepairOrder(); update.setId(orderId); update.setStatus(targetStatus); if (targetStatus 2) { update.setFinishTime(new Date()); } repairOrderMapper.updateStatus(update); // 再写一条跟踪记录 }canTransit 方法就是把状态机表翻译成两层 if当前 0 才允许变 1 或 3当前 1 才允许变 2。这样即使小程序端被绕过直接拿 Postman 调接口也无法把一个已完成单改回处理中。这里的 BizException 可以是一个简单的 RuntimeException 子类在全局异常处理器里统一返回 Result.error。3.3 MyBatis 动态 SQL 与日期处理让状态查询不写死列表查询是最能体现 MyBatis 价值的地方。学生要筛选“只看待受理”管理员要按维修工查两个页面共用同一个接口但条件不一样。动态 SQL 用 where 加 if 来自动拼装避免写三个 Mapper 方法。!-- mapper/xml/RepairOrderMapper.xml -- select idpageByUser resultTypecom.dorm.repair.entity.RepairOrder select * from t_repair_order where if testuserId ! null and user_id #{userId} /if if teststatus ! null and status #{status} /if if testrepairerId ! null and repairer_id #{repairerId} /if /where order by create_time desc limit #{offset}, #{limit} /select这段 SQL 的巧妙之处在 where 标签会自动去掉第一个 and你不需要担心条件拼错。offset 由 Service 层计算offset (page - 1) * size。注意 size 要做上限限制比如最大 50防止学生传 100000 把数据库拖垮。日期格式化是这个项目最容易翻车的点之一。Spring 默认用 Jackson 序列化 LocalDateTime 时可能输出成 2025-06-01T10:20:30 这种带 T 的格式小程序端拿到后要手动 replace很容易漏。更糟的情况是实体用了 java.util.DateJackson 默认序列化输出毫秒时间戳前端完全没法显示。我在 application.properties 里会做两件事spring.jackson.date-formatyyyy-MM-dd HH:mm:ss spring.jackson.time-zoneGMT8 mybatis.configuration.map-underscore-to-camel-casetruedate-format 让 Date 类型输出成标准字符串time-zone 避免数据库存的是北京时间JSON 里差 8 小时。map-underscore-to-camel-case 把数据库的 create_time 直接映射成实体的 createTime省去每个字段手写 resultMap。这三个配置写好后前端拿到的时间就是干净可展示的字符串。4. 微信小程序端从登录到报修提交前后端分离下的小程序实现后端接口就绪后小程序端更像是“接口的翻译器”。微信小程序不能像网页那样直接发跨域请求它有自己的一套登录和上传体系。这里按这个系统最常用的两个流程讲登录换 openid、提交报修带图片。4.1 微信登录换 openid 的两种做法wx.login 与获取手机号微信小程序登录获取手机号是正式产品最推荐的方式但个人主体的小程序拿不到这个接口的权限毕设场景里用 wx.login 换 openid 是最省事的。流程是小程序端 wx.login 拿到临时 code后端拿 code 调微信的 code2session 接口得到 openid 和 session_key。openid 就是用户的唯一身份后端把自己的 token 返回给前端后续请求就带这个 token。// pages/login/login.js Page({ handleLogin() { wx.login({ success: async (res) { if (!res.code) { wx.showToast({ title: 登录失败, icon: none }); return; } try { const { data } await new Promise((resolve, reject) { wx.request({ url: http://localhost:8080/api/login/wx, method: POST, data: { code: res.code }, success: resolve, fail: reject }); }); if (data.code 0) { wx.setStorageSync(token, data.data.token); wx.setStorageSync(userRole, data.data.role); wx.reLaunch({ url: /pages/index/index }); } else { wx.showToast({ title: data.msg, icon: none }); } } catch (e) { wx.showToast({ title: 网络异常, icon: none }); } } }); } });这里说明一下wx.login 的 code 有效期只有五分钟且只能用一次后端拿到 code 后必须立刻调微信接口。小程序端不需要关心 openid 是什么只需要把后端返回的 token 存进 storage。我在项目里还会把 role 一起存下来这样首页可以按角色跳转不同页面学生进报修页维修工进工单列表管理员进后台。后端的 LoginController 里用 RestTemplate 调微信 code2session核心代码不复杂但要注意它返回的 openid 可能对应库里的老用户也可能是新用户。老用户直接更新 token新用户需要先弹窗补录学号和密码完成账号绑定。这里的绑定流程是先用 openid 查用户表查不到就创建一条 role1、username 为空的记录等学生在绑定页填完学工号和密码后再回填 username 和 phone。4.2 报修表单与图片上传chooseMedia 搭配 wx.request 上传报修单的核心信息是标题、描述、图片。图片这块有个典型误区直接用 wx.request 传 base64 会导致包体膨胀三四张图就可能突破后端请求体大小限制。正确做法是用 wx.uploadFile 走 multipart 上传每张图单独上传后端返回一个路径最后把多个路径用逗号拼接存进 images 字段。// pages/report/report.js async chooseAndUpload() { const res await wx.chooseMedia({ count: 3, mediaType: [image], sizeType: [compressed] }); const tempFiles res.tempFiles; const uploadTasks tempFiles.map(file { return new Promise((resolve, reject) { wx.uploadFile({ url: http://localhost:8080/api/upload, filePath: file.tempFilePath, name: file, success: (uploadRes) { try { const json JSON.parse(uploadRes.data); if (json.code 0) { resolve(json.data); } else { reject(new Error(json.msg)); } } catch (e) { reject(e); } }, fail: reject }); }); }); const paths await Promise.all(uploadTasks); this.setData({ imagePaths: paths }); }注意 wx.uploadFile 的返回值是个字符串必须 JSON.parse 一次否则你会拿到一整个表单字符串后端点怎么调都显示报错。name 参数 file 必须和后端 Controller 方法参数里的 RequestParam(file) 同名不然 SpringMVC 会把文件参数解析成空。后端上传接口用 MultipartFile 接收保存到服务器本地磁盘然后返回形如 /uploads/20250601/xxx.jpg 的相对路径而不是绝对路径 D:\uploads\xxx.jpg因为绝对路径在换服务器后会直接失效。上传完成后页面里用 imagePath 拼接预览地址提交按钮再把标题、描述、图片路径数组一起 POST 到 /api/repair。提交前做一次二次确认防止学生误点发送一个没写完的报修单。4.3 我的报修列表与状态刷新onShow 里拉取数据而不是在 onLoad这是很多小程序新手会踩的坑列表页只在 onLoad 里请求了一次数据然后从详情页返回后报修单状态还是旧的。原因是 onLoad 在整个页面生命周期里只触发一次而 onShow 每次页面显示都会触发。所以列表和“我的报修”页的数据请求要放在 onShow或者 onShow 里做一次轻量刷新。// pages/my/repairs.js Page({ data: { list: [], page: 1, size: 10, loading: false }, onShow() { if (wx.getStorageSync(token)) { this.fetchList(true); } }, async fetchList(reset false) { if (this.data.loading) return; this.setData({ loading: true }); const page reset ? 1 : this.data.page; try { const res await request({ url: /api/repair/list?page${page}size${this.data.size}, header: { Authorization: Bearer wx.getStorageSync(token) } }); const records res.data.data; this.setData({ list: reset ? records : this.data.list.concat(records), page: page 1 }); } finally { this.setData({ loading: false }); } }, onReachBottom() { this.fetchList(); } });这段代码里假设你已经把 wx.request 封装成 utils/request.js统一处理 header 和错误拦截避免每个页面重复写 wx.getStorageSync。onReachBottom 是触底加载配合后端的分页参数数据量超过几十条后不会卡顿。后端返回 PageVO 时我习惯只返回当前页记录和一个 total小程序端用 total 判断还有没有下一页。需要注意如果后端用了 Session 鉴别登录小程序端常常冷启动后 Session 还在但 cookie 丢失所以上面统一用 token 方式。token 失效时 request 封装里要拦截 code 401跳转回登录页并清掉本地 storage。这样联调阶段你在开发者工具里改完代码重新编译登录态也能自己恢复。5. 联调与避坑小程序报修系统最常见的 5 个翻车现场SSM 和小程序各自运行正常不等于联调就顺利。接口通了但页面报错、图片传了但回显失败、登录跳转后又被踢回登录页这些问题不会在单端测试里暴露只能靠联调阶段一条条排。下面这五个问题是我做这类系统时反复遇到的按“现象→原因→解决”写方便你直接对着排查。5.1 小程序请求不到本地接口域名校验与“不校验合法域名”现象开发者工具里请求正常手机真机预览时所有 wx.request 全部报 fail错误信息类似 request:fail点开详情能看到 URL 是 http 开头请求根本没到达后端。原因微信小程序要求所有请求地址必须是配置好的 https 合法域名而本地联调用的通常是 http://192.168.x.x:8080 或 localhost真机上不配置域名就会被直接拦截开发者工具里能通是因为默认勾选了跳过校验。解决开发者工具右上角「详情 → 本地设置」勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”这一项这是开发阶段的临时方案。手机真机调试时需要把网络请求地址从 localhost 改成你电脑在局域网里的 IP比如 http://192.168.1.101:8080同时关闭电脑防火墙或放开 8080 端口。等这个小程序要上线就必须在 mp 后台配置服务器域名且要求 https那不是毕设阶段要考虑的事。5.2 后端拿到了请求却回不了小程序CORS 跨域配置别漏掉现象如果同时做了一个后台管理网页网页端调用 SSM 接口时报 Access-Control-Allow-Origin 错误小程序端通常不报但如果有 H5 版本这个问题会直接卡住联调。原因浏览器同源策略限制跨域请求需要后端在响应头里明确允许来源。小程序请求本身没有 Origin 限制但 CORS 配置能避免同一套后端在 Web 管理端没法用。解决在 SSM 里加一个 CorsFilter 是最省事的做法不用改 SpringMVC 的配置只要在 Filter 里手动写响应头所有接口都能覆盖到。下面这个过滤器可以直接复制到 common/filter 包下。// common/filter/CorsFilter.java WebFilter(/*) public class CorsFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletResponse response (HttpServletResponse) resp; response.setHeader(Access-Control-Allow-Origin, req.getHeader(Origin)); response.setHeader(Access-Control-Allow-Methods, GET,POST,PUT,DELETE,OPTIONS); response.setHeader(Access-Control-Allow-Headers, Content-Type,Authorization); response.setHeader(Access-Control-Allow-Credentials, true); if (OPTIONS.equalsIgnoreCase(((HttpServletRequest) req).getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return; } chain.doFilter(req, resp); } }代码里把 Access-Control-Allow-Origin 设置成请求的 Origin而不是写死 *因为 allowCredentials 为 true 时浏览器不允许 *。这里要注意一个隐藏问题如果 session 需要 cookie跨域时还必须设置 allowCredentials 为 true同时不能用 *。我用 token 方案后这个过滤器只要保证非登录接口能通过预检即可。5.3 JSON 里的时间变成一串数字LocalDateTime 序列化坑现象小程序端拿到报修列表createTime 显示成 2025-06-01T10:20:30 这样的字符串或者显示成一串毫秒数字页面直接展示很难看如果要做“剩余时间”计算还得先转换格式。原因实体用 java.util.Date 时SpringMVC 默认的 Jackson 序列化会输出毫秒时间戳换成 LocalDateTime 后它又按 ISO 格式加一个字母 T 分隔前端并不能直接展示。根本没有统一处理全局日期格式。解决在 application.properties 配置 spring.jackson.date-formatyyyy-MM-dd HH:mm:ss 和 spring.jackson.time-zoneGMT8这样 Date 类型输出成标准字符串。实体字段如果是 LocalDateTime还需要引入 jackson-datatype-jsr310然后在同一配置文件里关闭时间戳输出不然注解和全局配置会打架。我的习惯是统一用全局配置实体里不加任何 JsonFormat这样字段换类型时不会漏改注解。5.4 图片上传后回显失败Tomcat 虚拟路径与应用上下文现象上传图片时接口返回 D:/uploads/xxx.jpg小程序端把这个路径直接塞给 标签页面图片 404但把路径复制到浏览器里又能打开。原因磁盘绝对路径不能直接被客户端访问Tomcat 的默认静态资源根目录是 webapp外部文件不在它的管辖范围内。数据库里存绝对路径换一台电脑部署时路径变了所有图片全部失效这类问题最容易让人误以为是端口被占或者防火墙拦截很玄学。解决数据库里存相对路径 /uploads/xxx.jpg然后给 SSM 加一个 WebMvcConfigurer把 /uploads/** 映射到磁盘目录。// config/WebMvcConfig.java Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/uploads/**) .addResourceLocations(file: uploadDir); } }配置文件里的 uploadDir 结尾一定要带斜杠addResourceLocations 只认 file: 前缀。这样小程序端访问 http://localhost:8080/uploads/xxx.jpg 就正常了。换环境时只需要改配置文件不用改数据库里的路径。5.5 刷新页面登录态就丢Session 与 token 的选择现象小程序登录成功后切到后台再回前台下一次请求又开始跳登录页或者开发者工具里换个编译模式之前登录的 Session 全丢了学生要反复扫码。原因SSM 默认用 HttpSession 保存用户Session 依赖 Cookie而小程序对 Cookie 的支持很弱开发者工具和真机的行为还不一样。如果同时做了网页管理端跨域下 Cookie 的 SameSite 策略还会带来二次坑所以纯后端用 Session 方案做小程序登录就是给自己挖坑。解决把登录态改成 token 字符串。登录成功后后端生成 UUID 或 JWT 返回前端前端存 storage请求头带 Authorization后端拦截器每次校验 token。UUID 方案比 JWT 简单适合毕设但答辩时要能说清它只能单机使用、不能集群部署。给一段拦截器核心实现// common/interceptor/AuthInterceptor.java public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String auth request.getHeader(Authorization); if (auth ! null auth.startsWith(Bearer )) { String token auth.substring(7); Integer userId tokenManager.getUserId(token); if (userId ! null) { request.setAttribute(userId, userId); return true; } } response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\未登录\}); return false; }tokenManager 可以是一个 ConcurrentHashMapkey 是 tokenvalue 是 userId登录时 put退出时 remove。毕业设计用这种简单实现比 JWT 更不容易踩密钥和过期时间的坑答辩时你说清楚它不支持分布式即可。用了这个方案后第 5.2 节里的 CORS 过滤器也不用手忙脚乱地处理 credentials 了前端不再依赖 Cookie。6. 让宿舍报修系统在答辩时更耐看模拟数据、接口验证与性能观察数据模型和后端接口都跑通后演示阶段最怕冷场。我会在答辩前做三件事都不需要额外装复杂框架。第一件事是生成好看的模拟数据。不要手工往数据库插几十条写一个临时 SQL 脚本用循环生成 50 条报修单日期分布在最近 30 天status 随机但保证 0、1、2 都有。这样管理后台的列表、统计图和“维修完成率”这些页面都有内容可展示老师不会看到空表。脚本里注意把学生和维修工都绑定到真实的用户 ID 上否则列表关联查询会出现空字段。第二件事是用 Apifox 建一个「宿舍报修系统」的项目把登录、图片上传、报修列表、状态更新这四组接口各存一个用例形成回归用例集。每次改完后端代码先把这组用例跑一遍能很快抓住“新增统计接口把动态 SQL 改坏”这类问题。小程序端那边可以用微信开发者工具的 Network 面板对比接口请求不用额外抓包工具。第三件事是观察一句慢 SQL 的执行计划。给 t_repair_order 加索引后用EXPLAIN SELECT * FROM t_repair_order WHERE status0 ORDER BY create_time DESC看 key 是不是走的 idx_status。如果数据量上万status 区分度低你会发现 MySQL 宁愿全表扫也不走索引这时可以改成 status create_time 的联合索引。这个调试过程写在设计文档里比罗列框架依赖高级得多。我自己的教训是第一次做这类项目时把时间都花在小程序的按钮和动画上结果后端的更新状态接口没做权限校验演示现场被人抓住漏洞特别尴尬。现在我会先花半天把状态机和接口清单写在设计文档里再让小程序的页面用 mock 数据先开发后端联调时只改接口地址。这套顺序能帮你少走很多弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表