☰
基于SpringBoot+Vue+MyBatis的公寓报修管理系统全栈开发实战
2026/10/9 8:22:05 网站建设 项目流程

1. 项目概述

先说结论:这是一套非常典型的“前后端分离 + 关系型数据库”全栈管理系统,业务逻辑围绕“宿舍/公寓报修工单”的完整生命周期展开——用户在线提交报修、维修工接单处理、管理员统一监管。技术上就是SpringBoot扛后端接口,Vue做前端页面,MyBatis负责SQL操作,MySQL存数据。这四个东西组合在一起,说句实在话,就是目前国内Java后端岗位和毕业设计里最主流的“标准套餐”之一。

为什么这套组合这么普及?原因很简单:每一层都有极其成熟的生态。SpringBoot把Spring那套复杂的XML配置彻底简化了,内嵌Tomcat,一个java -jar就能把服务跑起来;Vue自带响应式机制和组件化开发模式,写后台管理系统效率极高;MyBatis把SQL和Java代码解耦,xml文件里写好SQL就能映射成对象;MySQL开源免费、性能稳定,中小型业务场景下根本不需要考虑更高端的数据库。对于你要做的公寓报修系统这种体量的项目,这套技术栈完全够用,而且网上资料极其丰富,遇到问题随便一搜就有解决方案。

这篇博文我打算把整个项目从零到一拆开揉碎来讲,包括数据库设计、后端接口逻辑、前端页面交互、环境版本匹配、部署上线,以及我作为老开发踩过的大大小小的坑。无论你是计算机专业做毕设的学生、自学Java全栈的初学者、还是想给单位/宿舍快速搭一套内部报修平台的工程师,这篇文章应该都能帮你省下大量试错时间。

先说个大前提:这个系统我用的是JDK 1.8(后面细说为什么不用高版本)、SpringBoot 2.7.x、MyBatis 3.5.x、MySQL 5.7或8.0都可以、Vue 2.6.x配Element UI。截至2025年,这套版本组合依然是兼容性最稳、坑最少、资料最多的“黄金组合”。

2. 整体设计与技术选型思路拆解

2.1 为什么选SpringBoot而非SpringMVC单体项目?

其实很多初学Java的读者可能会有疑问:公寓报修系统这种中小型项目,用传统的SSM框架(Spring + SpringMVC + MyBatis)完全能写,为什么要用SpringBoot?

区别在于开发的工程化体验。SSM时代,你需要手动配置web.xml、spring-context.xml、springmvc.xml,还得处理一堆jar包版本冲突,光环境搭建就能折腾一两天。SpringBoot的核心思想是“约定优于配置”,自动配置帮你处理掉绝大多数繁琐配置工作,你只需要把注意力放在业务逻辑上。

以一个具体的开发流程对比为例:如果使用SSM,创建一个CRUD接口可能需要配置事务管理器、配置视图解析器、配置拦截器、配置Jackson转换器,十几步操作;而使用SpringBoot,spring-boot-starter-web依赖一引入,加上@RestController注解,一个JSON接口就通了。这种开发效率差距在互联网公司里体现为压缩了整个项目的交付周期。

另外非常重要的一点是生态的延续性。SpringBoot 2.7.x基于Spring 5.3,既兼容传统的SSM写法,又支持大量云原生特性,未来如果项目要升级成SpringCloud微服务架构,底层的改造工作量也是可控的。你的报修系统不可能永远是一个独立小单体,一旦需要对接校园统一登录、对接企业微信通知、对接统一支付平台,SpringBoot天然的starter机制会大幅降低这些集成的风险。

2.2 前后端分离到底解决了什么问题?

公寓报修管理系统的开发团队(或者说一个人)通常会分为两条线推进:前端负责页面展示和用户交互,后端负责业务逻辑和数据库操作。如果用传统模板引擎(如JSP、Thymeleaf)写,前端页面和后端Java代码耦合在同一个工程里,每改动一个按钮样式都要重新编译打包,而且前端设计师根本无法独立开发调试。

