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转换器十几步操作而使用SpringBootspring-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功能在报修管理中非常实用。比如筛选报修单列表时用户可能选择“只查我的工单”“只查未处理的”“按紧急程度过滤”条件组合多种多样。whereif标签组合几行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以上版本建议带useSSLfalseserverTimezoneAsia/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) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }为什么必须统一格式因为前端Axios拦截器只要判断一次code字段就能统一处理所有请求的成功/失败状态不需要针对每个接口写一遍if/else。碰到登录过期后端返回code401前端拦截器统一跳转到登录页碰到业务异常后端返回code500和具体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 AuthenticationMySQL 8.0默认推荐Use Legacy Authentication兼容老版本客户端这一步极其关键。如果你要连接的客户端比如DBeaver、Navicat、老版本mysql-connector-java不支持新的caching_sha2_password认证连接会直接报错“Unable to load authentication plugin”。如果此时你已经用root登录不了需要在my.ini的[mysqld]节点下临时加一行default-authentication-pluginmysql_native_password再重启服务。创建数据库和授权CREATE DATABASE repair_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER repair_app% IDENTIFIED BY Repair123456; 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?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: repair_app password: Repair123456 driver-class-name: com.mysql.cj.jdbc.Driver时区配置serverTimezoneAsia/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.activeprod生产环境建议用nohup或者systemd守护进程方式运行不要用java -jar直接霸占终端。简单的systemd配置如下[Unit] DescriptionRepair System SpringBoot App Aftermysql.service [Service] Userrepair ExecStart/usr/bin/java -jar /opt/repair/repair-system.jar Restartalways RestartSec5 [Install] WantedBymulti-user.target6.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-formatyyyy-MM-dd HH:mm:ssMaven中spring-boot-maven-plugin报错JDK和插件版本不匹配4.x插件配JDK 8会有问题改用2.x版本这张表里的前四个问题我基本每隔一段时间就会遇到一次覆盖了环境入门阶段90%的卡点。7. 项目扩展与实操总结到这里整套系统的技术链路就完整了。最后聊几个和我实战经验最相关的话题希望能帮你进一步拓清思路。关于源码质量很多人喜欢直接拿网上的开源项目改个名就交差。说实话这种操作毕业论文查重大概率逃不过而且源码里埋了很多坑比如写死的数据源配置、没有统一异常处理、安全漏洞百出。我的建议是参考开源项目的数据表设计和接口规划但代码务必亲手敲一遍。报修系统这个量级的项目一个人一周到十天完全可以写完。手敲一遍的价值不仅在于“知道怎么跑通”更在于你熟悉了每个类、每个方法、每个SQL的位置和逻辑答辩被问到任何细节都能从容回答。关于学习路径的内化如果你是为了找工作而做这个项目建议完成基础版后自己再追加一两个亮点。比如用Redis给“报修分类统计”加缓存、用RabbitMQ做“报修提交后异步通知维修工”、用定时任务自动将超时未接单的工单升级为紧急工单并在首页提醒管理员。这些扩展功能在实际面试中非常加分因为它们展示了你对技术栈的综合应用能力而不只是简单的CRUD搬砖。当然每一项扩展都需要对应的中间件支持如果嫌部署麻烦优先选Redis因为它体积小巧、Windows上也能跑。关于常见错误的最后一个提醒开发阶段一定要养成看控制台日志的习惯。不要只盯前端页面报错信息。后端日志里SpringBoot默认会输出完整的堆栈信息比如FileNotFoundException、NullPointerException、SQLException精确定位到代码行号。遇到问题时把异常信息复制到搜索引擎或者大模型工具里问一圈基本都能找到解决办法这是程序员最核心的生存技能之一。我个人在实际操练这个项目的过程中深有体会项目本身不难难的是每一步都不出差错地走完。从环境的匹配、数据库的初始化、到前端构建部署任何一个环节脱节都会浪费时间。建议严格按照本文的顺序先把后端跑通Postman测试接口再搭前端联调页面调通核心流程最后再考虑部署上线。按这个节奏走这个项目你一定能顺利拿下来。