简介一套面向毕业设计场景的设备故障报修小程序完整交付方案基于微信小程序SSMMySql架构开发适合计算机相关专业学生参考或二次开发。资源覆盖管理员、用户、维修员三个角色后台管理采用Java的SSM框架小程序端通过微信开发者工具构建实现实验室管理、报修提交、维修报告、经验分享与留言板等核心功能并附带可行性分析、系统设计和数据库设计说明实用性与完整性兼备。压缩包共1255个文件约49.98MB包含png截图、java后台源码、vue前端页面、js逻辑脚本、wxml/wxss小程序页面、sql数据库脚本以及mp4视频演示和毕业论文文档文件类型覆盖源码、文档与演示素材结构清晰便于定位学习。已有130人浏览学习适合需要快速搭建同类报修系统或完成毕业设计的学生获取完整落地方案。1. 设备故障报修小程序一个把SSM、MySQL和微信小程序串起来的毕业设计题目每年毕业季大量计算机专业学生被分到“设备故障报修小程序”这个题目。它不像电商系统那样堆功能也不像纯管理后台那样只做CRUD而是要你同时处理微信小程序端的表单交互、图片上传、工单状态流转以及后端SSM框架下的权限控制和MySQL里的数据建模。这套系统的核心价值很直接把原本靠电话、纸质单据流转的设备报修流程数字化用户提交、维修人员接单、管理员监督三个角色在同一闭环里协作。如果你正卡在这个选题上这篇笔记会把从建库到联调的完整路径、参数怎么定、哪里最容易翻车一次讲透。2. SSMMySQL微信小程序这套组合凭什么能扛起报修系统做毕业设计最怕的不是功能少而是技术栈之间互相打架。微信小程序负责界面SSM负责后端业务MySQL负责数据存储这三者之间的接口边界如果一开始不划清楚后面联调会非常痛苦。这一章先把选型和架构分工讲明白让你在动手写代码之前就知道每个配置文件、每个请求路径该归谁管。2.1 三层架构怎么切Spring管对象SpringMVC管路由MyBatis管SQL很多同学背过“SSM是SpringSpringMVCMyBatis”但一写代码就懵这个配置该放哪个文件那个Bean该交给谁管在报修系统里后端需要按照一条清晰的调用链运转微信小程序发来HTTP请求SpringMVC的DispatcherServlet把请求分发给ControllerController调用Service层做业务校验Service通过MyBatis的Mapper接口执行SQL最后把结果封装成JSON返回给小程序。这条链的任何一环断开报修单就流转不起来。Spring在其中的角色是容器管理Service、Mapper这些对象的创建与注入这就是常说的控制反转。你在Service里写一个Autowired就能拿到另一个Bean不必自己new。SpringMVC管的是Web层它决定哪个URL对应哪个Controller方法也就是路由映射。MyBatis把Java方法与SQL语句绑定你可以写XML形式的SQL也可以写注解形式。做毕业设计我建议用XML形式因为后面调整筛选条件时在XML里改SQL比在注解里改字符串更直观也不会污染Java代码。落到报修系统上用户提交一张报修单请求路径是POST /api/repair/order。SpringMVC先解析请求头和请求体交给RepairOrderControllerController里注入RepairOrderServiceService里再注入RepairOrderMapper和DeviceMapper一个写入工单一个更新设备状态。任何一步抛异常事务管理器会把两步一起回滚。理解了这条链后面配applicationContext.xml和spring-mvc.xml时你就知道每个配置项到底在给谁服务。这里还有一个SSM项目最常见的配置误区。applicationContext.xml是Spring的根容器管数据源、事务、Service层和Mapper层spring-mvc.xml是SpringMVC的子容器只扫描Controller管注解驱动和JSON消息转换。子容器能看到父容器的Bean父容器看不到子容器的Bean。如果你把Service也扫描进spring-mvc.xml接口能跑通但事务可能失效因为事务拦截器挂在根容器的代理对象上子容器里重新生成的Service实例绕过了代理。这是后面避坑章节要展开的经典问题。把所有配置过一遍后这套架构在脑子里应该是这样的小程序端是薄客户端只负责收集报修信息、拍照上传、渲染工单列表后端SSM是业务中枢所有状态合法性校验、权限判断、数据落库都在这里完成MySQL是最终存储。三层之间通过JSON交换数据接口路径统一以/api开头遵循restful风格。这个约定从第一天就要定好后面前端mock数据、后端自测、最后联调都按同一套接口文档走。2.2 报修场景的数据特征MySQL为什么够用且好查设备故障报修的数据模型有几个特点。第一数据量不大。一个学校或一个中型企业设备几千台算多的工单一天上百条已经是高负载。第二关系清晰。用户、设备、工单、维修记录四类实体之间有明确的关联。第三事务边界清楚。一次报修操作涉及插入工单和更新设备状态这两步要么都成功、要么都失败。这三条正好都在MySQL的能力范围内InnoDB引擎默认支持事务配合MyBatis可以直观地控制提交和回滚。有同学会纠结要不要加Redis做缓存、要不要上MongoDB这套毕业设计完全没必要。报修工单的查询基本都是“按用户查自己的工单”“按状态查所有待受理工单”这类查询用MySQL的索引就能覆盖给order_no和设备ID建上索引后体验没有任何问题。MySQL 8.0默认的utf8mb4字符集对中文和emoji都友好报修描述里偶尔出现的特殊符号也不会存丢。这里要重点说工单状态字段的设计。status我习惯用TINYINT存整数0待受理1维修中2已完成3已取消。为什么不用字符串因为整数在前后端传递时不涉及编码问题小程序端做状态筛选时直接传0/1/2后端MyBatis里也能用数字直接比较。反过来如果写成“waiting”“processing”这种字符串数据库里看着直观但代码里到处是字符串常量改一个状态名要连带改接口文档和前端判断逻辑。整数状态唯一的缺点是可读性差解决办法是在Java实体类里定义一个常量类把0/1/2/3声明成静态常量代码里读起来一样直观。字段层面的另一个建议是冗余设计。repair_order表里直接存device_name和device_location不要只存device_id。设备位置可能在维修过程中被修改但工单需要保留报修那一刻的快照。这种“冗余字段保存快照”的做法在真实企业系统里很常见答辩时讲出来是加分点。MySQL在表关联上用冗余字段换取查询速度对报修这种写入远少于查询的场景很划算。2.3 小程序端用原生还是uniapp这个题目别给自己加戏做毕业设计选微信小程序技术栈时很多同学会纠结原生WXML写起来麻烦uniapp学一套语法能同时出H5、App和微信小程序。但回到题目本身我建议直接用原生开发。题目明确写的是微信小程序答辩时演示的就是微信开发者工具里的效果原生的调试链路最短报修系统的页面类型有限登录页、报修表单页、工单列表页、详情页加上管理端的看板原生WXML完全够用原生API文档最齐全踩坑时搜到的解决方案也最多这对时间紧张的毕设阶段是实打实的优势。如果你执意用uniapp注意几个与原生小程序的差异。生命周期钩子虽然同名但onLoad的触发时机和参数类型有细微差别scroll-view的滚动事件在uniapp里需要额外兼容自定义导航栏的适配方式也要自己处理。微信小程序顶部导航栏高度在不同型号手机上不一致如果页面里用了自定义导航栏原生开发可以用wx.getMenuButtonBoundingClientRect()拿到胶囊按钮的位置这个API在uniapp里也能用但包装后的返回结构有差异调试起来多一道周折。小程序端技术栈定了之后联调重点就是接口对齐。前端不用像PC管理系统那样考虑浏览器兼容因为小程序运行环境相对统一只需要注意基础库版本。我通常会在app.json里指定一个适中的基础库最低版本比如2.20.0以上既不影响wx.chooseMedia这类新API也兼容大多数用户的微信版本。这项配置写在app.json的“libraries”或project.config.json里答辩时老师问到也能讲出依据。3. 建库建表到登录接口跑通SSM工程落地的第一步这一章开始动手。先把数据库和四张核心表建好再把SSM工程骨架拉起来最后用一个登录接口验证整条链路通不通。登录接口虽然简单但它能一次性检验数据库连接、MyBatis映射、SpringMVC路由、JSON序列化这几件最容易出错的事建议你把它当成SSM工程的“hello world”来对待。3.1 四张核心表用户、设备、工单、维修记录的建表SQL与字段权衡我一般把建表SQL写进一个schema.sql文件方便反复初始化数据库。下面就是这套报修系统的核心建表语句主键用自增ID命名统一用下划线风格字符集统一utf8mb4。CREATE DATABASE IF NOT EXISTS repair_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE repair_system; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), role TINYINT NOT NULL DEFAULT 2 COMMENT 0-管理员1-维修人员2-普通用户, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE device ( id INT PRIMARY KEY AUTO_INCREMENT, device_code VARCHAR(50) NOT NULL UNIQUE, device_name VARCHAR(100) NOT NULL, location VARCHAR(200), status TINYINT NOT NULL DEFAULT 1 COMMENT 1-正常0-故障, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE repair_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, device_id INT NOT NULL, device_name VARCHAR(100) NOT NULL, device_location VARCHAR(200) NOT NULL, user_id INT NOT NULL, repairer_id INT DEFAULT NULL, description VARCHAR(500) NOT NULL, images VARCHAR(1000), status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待受理1-维修中2-已完成3-已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_device (device_id), KEY idx_user (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE repair_log ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, content VARCHAR(500) NOT NULL, operator_id INT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表逻辑说明sys_user表用role字段区分三类角色比单独建三张身份表简单后面用角色值控制菜单和接口权限也方便。repair_order里冗余了device_name和device_location这是刻意为之目的在前面讲过保留报修时刻的快照。images字段用varchar存逗号分隔的图片路径毕业设计阶段完全够用不需要为它单独建附件表。字段层面的两个参数选择值得说明。status统一用TINYINT写法上是“mysql设置默认值为0”的典型用法工单表里DEFAULT 0表示新单默认待受理设备表里DEFAULT 1表示新设备默认正常。时间字段全部用DATETIME而不是TIMESTAMP因为TIMESTAMP有2038年问题DATETIME在MySQL 8.0里默认值写起来也更顺手。如果你是MySQL 5.7注意DATETIME的DEFAULT CURRENT_TIMESTAMP在部分版本里支持不完整建表报错时改成显式插入即可。3.2 SSM工程目录、pom依赖与spring-mybatis.xml、spring-mvc.xml的分工传统SSM项目的目录结构我习惯这样组织controller放请求入口service放业务逻辑dao放MyBatis接口entity放实体类resources下放配置文件。包名用com.example.repair对应目录树如下。src/main/java/com/example/repair ├── controller │ ├── UserController.java │ ├── DeviceController.java │ └── RepairOrderController.java ├── service │ ├── RepairOrderService.java │ └── impl/RepairOrderServiceImpl.java ├── dao │ ├── UserMapper.java │ ├── DeviceMapper.java │ └── RepairOrderMapper.java └── entity ├── SysUser.java ├── Device.java └── RepairOrder.java src/main/resources ├── jdbc.properties ├── mybatis-config.xml ├── spring-mybatis.xml └── spring-mvc.xmlpom.xml里的核心依赖就五类spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-java再加上JSON转换用的jackson-databind。这里有个新版本容易踩的坑MySQL官方驱动在8.0之后的Maven坐标从mysql:mysql-connector-java迁移到了com.mysql:mysql-connector-j如果你用的仓库没同步新坐标就锁定旧坐标加版本号比如5.1.49驱动类名还是com.mysql.jdbc.Driver。配置文件的职责可以这样记jdbc.properties只放数据库连接四件套driver、url、username、passwordspring-mybatis.xml负责把数据源交给SqlSessionFactoryBean并扫描dao包生成Mapper代理spring-mvc.xml只扫描controller包配置注解驱动和JSON消息转换器。这里反复强调“只扫描controller包”就是为了避免出现上一章说的父子容器Bean重复问题。3.3 微信小程序登录换openid第一个接口的完整链路登录接口是SSM工程跑通的第一关。微信小程序登录的标准做法是小程序端调用wx.login拿到临时code把code发给后端后端用code去微信接口换openid查sys_user表没有就自动注册然后生成一个自定义token返回给小程序端。下面这个UserController只写了入口方法重点是展示Controller怎么接请求、Service怎么被注入。RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginRequest req) { String openid userService.getOpenidByCode(req.getCode()); SysUser user userService.findOrCreateUser(openid, req.getNickname()); String token userService.createToken(user.getId()); return Result.success(token); } }这段代码的逻辑说明RestController是Controller和ResponseBody的组合表示这个类里所有方法的返回值都直接序列化成JSON写入响应体不需要再写ResponseBody。RequestBody把小程序POST上来的JSON字符串反序列化成LoginRequest对象LoginRequest里只需要code和nickname两个字段。getOpenidByCode这一步是后端用HttpClient调微信的jscode2session接口返回值里能拿到openid和session_key但session_key不要存数据库只留openid做用户标识即可。Service层的实现要特别注意事务注解。findOrCreateUser方法内部有“查不到就插入”的逻辑我建议把“根据openid查询用户”和“插入新用户”放在同一个方法里并加上Transactional(rollbackFor Exception.class)。为什么要显式指定rollbackFor因为Spring的默认事务只回滚RuntimeException如果方法里catch住了异常并返回了错误标记事务不会自动回滚。这个细节在答辩时被追问的概率很高。最后是UserMapper.xml里的两条SQL查询和插入的写法要注意查询返回null时不要误判成数据库异常插入时用useGeneratedKeystrue把自增主键回填到user对象里方便后续生成token。select idselectByOpenid resultTypecom.example.repair.entity.SysUser SELECT * FROM sys_user WHERE username #{openid} /select insert idinsertUser useGeneratedKeystrue keyPropertyid INSERT INTO sys_user (username, nickname, role, create_time) VALUES (#{username}, #{nickname}, 2, NOW()) /insert参数说明username字段这里直接存openid因为微信用户没有传统意义上的用户名用openid当唯一登录名最省事nickname从小程序端传入允许为空。role固定写2表示新注册用户都是普通用户。等管理员后台开通维修权限时再UPDATE role字段这个权限模型足够支撑毕业设计的角色演示。4. 小程序端报修流程实现从表单提交到工单状态流转后端登录链路跑通后开始写小程序端。这一章按报修主流程来推进先封装统一的request请求再实现报修表单和图片上传最后处理工单列表的状态筛选与三个角色的权限差异。页面文件不追求花哨重点是让数据流转符合后端接口的预期。4.1 页面结构与request封装把token和请求路径统一管起来小程序的页面我一般按业务划分pages/login放登录页pages/repair下面放form报修表单、list工单列表、detail工单详情pages/user放个人中心pages/admin放管理端看板。每个页面一个文件夹包含wxml、wxss、js、json四个文件这个结构本身没什么可说的真正值得打磨的是请求封装。const BASE_URL http://127.0.0.1:8080/repair_system/api; function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method || GET, data: data || {}, header: { Content-Type: application/json, token: wx.getStorageSync(token) }, success(res) { if (res.statusCode 200) { resolve(res.data); } else { reject(res); } }, fail(err) { reject(err); } }); }); } module.exports { request };逻辑说明把BASE_URL、token读取、状态码判断收拢在一个文件里后续所有页面请求都不用关心这些重复逻辑。token从本地缓存里读取每次请求自动带上后端拦截器从Header里取token校验登录态这样比每次手动传token安全也省事。BASE_URL里的端口必须和Tomcat一致我习惯把SSM项目打成war包部署在Tomcat的webapps目录下所以路径里保留repair_system项目名如果你用SpringBoot的内置Tomcat把BASE_URL改成根路径即可。参数说明method不传时默认GETdata不传时给空对象避免wx.request在GET请求下传参格式不一致。成功回调里只判断statusCode是否为200业务上的成功或失败统一通过后端返回的code字段区分这样前端可以集中处理“token过期”这类全局错误。微信小程序设置缓存时间这一点也要提一下。token的存储用wx.setStorageSync但缓存时间需要自己控制。我通常在login成功后额外存一个expireTime字段每次请求前检查当前时间是否超过expireTime超过就跳转登录页。这个逻辑写在request函数开头能避免token失效后接口报错堆叠。4.2 报修表单与图片上传wx.chooseMedia到wx.uploadFile的组合报修表单页是核心交互页面需要用户选择设备、填写故障描述、拍照上传。设备选择我用picker组件从设备列表里选故障描述用textarea图片用wx.chooseMedia选择然后调用wx.uploadFile逐个上传成功后把返回的图片路径拼到数组里。Page({ data: { deviceList: [], deviceId: null, description: , images: [] }, async onLoad() { const { request } require(../../utils/request); const res await request(/device/list, GET); this.setData({ deviceList: res.data }); }, async handleChooseImage() { const res await wx.chooseMedia({ count: 3, mediaType: [image], sizeType: [compressed] }); this.setData({ images: res.tempFiles.map(item item.tempFilePath) }); }, async handleSubmit() { const { request } require(../../utils/request); const uploaded []; for (let i 0; i this.data.images.length; i) { const filePath this.data.images[i]; const res await this.uploadOne(filePath); uploaded.push(res); } const payload { deviceId: this.data.deviceId, description: this.data.description, images: uploaded.join(,) }; await request(/repair/order, POST, payload); wx.showToast({ title: 提交成功, icon: success }); }, uploadOne(filePath) { return new Promise((resolve, reject) { wx.uploadFile({ url: BASE_URL /file/upload, filePath: filePath, name: file, success(res) { resolve(JSON.parse(res.data).data); }, fail: reject }); }); } });逻辑说明handleChooseImage里count限制3张sizeType选compressed压缩图这是为了让图片上传更省流量也避免后端接收超大文件。handleSubmit里先循环上传图片得到路径数组再一起提交表单。为什么要先传图后提交因为后端需要先保存图片文件、拿到URL再把这些URL作为工单字段写入数据库一次性提交可以避免“工单建好了图片却传失败”的脏数据。参数说明wx.uploadFile的name字段“file”要和后端MultipartFile的参数名对上否则后台拿不到文件对象。success回调里res.data是后端返回的JSON字符串必须JSON.parse之后再取data字段。BASE_URL这里用的是相对接口路径和后端文件上传接口要保持一致。图片上传这块有个很实际的边界问题微信小程序单次上传默认限制10MB左右压缩后一般能过。如果图片仍然过大可以在chooseMedia之后用canvas压缩或者在后端限制文件大小并返回明确错误信息避免上传失败时前端只看到一个undefined。4.3 工单列表与角色权限用户、维修人员、管理员怎么共用一套接口工单列表是小程序端最复杂的页面因为同一个列表页要服务三种角色。普通用户只看自己提交的工单维修人员看到的是被分配给自己或者待领取的工单管理员看到全部工单。我的做法是后端提供同一个接口用role和userId组合过滤条件前端不用区分页面类型。RestController RequestMapping(/api/repair) public class RepairOrderController { Autowired private RepairOrderService repairOrderService; GetMapping(/order/list) public Result list(RequestParam(required false) Integer status, RequestHeader(value token, required false) String token) { Integer userId TokenUtil.getUserId(token); Integer role TokenUtil.getRole(token); ListRepairOrderVO list repairOrderService.selectByRole(role, userId, status); return Result.success(list); } PostMapping(/order/status) public Result updateStatus(RequestBody StatusUpdateReq req, RequestHeader(value token, required false) String token) { repairOrderService.updateStatus(req.getOrderId(), req.getStatus(), token); return Result.success(null); } }逻辑说明list接口从token里解析出userId和role再透传给Service层。Service层根据role决定过滤条件角色为2时只查user_id等于当前用户角色为1时查repairer_id等于当前用户或status为0的待受理单角色为0时不做userId过滤只按status筛选。这种做法把权限判断收敛在后端前端不用关心“谁能看到什么”安全性也更好用户不能通过改请求参数看到别人的工单。参数说明status是可选参数不传时返回该角色可见的全部工单传0/1/2时按状态过滤。updateStatus接口要校验状态流转的合法性比如普通用户只能把待受理工单改成已取消维修人员只能把待受理改成维修中、维修中改成已完成不能跨状态跳转。这个校验放在Service层用switch-case写状态机判断答辩时是加分点。前端列表页只需要按后端返回的字段渲染。提到微信小程序单选框的应用状态筛选的tab栏可以用radio样式模拟也可以用scroll-view横向滚动加选中态class实现。还有微信小程序项目实例这个热词其实不需要去抄别人的开源项目把自己手写的这个列表页加上下拉刷新和触底加载就足够完整体验了。5. 联调避坑指南SSM、MySQL和微信小程序组合里的5个高频问题进入联调阶段后遇到的问题往往不是“不会写代码”而是“不知道去哪里查”。这一章把SSMMySQL微信小程序三端联调时最高频的5个问题按现象、原因、解决的顺序写清楚。每一条都是我自己做类似项目时实打实翻过车的场景不是网上抄来的概念。5.1 真机预览连不上本机后端localhost与局域网IP的区别现象在微信开发者工具里接口全部正常拿手机真机预览时所有请求超时network面板里显示请求发不出去。原因微信开发者工具有“不校验合法域名”的开关可以允许开发者工具访问本机localhost但真机上的小程序运行在你的手机里手机里的localhost指的是手机自己不是电脑。你的后端跑在电脑的8080端口手机完全访问不到电脑的127.0.0.1。解决把BASE_URL里的127.0.0.1改成电脑在局域网里的IP比如192.168.1.10手机和电脑连同一个WiFi再预览。在命令行用ipconfigWindows或ifconfigMac确认IP改完后端Tomcat没有跨域限制小程序端可以直连。注意iPhone更容易触发这个问题同时在项目配置里把“不校验合法域名”打开——这个开关只对开发者工具生效真机调试时也需要在手机端开启调试模式。5.2 MySQL 8.0连接闪断时区、SSL与驱动坐标的连锁问题现象项目启动时报The server time zone value ??? 标准时间 is unrecognized或者访问数据库接口时偶尔报Communications link failure。原因MySQL 8.0的安装配置教程里经常强调新版驱动默认要求serverTimezone明确指定时区。你本机MySQL的时区是系统默认的CST但JDBC驱动不认这个缩写连接直接失败。SSL则是因为MySQL 8.0默认开启SSL认证而本地连localhost时SSL握手失败会让驱动尝试多次连接看起来像网络闪断。解决jdbc.properties里把连接串补全加上serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue。allowPublicKeyRetrieval这个参数是因为MySQL 8.0在缓存SHA2密码时非SSL连接需要显式允许公钥检索不加它有时会直接报Public Key Retrieval is not allowed。三个参数一起写不再单独查原因。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/repair_system?serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456参数说明driver用com.mysql.cj.jdbc.Driver这是MySQL 8.0的新驱动类名老驱动类名com.mysql.jdbc.Driver虽然还兼容但在日志里会打Deprecation警告。如果项目还在用5.x驱动这两个参数不生效优先把驱动版本升上去。5.3 接口返回中文变问号SpringMVC的JSON编码与消息转换器现象小程序端请求登录或工单详情返回的中文全部变成????或者\u5bf9\u65b9这样的Unicode转义序列。英文和数字正常。原因分两种情况。乱码问号是MySQL连接串没指定characterEncoding导致写入库表时用了latin1而Unicode转义是SpringMVC的Jackson消息转换器在序列化时默认输出Unicode转义字符数据库里存的是中文JSON里却变成转义形式小程序端解析后显示乱码。解决两个地方同时改。jdbc.url里加characterEncodingutf8确保JDBC读写字符集和数据库一致。然后在spring-mvc.xml里配置消息转换器强制用UTF-8输出。mvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.StringHttpMessageConverter property namedefaultCharset valueUTF-8/ /bean bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter/ /mvc:message-converters /mvc:annotation-driven逻辑说明StringHttpMessageConverter负责处理纯字符串响应MappingJackson2HttpMessageConverter负责Java对象转JSON。如果你后端每个Controller方法都返回Result对象主要是Jackson在起作用但如果某个接口直接返回StringString的编码转换器配置错了就会乱码。两处都配上根治问题。还有一种情况是XML里的SQL查出的中文本身就乱码那是数据写入阶段就错了需要先把当前表DROP掉重建再按上面的配置重新导入数据。5.4 wx.uploadFile后台拿不到文件multipart解析器没配置现象小程序端用wx.uploadFile上传报修图片后端接口报415或500断点发现MultipartFile参数是null普通表单字段却能收到。原因SpringMVC默认没有注册CommonsMultipartResolver文件上传请求到了DispatcherServlet后被当成普通POST处理Servlet API里拿不到Part内容。这是SSM项目里最常见的漏配置之一因为很多人背了文件上传的代码却没背这个Bea配置。解决在spring-mvc.xml里加multipartResolver并设置必要的参数。bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namedefaultEncoding valueUTF-8/ property namemaxUploadSize value10485760/ /bean参数说明maxUploadSize上限10MB是给整次请求的总大小兜底避免恶意上传撑爆内存。defaultEncoding设置UTF-8和前面JSON编码一致。注意bean的id必须是multipartResolverSpringMVC在初始化时按这个固定名称查找解析器id写错的话配置不生效且没有报错提示这是典型的“沉默失效”。依赖方面需要额外引入commons-fileupload包别漏了。5.5 Service自调用导致事务失效写代码时最容易漏的一层代理现象一个Service方法调用同一个类里的另一个方法被调用的方法里明明加了Transactional但插入数据库失败时不回滚数据留下半截。原因Spring的事务是通过AOP给Service生成代理对象实现的只有从外部进入代理对象的方法才会被事务拦截器处理。同类内部通过this.xxx()调用时直接调的是原对象的方法绕过了代理事务注解自然失效。这个坑在报修系统里很容易踩RepairOrderServiceImpl里createOrder方法调本类的updateDeviceStatus方法后者更新设备状态失败时工单却已经插进去了。解决把需要事务保护的两个操作拆到两个不同的Service方法里或者注入自身代理。最简单的是在RepairOrderServiceImpl里把设备状态更新逻辑放到DeviceServiceImpl里然后通过Autowired注入DeviceService再跨类调用。这样外部调用链变成RepairOrderService - DeviceService后者的方法由代理对象接收事务生效。正确写法示例RepairOrderServiceImpl通过注入DeviceService把更新设备状态的操作委托给它。Autowired private DeviceService deviceService; Transactional(rollbackFor Exception.class) public void createOrder(RepairOrder order) { repairOrderMapper.insert(order); deviceService.updateStatus(order.getDeviceId(), 0); }逻辑说明createOrder上的Transactional把insert和updateStatus包进同一个事务。关键在deviceService是代理对象updateStatus内部的事务传播级别默认REQUIRED会加入createOrder开启的事务任何一步抛异常都会整段回滚。如果你在RepairOrderServiceImpl里直接写this.updateDeviceStatus这个事务就断了。6. 从能跑到能答辩演示数据、视频录制与答辩自查清单系统能跑只是及格线毕业设计的评分很大程度取决于演示效果和答辩表现。这一章提供三个具体技巧用存储过程快速造出半个多月的工单数据按角色顺序录制视频演示以及答辩时被追问MySQL知识点怎么答。6.1 用一段存储过程造出半个多月的工单数据演示时最尴尬的事就是列表页空荡荡。手动一条条录工单既慢又假建一个临时存储过程批量生成数据几分钟就能造出几十条时间分布合理的工单。DELIMITER $$ CREATE PROCEDURE gen_orders() BEGIN DECLARE i INT DEFAULT 0; DECLARE uid INT DEFAULT 2; DECLARE did INT DEFAULT 1; WHILE i 50 DO INSERT INTO repair_order(order_no, device_id, device_name, device_location, user_id, description, status, create_time, update_time) VALUES ( CONCAT(RO, DATE_FORMAT(NOW(), %Y%m%d), LPAD(i, 4, 0)), 1 (i % 5), CONCAT(实验室设备, 1 (i % 5)), 综合楼三楼, uid, 演示数据自动生成, i % 4, DATE_SUB(NOW(), INTERVAL (50 - i) HOUR), NOW() ); SET i i 1; END WHILE; END$$ DELIMITER ; CALL gen_orders();逻辑说明1 (i % 5)让50条工单分散在5台设备上i % 4让状态在0到3之间均匀分布DATE_SUB(NOW(), INTERVAL (50 - i) HOUR)从当前时间倒推50小时生成的时间跨度覆盖了“最近两天”的演示窗口。生成完记得DROP PROCEDURE gen_orders避免数据库里留一个临时对象被答辩老师看到。演示数据这种外观要自然状态分布要合理别让30条工单全部显示“已完成”那一眼就假了。6.2 视频演示的录制顺序三个角色一条主流程视频演示的录制顺序我建议按真实使用场景走不要一上来就展示代码。第一步用普通用户账号提交一张报修单拍两张照片突出图片上传和表单校验第二步切换到维修人员账号从待受理列表里认领这张工单修改状态为维修中填写维修记录第三步切换到管理员账号查看工单统计和设备状态。整个过程控制在5到8分钟微信开发者工具的调试面板可以适当露出来让老师看到network请求但不要展示源码细节。6.3 答辩被问MySQL时的三个必答点老师问得最密集的永远是数据库部分。三个必答点提前准备第一InnoDB和MyISAM的区别你要说出InnoDB支持事务和外键这也是报修系统选InnoDB的原因第二项目的索引设计说清楚order_no唯一索引和status普通索引分别服务什么查询顺便提一句联合索引如果涉及多条件查询该怎么建第三MySQL 8.0相比5.7的差异重点提默认字符集utf8mb4和窗口函数这两个点能体现你真正用到了新版本特性。面试题里常考的隔离级别也可以准备一句话默认REPEATABLE READ通过MVCC解决幻读这个深度足够应对大多数提问。做这个项目到最后一刻我最大的教训是把所有坑集中记在一个md文件里从MySQL安装配置到小程序真机调试每解决一个问题就补一条。答辩前一晚拿出来过一遍很多问题其实早在联调阶段已经遇到过。希望帮到你。本文还有配套的精品资源点击获取