前后端分离后,Vue工程运行在8080端口,SpringBoot工程运行在8081端口(或直接用nginx转发),两者只通过JSON数据进行通信。前端的路由、页面状态、交互逻辑完全由Vue管理;后端只提供RESTful API,不关心数据最终展示成什么样。这种模式最直接的好处是——后端和前端可以并行开发,接口约定好之后各干各的,效率直线上升。

另外,对报修系统来说,用户角色分为学生(提交报修)、维修工(处理报修)、管理员(统计分析),三种角色的页面差异很大。如果用服务端渲染,每次切换角色都要重新加载整个页面;用Vue后,前端通过路由懒加载区分角色模块,切换页面时只加载对应组件,体感流畅得多。

2.3 MyBatis还是JPA?数据库访问层如何取舍

很多毕设或者学习项目会面对一个经典纠结:数据库访问层用MyBatis还是Spring Data JPA?

我的选择是MyBatis,理由非常务实。报修系统虽然实体不多(用户、报修单、维修记录、评价),但报表统计类查询非常复杂,比如“某一个月内完成率最高的维修工”、“每个宿舍楼的报修分类占比”这类SQL,需要多表联查、子查询、聚合函数。MyBatis的XML文件可以编写完整的SQL语句,调优逻辑一目了然;而JPA虽然写CRUD特别快,但遇到复杂查询时,要么写JPQL(一样要学习)、要么用原生SQL(需要对JPA细节有很深理解),一旦报错排查起来相当消耗时间。

此外,MyBatis的动态SQL功能在报修管理中非常实用。比如筛选报修单列表时,用户可能选择“只查我的工单”“只查未处理的”“按紧急程度过滤”,条件组合多种多样。<where>+<if>标签组合几行XML就能解决动态条件拼装问题,比Java代码里拼接字符串SQL优雅得多,而且不会出现SQL注入风险。

2.4 MySQL为何是这个量级项目的稳定解

对于公寓/宿舍报修这种场景,用户量级撑死了几千人,报修单数据一年最多几万条,MySQL在这个体量下表现非常从容。InnoDB引擎支持行级锁和事务,报修单创建、状态流转、完成确认这几个动作都涉及数据一致性,事务机制足够可靠。5.7版本和8.0版本我都跑过这个系统,没有遇到兼容性障碍,建议用5.7.44,经历多年打磨,稳定性天花板极高;mysql-connector-java用8.0.x版本,可以和5.7跟8.0两个版本都兼容。唯一要注意的是驱动连接串写法:8.0以上版本建议带useSSL=false&serverTimezone=Asia/Shanghai参数,避免时区和SSL报错。

3. 数据库模型设计与建表实现

3.1 核心表结构与字段规划

报修系统的数据模型不需要很复杂,四张核心表就够了:用户表(t_user)、报修单表(t_repair)、维修记录表(t_repair_log)、评价表(t_evaluation)。如果你们学校要求做宿舍楼管理,可以再加一张楼栋表(t_building),但核心逻辑不变。

我把设计思路和DDL完整贴一下(基于我自己实际工程的简化版):

-- 用户表:学生/维修工/管理员共用 CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT 'MD5/BCrypt加密密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `role` tinyint(4) NOT NULL DEFAULT '1' COMMENT '角色:1学生 2维修工 3管理员', `building_no` varchar(20) DEFAULT NULL COMMENT '所在楼栋号(学生用)', `room_no` varchar(20) DEFAULT NULL COMMENT '房间号(学生用)', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1正常 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

这里有两个很容易踩的细节。第一,password字段长度至少留100个字符,如果你用BCrypt加密,生成的字符串长度是60位,MD5加盐的话长度可能到56位,千万别只留32位,否则上线第一天就大面积报错。第二,role字段用数字枚举值而不是字符串,能显著降低存储开销,同时Java侧定义一个枚举类或者直接用常量类去对应。

报修单表是整个系统的核心业务表,设计时重点考虑状态流转字段:

