☰
SSM+MySQL+微信小程序设备报修系统开发实战指南
2026/9/26 1:04:52 网站建设 项目流程

简介:一套面向毕业设计场景的设备故障报修小程序完整交付方案,基于微信小程序+SSM+MySql架构开发,适合计算机相关专业学生参考或二次开发。资源覆盖管理员、用户、维修员三个角色,后台管理采用Java的SSM框架,小程序端通过微信开发者工具构建,实现实验室管理、报修提交、维修报告、经验分享与留言板等核心功能,并附带可行性分析、系统设计和数据库设计说明,实用性与完整性兼备。压缩包共1255个文件,约49.98MB,包含png截图、java后台源码、vue前端页面、js逻辑脚本、wxml/wxss小程序页面、sql数据库脚本,以及mp4视频演示和毕业论文文档,文件类型覆盖源码、文档与演示素材,结构清晰便于定位学习。已有130人浏览学习,适合需要快速搭建同类报修系统或完成毕业设计的学生获取完整落地方案。

1. 设备故障报修小程序:一个把SSM、MySQL和微信小程序串起来的毕业设计题目

每年毕业季,大量计算机专业学生被分到“设备故障报修小程序”这个题目。它不像电商系统那样堆功能,也不像纯管理后台那样只做CRUD,而是要你同时处理微信小程序端的表单交互、图片上传、工单状态流转,以及后端SSM框架下的权限控制和MySQL里的数据建模。这套系统的核心价值很直接:把原本靠电话、纸质单据流转的设备报修流程数字化,用户提交、维修人员接单、管理员监督,三个角色在同一闭环里协作。如果你正卡在这个选题上,这篇笔记会把从建库到联调的完整路径、参数怎么定、哪里最容易翻车,一次讲透。

2. SSM+MySQL+微信小程序这套组合凭什么能扛起报修系统

做毕业设计,最怕的不是功能少,而是技术栈之间互相打架。微信小程序负责界面,SSM负责后端业务,MySQL负责数据存储,这三者之间的接口边界如果一开始不划清楚,后面联调会非常痛苦。这一章先把选型和架构分工讲明白,让你在动手写代码之前就知道每个配置文件、每个请求路径该归谁管。

2.1 三层架构怎么切:Spring管对象,SpringMVC管路由,MyBatis管SQL

很多同学背过“SSM是Spring+SpringMVC+MyBatis”,但一写代码就懵:这个配置该放哪个文件?那个Bean该交给谁管?在报修系统里,后端需要按照一条清晰的调用链运转:微信小程序发来HTTP请求,SpringMVC的DispatcherServlet把请求分发给Controller,Controller调用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先解析请求头和请求体,交给RepairOrderController;Controller里注入RepairOrderService;Service里再注入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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表逻辑说明: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.xml

pom.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、password);spring-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时不要误判成数据库异常;插入时用useGeneratedKeys="true"把自增主键回填到user对象里,方便后续生成token。

<select id="selectByOpenid" resultType="com.example.repair.entity.SysUser"> SELECT * FROM sys_user WHERE username = #{openid} </select> <insert id="insertUser" useGeneratedKeys="true" keyProperty="id"> 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不传时默认GET,data不传时给空对象,避免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); List<RepairOrderVO> 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个高频问题

进入联调阶段后,遇到的问题往往不是“不会写代码”,而是“不知道去哪里查”。这一章把SSM+MySQL+微信小程序三端联调时最高频的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再预览。在命令行用ipconfig(Windows)或ifconfig(Mac)确认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里把连接串补全,加上serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true。allowPublicKeyRetrieval这个参数是因为MySQL 8.0在缓存SHA2密码时,非SSL连接需要显式允许公钥检索,不加它有时会直接报Public Key Retrieval is not allowed。三个参数一起写,不再单独查原因。

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/repair_system?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456

参数说明: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里加characterEncoding=utf8,确保JDBC读写字符集和数据库一致。然后在spring-mvc.xml里配置消息转换器,强制用UTF-8输出。

<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="defaultCharset" value="UTF-8"/> </bean> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven>

逻辑说明:StringHttpMessageConverter负责处理纯字符串响应,MappingJackson2HttpMessageConverter负责Java对象转JSON。如果你后端每个Controller方法都返回Result对象,主要是Jackson在起作用;但如果某个接口直接返回String,String的编码转换器配置错了就会乱码。两处都配上,根治问题。还有一种情况是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 id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="defaultEncoding" value="UTF-8"/> <property name="maxUploadSize" value="10485760"/> </bean>

参数说明:maxUploadSize上限10MB,是给整次请求的总大小兜底,避免恶意上传撑爆内存。defaultEncoding设置UTF-8,和前面JSON编码一致。注意bean的id必须是multipartResolver,SpringMVC在初始化时按这个固定名称查找解析器,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安装配置到小程序真机调试,每解决一个问题就补一条。答辩前一晚拿出来过一遍,很多问题其实早在联调阶段已经遇到过。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询