简介这是一套面向计算机专业本科生的毕业设计完整实践资源聚焦设备故障报修场景解决高校实验室、企业办公场所等实体单位中传统报修流程低效、信息不透明、响应滞后等问题。资源包含微信小程序前端Vue原生WXML/WXSS、SSM框架Java后台127个Java类含Controller/Service/DAO分层实现、MySQL数据库含建表SQL与ER设计说明、完整毕业论文含可行性分析、系统设计、功能模块详述及操作演示视频覆盖从需求分析到部署上线的全流程。压缩包共1255个文件49.98MB以JS/Vue/WXML/WXSS前端交互与视图、Java后端逻辑、PNG/SVG界面图标与截图、JSON配置与接口数据为主辅以BAT脚本一键安装/运行、SQL数据库初始化、MP4全流程演示结构清晰、开箱即用。目前已有131人学习下载适合需快速掌握小程序Java全栈开发、理解多角色权限系统管理员/用户/维修员与真实业务闭环的学生与初学者。1. 这不是“又一个毕业设计”微信小程序SSMMySQL 报修系统为什么能跑通、能交付、还能真用你手头这份「设备故障报修小程序」毕业设计材料表面看是套模板微信小程序前端 SSMSpring SpringMVC MyBatis后端 MySQL 数据库。但真正拉开差距的不是它用了什么技术栈而是它必须同时扛住三类真实压力微信生态的运行约束比如真机扫码调试失败、登录态失效、页面白屏、SSM 层的事务一致性比如报修提交成功但工单没进数据库、状态更新丢失、MySQL 的并发写入安全比如同一台打印机被多人同时报修重复派单。我带过 12 届毕设翻车最多的不是代码写不出来而是把“能编译”当成“能交付”——小程序开发者工具里点开正常一扫真机就 404SSM 接口 Postman 调通了小程序 wx.request 却拿不到数据MySQL 建表时没设外键和索引50 人同时报修时工单列表卡死 3 秒。这篇笔记不讲“SSM 是什么”只拆解怎么让这套组合在毕业答辩前一周稳定跑起来、数据不丢、接口不崩、演示视频不黑屏。适合正在赶毕设、手里有源码但卡在联调、部署或论文描述环节的本科生也适合想快速复用该架构做轻量运维系统的校企合作工程师。2. 小程序端不是写页面是驯服微信的运行时环境微信小程序不是网页它有自己的生命周期、网络模型和权限沙箱。直接把 Vue 项目逻辑搬过来必翻车。本项目小程序端核心任务就三个用户扫码报修、实时查看工单状态、管理员审核派单。所有功能都得绕开微信的限制边界来设计。2.1 页面结构与导航顶部导航栏高度不是固定 44px而是动态计算微信小程序顶部导航栏navbar高度在不同机型、不同微信版本下差异极大。iPhone X 及以上有刘海区安卓部分全面屏机型状态栏高度也不同。硬写height: 44px会导致内容被遮挡或留白过多。正确做法是用wx.getSystemInfoSync()动态获取// utils/system.js export function getSafeAreaTop() { const systemInfo wx.getSystemInfoSync(); // 微信基础库 2.7.0 支持 safeArea但需兼容旧版 if (systemInfo.safeArea systemInfo.safeArea.top) { return systemInfo.safeArea.top; } // 降级方案iOS 默认 44安卓默认 20但实际取 statusBarHeight 44 return systemInfo.statusBarHeight ? systemInfo.statusBarHeight 44 : 44; }提示getSafeAreaTop()必须在onLoad或onShow中调用不能在data初始化时直接赋值否则可能取到旧值。我在app.js全局挂载globalData.safeTop getSafeAreaTop()所有页面通过this.app.globalData.safeTop读取避免重复计算。2.2 网络请求wx.request 不是 fetch必须处理 code0 和 code200 的双重校验SSM 后端返回 JSON 时SpringMVC 默认状态码是 200但微信小程序wx.request的res.statusCode是 HTTP 状态码而业务状态码如{code: 200, msg: success, data: {...}}藏在res.data.code里。新手常犯错误只判res.statusCode 200就认为成功结果后端抛异常返回500小程序却显示“提交成功”。// utils/request.js export function request(url, data {}, method POST) { return new Promise((resolve, reject) { wx.request({ url: https://your-api-domain.com url, method, data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || // 登录态透传 }, success: (res) { // 第一层HTTP 状态码校验 if (res.statusCode ! 200) { wx.showToast({ title: 网络错误 ${res.statusCode}, icon: none }); return reject(new Error(HTTP ${res.statusCode})); } // 第二层业务状态码校验SSM 统一返回格式 const { code, msg, data: responseData } res.data; if (code ! 200) { wx.showToast({ title: msg || 操作失败, icon: none }); return reject(new Error(msg)); } resolve(responseData); }, fail: (err) { wx.showToast({ title: 请求失败请检查网络, icon: none }); reject(err); } }); }); }参数说明url传/api/repair/submit这类相对路径域名由utils/request.js统一管理避免在每个页面硬编码header.Authorization从本地缓存读 token这是 SSM 后端Authentication拦截器校验的依据code ! 200判定逻辑必须和后端ResultUtil.success()返回结构严格对齐——这是联调第一道生死线。2.3 列表加载更多不是简单 append要防重复请求和状态错乱报修工单列表页pages/list/list.wxml需支持下拉刷新和上拉加载。常见翻车点用户快速滚动触发多次onReachBottom导致重复请求刷新时新数据覆盖旧数据但 loading 状态未重置分页参数pageNo和pageSize传错。// pages/list/list.js Page({ data: { list: [], pageNo: 1, pageSize: 10, hasMore: true, isLoading: false, isRefreshing: false }, onLoad() { this.loadList(true); // 初始化加载 }, loadList(isRefresh) { if (this.data.isLoading || (!isRefresh !this.data.hasMore)) return; this.setData({ isLoading: true, isRefreshing: isRefresh }); request(/api/repair/list, { pageNo: isRefresh ? 1 : this.data.pageNo, pageSize: this.data.pageSize }).then(res { const { list, total } res; const newData isRefresh ? list : [...this.data.list, ...list]; this.setData({ list: newData, pageNo: isRefresh ? 1 : this.data.pageNo 1, hasMore: newData.length total, isLoading: false, isRefreshing: false }); }).catch(() { this.setData({ isLoading: false, isRefreshing: false }); }); }, onPullDownRefresh() { this.loadList(true); }, onReachBottom() { this.loadList(false); } });关键细节isRefresh参数控制数据合并逻辑hasMore判断依据是当前列表长度 total而非list.length pageSize后者在最后一页可能刚好等于 pageSizesetData必须在then和catch中都调用否则 loading 状态会卡死。3. SSM 后端不是搭框架是构建事务闭环与接口契约SSM 在这里不是炫技而是解决三个硬问题用户登录态鉴权、报修单创建与分配的原子性、多角色用户/维修员/管理员数据隔离。Spring Boot 虽热但毕业设计用原生 SSM 更易溯源、答辩时老师更熟悉。3.1 登录拦截器用 Session 还是 JWT毕业设计选 Session 更稳微信小程序登录流程是前端调wx.login()→ 拿code→ 传给后端 → 后端用code换openid→ 生成 sessionKey。很多同学用 JWT结果调试时 token 过期、签名密钥泄露、刷新逻辑复杂。毕业设计阶段用 HttpSession Redis 存储最稳妥——Spring Session 自动集成 RedisController方法里直接HttpSession session注入即可。// config/WebConfig.java Configuration public class WebConfig implements WebMvcConfigurer { Bean public CookieSameSiteSupplier sameSiteSupplier() { return request - Strict; } Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .excludePathPatterns(/api/user/login, /api/user/wxlogin); } } // interceptor/LoginInterceptor.java public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(false); if (session null || session.getAttribute(openid) null) { response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\未登录\}); return false; } return true; } }注意excludePathPatterns必须排除/api/user/wxlogin微信登录接口否则拦截器会拦死首次登录response.getWriter().write(...)直接输出 JSON避免 Spring MVC 的ResponseBody干预确保小程序端能拿到标准错误结构。3.2 报修单创建MyBatis 的selectKey是事务安全的最后防线用户提交报修单时需同时插入repair_order表和关联的repair_image表图片路径。若用两条INSERT语句中间出错会导致数据不一致。SSM 标准解法是在repair_order插入时用selectKey获取自增主键再用该主键插入子表。!-- mapper/RepairOrderMapper.xml -- insert idinsertWithImages parameterTypecom.example.entity.RepairOrder selectKey keyPropertyid resultTypejava.lang.Long orderAFTER SELECT LAST_INSERT_ID() /selectKey INSERT INTO repair_order (device_id, description, user_openid, status, create_time) VALUES (#{deviceId}, #{description}, #{userOpenid}, 0, NOW()) /insert// service/RepairOrderService.java Transactional public void submitRepair(RepairOrder order, ListString imageUrls) { // 1. 插入主单获取 id repairOrderMapper.insertWithImages(order); // 2. 批量插入图片order.getId() 已被 selectKey 赋值 if (!imageUrls.isEmpty()) { ListRepairImage images imageUrls.stream() .map(url - { RepairImage img new RepairImage(); img.setOrderId(order.getId()); img.setUrl(url); return img; }) .collect(Collectors.toList()); repairImageMapper.insertBatch(images); } }血泪经验selectKey的orderAFTER表示在 INSERT 之后执行SELECT LAST_INSERT_ID()这是 MySQL 特性Oracle 需用BEFORE 序列Transactional必须加在 Service 方法上且该方法不能是 private 或 final否则 Spring AOP 失效insertBatch是 MyBatis-Plus 提供的批量插入比循环insert快 5 倍以上。3.3 角色权限控制用PreAuthorize还是自定义注解毕业设计选后者更可控SSM 项目中管理员可审核所有工单维修员只能处理自己名下的普通用户只能查自己的。Spring Security 的PreAuthorize配置复杂毕业答辩时容易说不清 SpEL 表达式。更推荐自定义RequireRole注解 AOP 切面// annotation/RequireRole.java Target({ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value() default {}; } // aspect/RoleCheckAspect.java Aspect Component public class RoleCheckAspect { Around(annotation(requireRole)) public Object checkRole(ProceedingJoinPoint joinPoint, RequireRole requireRole) throws Throwable { HttpServletRequest request ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String openid (String) request.getSession().getAttribute(openid); // 查询用户角色从数据库或缓存 String role userService.getRoleByOpenid(openid); if (Arrays.asList(requireRole.value()).contains(role)) { return joinPoint.proceed(); } throw new RuntimeException(权限不足); } }// controller/RepairOrderController.java RestController RequestMapping(/api/repair) public class RepairOrderController { PostMapping(/assign) RequireRole({ADMIN, MANAGER}) public Result assign(RequestBody AssignRequest req) { // 只有 ADMIN 和 MANAGER 能调用 repairOrderService.assignToWorker(req.getOrderId(), req.getWorkerId()); return Result.success(); } }参数说明RequireRole({ADMIN, MANAGER})显式声明角色比PreAuthorize(hasRole(ADMIN) or hasRole(MANAGER))更直观切面中userService.getRoleByOpenid()是从 MySQL 查user_role关联表避免把角色硬编码在注解里throw new RuntimeException会被全局异常处理器捕获统一返回{code:403,msg:权限不足}。4. MySQL 数据库不是建表就行是设计防并发、防误删、防查询慢的三层防护毕业设计的 MySQL 往往被当成“存数据的地方”但报修系统的真实瓶颈就在数据库同一设备被多人同时报修、管理员批量修改状态、历史工单按时间范围查询。没做防护演示时点两下就卡死。4.1 表结构设计外键不是可选项是数据一致性的物理保障很多同学为省事repair_order表里直接存device_id字符串不建外键。结果设备表被误删报修单里全是脏数据。必须用外键约束 级联更新-- 设备表 CREATE TABLE device ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 设备名称, location varchar(200) DEFAULT NULL COMMENT 位置, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 报修单表带外键 CREATE TABLE repair_order ( id bigint(20) NOT NULL AUTO_INCREMENT, device_id bigint(20) NOT NULL COMMENT 设备ID, description text COMMENT 故障描述, user_openid varchar(64) NOT NULL COMMENT 用户openid, status tinyint(4) DEFAULT 0 COMMENT 状态0待处理1处理中2已完成, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_device_id (device_id), KEY idx_user_openid (user_openid), CONSTRAINT fk_repair_device FOREIGN KEY (device_id) REFERENCES device (id) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;关键点ON DELETE RESTRICT防止设备被删导致工单孤儿ON UPDATE CASCADE当设备 ID 更新时自动同步虽极少发生但契约完整KEY idx_device_id和idx_user_openid是后续查询的命脉没索引的WHERE device_id ?会全表扫描。4.2 并发写入防护UPDATE 语句必须带 WHERE 条件且用行锁而非表锁维修员点击“开始处理”时后端执行UPDATE repair_order SET status 1 WHERE id ? AND status 0。漏掉AND status 0是致命错误——可能导致已处理的单被重复设置为“处理中”。// mapper/RepairOrderMapper.java Update(UPDATE repair_order SET status 1, worker_openid #{workerOpenid}, update_time NOW() WHERE id #{id} AND status 0) int updateStatusToProcessing(Param(id) Long id, Param(workerOpenid) String workerOpenid);// service/RepairOrderService.java public boolean startProcessing(Long orderId, String workerOpenid) { int updated repairOrderMapper.updateStatusToProcessing(orderId, workerOpenid); if (updated 0) { // 返回 0 表示原状态不是 0已被他人处理 throw new BusinessException(该工单已被处理请刷新页面); } return true; }玄学提醒MySQL 的UPDATE ... WHERE默认加行锁Record Lock但必须确保id是主键或唯一索引否则会升级为间隙锁Gap Lock甚至临键锁Next-Key Lock引发死锁。毕业设计用id主键绝对安全。4.3 查询性能优化分页不是 limit M,N是用游标分页防深分页当工单数超 1 万条SELECT * FROM repair_order ORDER BY create_time DESC LIMIT 10000, 10会极慢——MySQL 要先跳过 10000 行。毕业设计必须用游标分页Cursor-based Pagination-- 第一页最新10条 SELECT * FROM repair_order WHERE status IN (0,1,2) ORDER BY create_time DESC, id DESC LIMIT 10; -- 下一页假设上一页最后一条 create_time2024-05-20 10:00:00, id1005 SELECT * FROM repair_order WHERE status IN (0,1,2) AND (create_time 2024-05-20 10:00:00 OR (create_time 2024-05-20 10:00:00 AND id 1005)) ORDER BY create_time DESC, id DESC LIMIT 10;// mapper/RepairOrderMapper.java Select(script SELECT * FROM repair_order WHERE status IN foreach itemstatus collectionstatuses open( separator, close) #{status} /foreach if testcursorTime ! null and cursorId ! null AND (create_time lt; #{cursorTime} OR (create_time #{cursorTime} AND id lt; #{cursorId})) /if ORDER BY create_time DESC, id DESC LIMIT #{pageSize} /script) ListRepairOrder selectByCursor(Param(statuses) ListInteger statuses, Param(cursorTime) String cursorTime, Param(cursorId) Long cursorId, Param(pageSize) Integer pageSize);为什么有效游标分页利用索引create_time, id联合索引每次查询都从上一页末尾位置继续时间复杂度 O(log n)而非深分页的 O(n)foreach动态拼接status IN避免硬编码lt;是 XML 实体防止解析错误。5. 联调与避坑90% 的毕设翻车都发生在这一环再完美的代码联调不通就是废纸。本章直击微信小程序 SSM MySQL 三端联调的 5 个高频雷区每一条都是我帮学生 debug 时记下的血泪记录。5.1 小程序真机扫码 404不是域名没配是 Nginx 转发丢了 path现象开发者工具里wx.request正常真机扫码打开小程序后所有接口返回 404。原因Nginx 配置了proxy_pass http://localhost:8080;但没加结尾/导致https://your.com/api/repair/submit被转发成http://localhost:8080api/repair/submit少了个/。解决Nginx 配置必须写成proxy_pass http://localhost:8080/;注意末尾/或用rewrite显式重写location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }5.2 SSM 接口返回中文乱码不是 IDEA 编码是 Tomcat 的 URI 编码没设现象Postman 调用http://localhost:8080/api/repair/list?deviceName打印机正常但小程序wx.request传deviceName打印机后端 Controller 参数接收为??。原因Tomcat 8.5 默认 URI 编码是 UTF-8但微信小程序发送的 URL 编码是application/x-www-form-urlencoded需显式指定URIEncodingUTF-8。解决修改conf/server.xml在Connector标签里加URIEncodingUTF-8Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /5.3 MySQL 插入中文变问号不是数据库字符集是 JDBC URL 缺少参数现象SSM 插入description打印机卡纸MySQL 里存成??????。原因MySQL 服务端字符集是utf8mb4但 JDBC 连接 URL 没指定useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai。解决application.properties中 JDBC URL 必须完整spring.datasource.urljdbc:mysql://localhost:3306/repair_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse注意allowPublicKeyRetrievaltrue是 MySQL 8.0 必需参数否则连接失败useSSLfalse在本地开发可关闭生产环境应配 SSL。5.4 小程序wx.login()换不到openid不是 AppID 错是js_code用了两次现象小程序调wx.login()拿到code传给后端后端用https://api.weixin.qq.com/sns/jscode2session换openid返回{errcode:40029,errmsg:invalid code}。原因微信code一次性有效5 分钟过期。调试时在开发者工具里点多次“重新登录”code被重复使用。解决每次wx.login()成功后立即传给后端不要缓存code后端换openid后必须用openid作为 Session Key而不是code。5.5 毕业论文里“系统测试”章节写不实不是编数据是用 JMeter 做真实压测现象论文写“系统支持 100 并发用户”但没任何数据支撑答辩被问“怎么测的”当场哑火。解决用 JMeter 做最小化压测——不用学全功能只跑通POST /api/repair/submit接口下载 JMeter新建线程组Number of Threads 100Ramp-up 10Loop Count 1添加 HTTP 请求路径/api/repair/submitBody Data 写 JSON{deviceId:1,description:test}添加 HTTP Header Manager加Content-Type: application/json添加 View Results Tree 和 Summary Report运行截图 Summary Report 中的Average Response Time 500ms、90% Line 800ms、Error % 0。技巧JMeter 测试前先用ab -n 100 -c 10 http://localhost:8080/api/repair/list做快速验证abApache Bench比 JMeter 轻量适合毕业设计速测。6. 演示视频与答辩技巧让老师一眼看懂你的工作量毕业答辩不是代码朗诵会是价值呈现。老师平均听每人 8 分钟你要在 3 分钟内让他记住你解决了什么真问题、怎么解决的、为什么这个解法合理。演示视频和论文描述必须服务于这个目标。6.1 演示视频脚本按“场景-操作-结果”三段式拒绝功能罗列别拍“点击首页→点击报修→填写表单→提交→跳转成功页”。这叫操作录像不是演示。改成场景5秒实验室投影仪突然黑屏学生掏出手机。操作15秒打开小程序 → 扫描投影仪旁的二维码特写二维码→ 自动带入设备 ID → 拍照上传故障图 → 提交。结果10秒后台管理页弹出新工单红点提醒→ 管理员点击“分配” → 维修员手机收到微信服务通知展示通知栏→ 维修员确认接单 → 工单状态变为“处理中”。关键点全程用真机录屏禁用模拟器二维码用真实设备生成https://your.com/device/123后台管理页用 Chrome 打开 SSM 的/admin页面不要截图微信服务通知必须是真机收到证明消息推送链路通。6.2 论文“系统实现”章节用对比表格代替文字堆砌老师不会逐行读代码但会看表格。把核心模块实现方式做成对比表一目了然模块技术选型为什么选它替代方案及弃用原因用户登录微信wx.login() SSM Session免密码、免注册符合校园场景Session 比 JWT 更易调试JWT需维护密钥、刷新逻辑复杂毕设周期不够图片上传小程序wx.uploadFile() 七牛云 SDK七牛云免费额度够用CDN 加速快避免自建文件服务器本地存储需配置 Nginx 静态资源跨域麻烦工单分配SSMTransactional MySQL 行锁保证“分配”操作原子性行锁性能优于 Redis 分布式锁Redis 锁引入新组件增加部署复杂度写作技巧每行只写一个决策点用“为什么选它”体现思考深度“替代方案”展示技术权衡能力——这正是答辩时老师最想听的。6.3 答辩话术把“我做了”变成“我解决了”别讲“我用 Vue 写了小程序页面”。要说“我发现微信小程序真机环境下 navbar 高度不一致导致内容被遮挡。我通过wx.getSystemInfoSync()动态计算 safeAreaTop并在全局注入解决了 98% 机型的适配问题。”别讲“我用了 MyBatis”。要说“为防止报修单创建时主子表数据不一致我用selectKey获取自增 ID并在 Service 层用Transactional保证原子性经 JMeter 100 并发测试数据一致率达 100%。”我的习惯答辩前把每个功能点写成“问题-解法-验证”三句话贴在笔记本第一页。老师问到哪就翻到哪不背稿、不卡壳。希望帮到你。本文还有配套的精品资源点击获取