CREATE TABLE `t_repair` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `repair_no` varchar(32) NOT NULL COMMENT '报修单编号,如BX20250101001', `user_id` bigint(20) NOT NULL COMMENT '提交人ID', `worker_id` bigint(20) DEFAULT NULL COMMENT '维修工ID,未接单时为空', `title` varchar(100) NOT NULL COMMENT '报修标题', `description` text COMMENT '问题详细描述', `images` varchar(500) DEFAULT NULL COMMENT '图片路径,多张用逗号分隔', `category` tinyint(4) DEFAULT NULL COMMENT '分类:1水电 2网络 3门窗 4家电 5其他', `priority` tinyint(4) NOT NULL DEFAULT '2' COMMENT '优先级:1低 2中 3高', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1待派单 2已接单 3维修中 4已完成 5已评价', `appointment_time` datetime DEFAULT NULL COMMENT '期望上门时间', `accept_time` datetime DEFAULT NULL COMMENT '接单时间', `finish_time` datetime DEFAULT NULL COMMENT '完成时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_user_id` (`user_id`), KEY `idx_worker_id` (`worker_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报修单表';

重点说一下status字段的状态机设计。用数字枚举值的好处是扩展性强,状态流转规则写在Java枚举里。我设计的是:待派单→已接单→维修中→已完成→已评价,其中可以从待派单直接跳到已完成(管理员特殊处理),从维修中直接跳到待派单(维修工操作失败退回)。前端的按钮显隐完全是后端返回的status驱动的,而不是前端自己改状态,这样能保证多人同时操作时数据不会乱。

3.2 索引优化与事务设计思路

小项目也要注意基础优化。上面建表语句里我加了idx_status、idx_user_id、idx_worker_id三个索引,这是根据业务查询路径反推的:首页列表基本按“当前用户+状态”过滤,维修工端按“当前维修工+状态”过滤,所以这三个索引可以覆盖90%的查询场景。关于联合索引,如果后续要做“某栋楼所有待处理工单”这种统计,可以考虑加(building_no, status)联合索引,但初版不必过度设计。

事务方面,核心的“接单”“完成维修”“评价”三个操作必须加@Transactional。以接单为例,流程是:更新报修单的worker_id和status变为已接单,同时可能插入一条维修记录日志。如果没有事务,第一步成功而第二步失败,报修单状态变更为已接单但没有任何日志记录,后续排查问题就非常困难。悲观锁在报修系统里不太需要,因为并发量很低,但是要注意乐观锁的一个变通用法:更新报修单时加入WHERE status = 前一个状态的CAS式条件,防止两个维修工同时抢同一条工单。

4. 后端SpringBoot接口实现与业务逻辑

4.1 项目工程结构与分层设计

我习惯的工程目录结构是这样的:

src/main/java ├── com.example.repair │ ├── common // 通用工具类、常量、统一返回结构 │ │ ├── Result.java │ │ ├── ResultCode.java │ │ └── GlobalExceptionHandler.java │ ├── config // 跨域配置、MyBatis配置、拦截器配置 │ │ └── CorsConfig.java │ ├── controller // 接口调用层 │ │ ├── UserController.java │ │ ├── RepairController.java │ │ └── AdminController.java │ ├── service // 业务处理层 + 实现类 │ │ ├── RepairService.java │ │ └── impl │ │ └── RepairServiceImpl.java │ ├── mapper // MyBatis接口层 │ │ ├── RepairMapper.java │ │ └── xml → resources/mapper/RepairMapper.xml │ ├── entity // 数据库实体类 │ └── vo // 视图对象,供前端直接显示

很多初学者容易犯一个错误:把Controller写得巨胖,所有业务逻辑全堆在里面。说实话,如果只是应付答辩或纯学习,短期这样做确实能跑通,但后续维护会让你想当场离职。分层不是为了好看,是因为每个层都有独立的变更理由:Controller关注参数校验和HTTP响应格式;Service关注业务规则和事务边界;Mapper只关注SQL访问。我强烈建议从项目第一天就坚持这个分层习惯,这甚至比功能实现本身更值钱。

4.2 统一返回结构的意义与实现

后端接口一定要统一返回格式,这是前后端分离项目的基石。我用的结构如下:

{ "code": 200, "message": "success", "data": { ... } }

对应Java类:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

为什么必须统一格式?因为前端Axios拦截器只要判断一次code字段就能统一处理所有请求的成功/失败状态,不需要针对每个接口写一遍if/else。碰到登录过期,后端返回code=401,前端拦截器统一跳转到登录页;碰到业务异常,后端返回code=500和具体message,前端Message组件统一弹出。这个规范看似简单,但如果你不实现,前端代码会被各种非标准返回结果搞得千疮百孔。

4.3 报修工单的全流程接口设计

梳理一下核心接口清单(RESTful风格):

方法路径功能角色
POST/api/auth/login登录所有
POST/api/auth/logout退出所有
GET/api/repair/page分页查询报修单(支持状态、分类、楼栋筛选)管理员
POST/api/repair提交报修单学生
PUT/api/repair/{id}/assign派单给维修工管理员
PUT/api/repair/{id}/accept维修工接单维修工
PUT/api/repair/{id}/start开始维修维修工
PUT/api/repair/{id}/finish完成维修维修工
PUT/api/repair/{id}/evaluate评价学生
GET/api/repair/statistics报修汇总统计管理员

以“提交报修单”为例,Service层的核心逻辑是:

@Transactional public Long submitRepair(RepairSubmitVO vo, Long userId) { Repair repair = new Repair(); BeanUtils.copyProperties(vo, repair); repair.setUserId(userId); repair.setStatus(RepairStatusEnum.PENDING_ASSIGN.getCode()); // 生成唯一的报修编号:前缀BX + 年月日 + 随机三位 repair.setRepairNo(generateRepairNo()); repairMapper.insert(repair); return repair.getId(); }

注意这里并没有玩出复杂花样。业务规则简化为三条:表单校验交给前端(必须写:标题、描述、分类、预约时间)、状态初始化固定为待派单、报修编号保证唯一。加上@Transactional后,任何时候插入失败都会连同报修单数据一起回滚。

4.4 权限拦截与登录状态验证

SpringBoot实现登录拦截最常规的方案是HandlerInterceptor加JWT(或Session)。我用的是JWT,因为前后端分离模式下JWT天然支持跨域和移动端复用。具体实现:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { // 放行登录接口 if (request.getRequestURI().contains("/auth/login")) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token)) { response.setStatus(401); response.getWriter().write("未登录"); return false; } // 校验token,解析出userId和role Claims claims = JwtUtil.parseToken(token); if (claims == null) { response.setStatus(401); return false; } request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } }

这个拦截器需要注册到WebMvcConfigurer里,并配置好放行路径(如静态资源、登录接口、Swagger文档)。权限控制上,在Controller方法上加自定义注解@RequireRole(1),通过AOP切面判断当前用户角色是否匹配。这种注解方式比在每个接口里手写if判断优雅得多,见下面代码:

@Aspect @Component public class RoleAspect { @Before("@annotation(requireRole)") public void checkRole(JoinPoint joinPoint, RequireRole requireRole) { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); if (attrs != null) { HttpServletRequest request = ((ServletRequestAttributes) attrs).getRequest(); Integer role = (Integer) request.getAttribute("role"); if (role == null || role.intValue() != requireRole.value()) { throw new BusinessException("无权限操作"); } } } }

每次同学找我调试“接口被拦截”或“前端明明带着token但后端401”的问题,80%的情况是:Swagger测试的时候忘记在Header加上Authorization、或者JWT过期时间设置太短(比如设置1分钟)、或者前端Axios拦截器没有统一把token塞进请求头。这三种情况你写代码时一定要提前规避。

5. 前端Vue实现与交互页面拆解

5.1 前端工程初始化与环境配置

慢慢说前端这块。按照主流配置,我在Vue 2.6.14环境下跑项目。初始化用vue create命令(或手动webpack模板),装好vue-router、vuex、axios、element-ui。开发环境配置要点是代理转发:

// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true, pathRewrite: { '^/api': '' } } } }, lintOnSave: false // 关掉eslint,不然缩进空行能折腾死你 }

这里解释两个关键点。第一,前端开发服务器跑在8080,后端SpringBoot跑在8081,浏览器访问前端页面时的/api开头的请求会被Vue脚手架代理转发到后端,这样就不存在跨域问题。第二,后端依然要配置CorsFilter,防止以后前端不是通过代理而是直连后端时跨域被拦截。双保险,都别省。

前端页面结构按角色拆分为三个模块,路由配置如下:

const routes = [ { path: '/login', component: Login }, { path: '/student', component: StudentLayout, children: [ { path: 'submit', component: SubmitRepair }, // 提交报修 { path: 'list', component: StudentRepairList }, // 我的报修记录 { path: 'evaluate/:id', component: EvaluateRepair } // 评价 ] }, { path: '/worker', component: WorkerLayout, children: [ { path: 'tasks', component: WorkerTasks }, { path: 'history', component: WorkerHistory } ] }, { path: '/admin', component: AdminLayout, children: [ { path: 'dashboard', component: Dashboard }, // 数据统计面板 { path: 'repairs', component: AdminRepairList }, { path: 'users', component: UserManage } ] } ]

Vue Router的beforeEach钩子做登录验证和角色守卫:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.path !== '/login' && !token) { next('/login'); } else { next(); } });

这个路由结构对于中小型后台管理系统来说直观清楚,比动态路由(根据后端返回的权限表动态生成路由)简单得多,也不容易出错。动态路由适合大型多权限系统,报修管理的角色固定就三个,写死足够。

5.2 状态驱动的页面交互细节

报修单列表页是整个前端最核心的页面。我采用的方案是:使用Element UI的el-table展示数据,状态列用el-tag渲染不同颜色;操作列根据当前行的status动态显示按钮。

以维修工视角为例,一行报修单在不同状态下操作列应该是:

状态操作按钮
待派单无(还没分配到维修工)
已接单开始维修
维修中完成维修
完成后只读

这个动态判断逻辑必须在前端完成,同时后端接口也要做二次校验。这样做的原因很简单:前端控制的是用户体验,后端控制的才是数据安全。只有后端校验会导致用户看到“还没有权限的按钮”,只有前端校验会导致接口被恶意调用时无任何防护。两边都要写,缺一不可。

顺便提一个很影响体验的细节:提交报修表单时,图片上传组件建议做文件大小限制和图片压缩。我实测过,学生用手机拍照上传一张原图动辄3-5MB,如果不压缩,Tomcat默认1MB的上传限制直接报错。解决方案是前端el-upload组件配合before-upload钩子用canvas压缩图片,控制在500KB以内再传到后端。这个小功能能帮你省掉大量“上传报错”的工单。

5.3 Axios封装与数据状态管理

Axios封装的核心作用是集中处理token携带、错误提示、加载状态。我的实际代码:

// src/utils/request.js import axios from 'axios'; import { Message } from 'element-ui'; import router from '@/router'; const service = axios.create({ baseURL: '/api', timeout: 10000 }); service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = token; } return config; }); service.interceptors.response.use( response => { const res = response.data; if (res.code === 200) { return res.data; } else { Message.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); Message.error('登录已过期,请重新登录'); } else { Message.error('网络异常或服务器内部错误'); } return Promise.reject(error); } ); export default service;

这套封装的好处是所有列表页代码量很少,每个接口只需要关注自己的业务逻辑。比如提交报修:

submitRepair(formData) { return request.post('/repair', formData); }

我看到不少学习项目用fetch原生接口,每个页面自己写headers、自己处理错误码、自己跳登录,代码重复率极高。对于毕设和平时自己写项目来说,统一封装真的能让后续开发和调试舒服很多。

5.4 数据统计面板的图表实现

管理员角色往往会有一个可视化的统计首页,展示本月报修总量、各类型占比、各楼栋报修排行、维修工完成率排名。这个页面用封装ECharts来实现。

一个很好的Vue-ECharts集成方式是直接用echarts配合封装好的chart组件,在mounted阶段初始化实例:

mounted() { this.chart = echarts.init(this.$refs.chartRef); this.loadData(); }, methods: { loadData() { request.get('/repair/statistics', { params: { month: this.currentMonth } }).then(data => { this.chart.setOption({ title: { text: '本月报修分类统计' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.categories }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.counts }] }); }); } }

这里有个容易忽略的问题:ECharts图表在容器尺寸变化时不会自动重新计算宽高。如果你的页面有侧边栏折叠功能,折叠后图表会变得很挤或者空白。解决办法是监听侧边栏状态变化后调用chart.resize()方法。这个坑我记忆深刻,某次项目答辩时折叠侧边栏导致图表白屏,当场尴尬了一小段时间。

客户端渲染ECharts在页面切换时会有一段图表空白加载过程。如果毕设答辩需要录屏演示,建议在data里手动设置初始的option数据,让框架先渲染首屏内容,数据返回后再用setOption填充真实数据。这种细节在正式项目里往往体现出一个开发者考虑问题的完整程度。

6. 环境搭建、版本匹配与部署避坑

6.1 JDK与SpringBoot的版本选择逻辑

先说结论:截止2025年,不要盲目追求高版本。SpringBoot 2.7.x + JDK 1.8是当前最稳妥、运行占用低、兼容性最强、社区资料最多的搭配。很多人一上来就直接用JDK 17甚至JDK 21,然后Spring Initializr生成的SpringBoot 3.x依赖,跑起来一堆问题:比如MyBatis的typeAliasesPackage配置格式变了、javax.servlet变成了jakarta.servlet、一些第三方starter彻底不兼容了。初学者在这些环境问题上死磕纯粹是浪费时间,虽然有挑战性,但收益非常有限。

如果确实想用新特性,可以谈到SpringBoot 3.x + JDK 17的方案,但必须同时了解SpringBoot 3的两个重要变化:第一是javax包迁移到了jakarta,代码引用需要全局替换;第二是MyBatis官方建议用mybatis-spring-boot-starter2.3.1以上版本。实际上这两种分支我都在项目中试过,就报修系统的复杂度来说,平稳性优先原则建议用成熟稳定版本。

6.2 MySQL的安装与初始化细节

这一部分有必要展开说,因为我见到太多人在这个环节卡壳。MySQL安装分两种情况:Windows本地安装和Linux服务器部署。

Windows推荐直接用MySQL Installer图形化工具,选择Server Only即可。安装过程中有个环节是选认证方式:

  • Use Strong Password Encryption for Authentication(MySQL 8.0默认推荐)
  • Use Legacy Authentication(兼容老版本客户端)

这一步极其关键。如果你要连接的客户端(比如DBeaver、Navicat、老版本mysql-connector-java)不支持新的caching_sha2_password认证,连接会直接报错“Unable to load authentication plugin”。如果此时你已经用root登录不了,需要在my.ini的[mysqld]节点下临时加一行default-authentication-plugin=mysql_native_password再重启服务。

创建数据库和授权:

CREATE DATABASE repair_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'repair_app'@'%' IDENTIFIED BY 'Repair@123456'; GRANT ALL PRIVILEGES ON repair_system.* TO 'repair_app'@'%'; FLUSH PRIVILEGES;

这里务必注意两点:第一,数据库字符集一定用utf8mb4而不是utf8。MySQL的utf8实际上只支持部分UTF-8字符,遇到生僻字或emoji会插入失败,而utf8mb4是完整版。第二,不要用root账号连接应用。创建专门的应用账号,权限最小化是基本素养,也是答辩时老师会关注的专业细节。

启动SpringBoot项目时,确保application.yml里的连接参数和上面配置一致:

spring: datasource: url: jdbc:mysql://localhost:3306/repair_system?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: repair_app password: Repair@123456 driver-class-name: com.mysql.cj.jdbc.Driver

时区配置serverTimezone=Asia/Shanghai是强制要求。不写这个参数,连接MySQL 8.0时通常会报“The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized”错误,这是因为服务器默认时区没有正确识别。加上后问题立刻消失。

6.3 前端构建与Nginx部署实战

前端开发完成后的标准部署流程是:

npm run build

构建完成后会在项目根目录生成dist文件夹,里面是编译压缩过的静态文件。把这个dist文件夹复制到服务器的nginx html目录即可。

Nginx配置中最关键的是前端history路由的try_files配置和API反向代理,建议参考:

server { listen 80; server_name your_domain_or_ip; root /usr/share/nginx/html/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

try_files $uri $uri/ /index.html这行千万不能省。Vue Router使用history模式后,用户直接在浏览器输入/student/list路径,nginx默认找不到对应的物理文件,会返回404,而try_files会将所有不存在的路径回退到index.html,再交给Vue Router内部处理,这样刷新页面就不会白屏。

后端打包部署也很简单:

mvn clean package -DskipTests java -jar target/repair-system-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

生产环境建议用nohup或者systemd守护进程方式运行,不要用java -jar直接霸占终端。简单的systemd配置如下:

[Unit] Description=Repair System SpringBoot App After=mysql.service [Service] User=repair ExecStart=/usr/bin/java -jar /opt/repair/repair-system.jar Restart=always RestartSec=5 [Install] WantedBy=multi-user.target

6.4 常见版本兼容性问题速查

异常现象根因解决方案
Tomcat启动失败,错误包含Unable to start web server端口8080被占用netstat -ano查PID后释放端口,或改server.port
连接MySQL报Access denied for user用户名密码或授权错误用root登录后重新GRANT授权,清空密码重试
MyBatis报Invalid bound statement (not found)Mapper接口和XML文件未绑定检查XML文件路径是否在mapper-locations里
Jackson序列化Date格式不对默认时间是时间戳application.yml加spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
Maven中spring-boot-maven-plugin报错JDK和插件版本不匹配4.x插件配JDK 8会有问题,改用2.x版本

这张表里的前四个问题我基本每隔一段时间就会遇到一次,覆盖了环境入门阶段90%的卡点。

7. 项目扩展与实操总结

到这里整套系统的技术链路就完整了。最后聊几个和我实战经验最相关的话题,希望能帮你进一步拓清思路。

关于源码质量:很多人喜欢直接拿网上的开源项目改个名就交差。说实话,这种操作毕业论文查重大概率逃不过,而且源码里埋了很多坑(比如写死的数据源配置、没有统一异常处理、安全漏洞百出)。我的建议是:参考开源项目的数据表设计和接口规划,但代码务必亲手敲一遍。报修系统这个量级的项目,一个人一周到十天完全可以写完。手敲一遍的价值不仅在于“知道怎么跑通”,更在于你熟悉了每个类、每个方法、每个SQL的位置和逻辑,答辩被问到任何细节都能从容回答。

关于学习路径的内化:如果你是为了找工作而做这个项目,建议完成基础版后自己再追加一两个亮点。比如:用Redis给“报修分类统计”加缓存、用RabbitMQ做“报修提交后异步通知维修工”、用定时任务自动将超时未接单的工单升级为紧急工单并在首页提醒管理员。这些扩展功能在实际面试中非常加分,因为它们展示了你对技术栈的综合应用能力,而不只是简单的CRUD搬砖。当然,每一项扩展都需要对应的中间件支持,如果嫌部署麻烦,优先选Redis,因为它体积小巧、Windows上也能跑。

关于常见错误的最后一个提醒:开发阶段一定要养成看控制台日志的习惯。不要只盯前端页面报错信息。后端日志里SpringBoot默认会输出完整的堆栈信息,比如FileNotFoundException、NullPointerException、SQLException,精确定位到代码行号。遇到问题时把异常信息复制到搜索引擎或者大模型工具里问一圈,基本都能找到解决办法,这是程序员最核心的生存技能之一。

我个人在实际操练这个项目的过程中深有体会:项目本身不难,难的是每一步都不出差错地走完。从环境的匹配、数据库的初始化、到前端构建部署,任何一个环节脱节都会浪费时间。建议严格按照本文的顺序,先把后端跑通(Postman测试接口),再搭前端联调(页面调通核心流程),最后再考虑部署上线。按这个节奏走,这个项目你一定能顺利拿下来。

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

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

立即咨询