多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SpringBoot+Vue搭建企业车辆管理系统:设计开发部署全解析

SpringBoot+Vue搭建企业车辆管理系统:设计开发部署全解析 看到这个标题我第一反应就是这又是一个典型的课程/毕设级Java全栈项目。但别急着划走企业车辆管理系统这种垂直业务恰恰是SpringBootVue这套技术栈最有代表性的落地场景——它不只包含增删改查还牵扯审批流、状态机、数据关联、权限控制正好把Java后端和前端工程的核心知识点全部串了一遍。这篇文章我会把选型逻辑、数据库设计、后端写法、前端对接和部署踩坑从头到尾盘一遍尤其是2025年这个时间点版本坑太多直接照着做能省你两三天。1. 业务拆解一套企业车辆管理系统到底要管哪些事很多人拿到车辆管理系统这种项目第一反应就是做一张车辆信息的CRUD表格。这想法没错但如果你真去企业里用过车队管理系统就知道这里面远不止登记车辆这么简单。我先把这个系统的业务模型完整拆一遍后面写代码和建表才不会跑偏。1.1 核心业务流程一辆车从申请到归还的全生命周期企业车辆管理的本质是资源调度流程审批。它围绕一辆车、一个司机、一个申请人展开这样一条链路车辆档案管理车牌号、品牌型号、车辆类型轿车/商务车/货车、购置日期、保险公司、年检到期日、行驶证信息。这部分是纯基础数据也是最容易做的。司机/驾驶员管理不能只看系统里有没有司机得看司机有没有对应的准驾资格、驾照到期日、联系方式、所属部门。真实项目里司机表和车辆表是一对一或一对多关系。用车申请与审批员工提交用车申请起止时间、目的地、用车事由部门负责人审批调度员派车。这是整个系统的核心链路也是工作量最大的部分。出车与归还司机出车后更新车辆状态空闲/出车中/维修中归还时登记里程数系统自动计算本次行驶里程和大致油费。维修/保养/违章/保险这些属于养车侧的内容车辆不是只管开就行保养到期提醒、违章记录登记、保险续期预警都要有。说实话核心链路里最值得研究的还是申请→审批→派车→出车→归还这个状态流转。一辆车同一时间只能被一个人用可用/占用/维修三个状态之间的流转必须有明确的约束逻辑不然就会出现两单都派了同一辆车的情况。1.2 这套系统值得复盘的四个技术点从技术学习的角度看车辆管理系统比博客系统、商城系统更适合做SpringBootVue的练手项目原因有四数据关联类型丰富一对多一个司机多辆车、多对一多张申请单对应一个用车人、多对多多辆车内多个审批环节几乎覆盖了MyBatis关联查询的所有写法。权限粒度适中角色的概念被自然引出来——普通员工能提交申请部门主管能审批调度员能派车管理员能管全量数据正好用来练习拦截器和JWT鉴权。时间与状态字段不可少车辆保养提醒、保险到期、长期未归还的申请单这些都是需要在SQL里写时间函数和状态判断的场景。前端交互不简单申请表单涉及级联选择选车、选司机、选时间周期车辆状态需要动态变色审批列表需要可折叠展示Vue的组件拆分、状态管理、路由守卫这些知识点全用得上。把这个业务模型吃透了后面写表和代码其实是水到渠成的事。2. 2025年版本搭配SpringBoot、Vue、MySQL到底怎么选网络热词里springboot版本太高这个搜索挺扎心的我太懂这个痛点了。现在一搜教程全是SpringBoot 3.2/3.3的写法但很多老教程用的还是2.x照抄代码一堆报错。不是说新版本不能学而是要自己心里有数你的JDK版本、Maven版本、依赖坐标是否跟得上。这里先把我实测下来最稳妥的版本组合放出来。2.1 我自己在用的这套版本组合组件推荐版本说明JDK172025年还在用JDK 8的话很多新版本依赖都不再支持SpringBoot3.2.x3.x系列已经非常成熟官方推荐的长期稳定路线MyBatis3.5.16单独用MyBatis不依赖MyBatis-Plus也能搞定mybatis-spring3.0.3注意SpringBoot 3.x要配mybatis-spring 3.x别用2.xVue3.4.xVue3已经全面占据主导地位直接上3Vite5.x比Webpack轻快太多2025年不需要再纠结WebpackElement Plus2.7.xVue3对应的UI组件库MySQL8.0.x8.0是绝对主流5.7已经不适合新项目这里补充一句为什么SpringBoot 3.x和2.x在MyBatis上这么容易出问题SpringBoot 3.x换到了jakarta.*命名空间不再用javax.*。如果你pom里引的mybatis-spring-boot-starter还是老版本的2.x它内部依赖还是javax.sql那套运行时必然报找不到类的错误。所以选择依赖坐标时一定要确认dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency2.2 如果你非要守旧用SpringBoot 2.7这些差异要知道有些同学的毕设要求或者公司内网环境还是JDK 8SpringBoot 2.x那也不是不能做。只是心里要有这张对应表SpringBoot 2.7 JDK 8 MyBatis starter 2.3.x实测也稳定。javax.servlet和jakarta.servlet的导入包路径不同代码里如果import了javax用2.ximportjakarta用3.x。MySQL驱动坐标在2.x里写mysql-connector-java3.x里写com.mysql:mysql-connector-j并且MySQL 8.0以上建议把驱动类写com.mysql.cj.jdbc.Driver。其实最怕的不是选老版本而是教程用2.7你环境却装了JDK 17SpringBoot 2.7默认用javax在JDK 17下虽然能跑但到SpringBoot 3.x时代就已经不推荐了。所以我的建议很简单没有特殊理由新项目一律JDK 17SpringBoot 3.2.x遇到问题搜springboot3关键词资料比2024年时丰富得多。2.3 前端环境两个高频坑Vite版本和依赖安装失败Vue部分最容易出问题的不是Vue本身而是Vite和Node.js的版本匹配。Vite 5要求Node.js 18要是你机器上还是Node 14直接跑不起来。我建议Node安装20 LTS版本一个版本通吃Vite 5和大部分前端工具链。再就是npm install装依赖时容易位置报错。国内环境下建议第一时间换镜像源npm config set registry https://registry.npmmirror.com另外vue-play-m3u8这类标签搜索量很高我顺手说一句如果项目需要播放车载监控视频尤其m3u8格式使用hls.js这个库就可以了配置一个video标签的source不用装任何重型播放器组件。3. 数据库建模车辆、申请、维修这几张核心表怎么设计前端和后端代码可以慢慢调但建表必须一次到位。表结构设计得不好后面写Mapper都是痛苦的。我直接拿车辆管理系统里最常用的几张表出来讲你可以直接复制去用。3.1 车辆信息表一张表把车辆档案装完车辆信息表是整个系统的地基。除了基础档案字段关键在于status字段的设计。我把状态定成了tinyint类型0表示空闲、1表示出车中、2表示维修中这样在代码里用一个枚举就能管理查询也快。CREATE TABLE vehicle_info ( id bigint(20) NOT NULL AUTO_INCREMENT, plate_number varchar(20) NOT NULL COMMENT 车牌号, brand varchar(50) DEFAULT NULL COMMENT 品牌, model varchar(50) DEFAULT NULL COMMENT 车型, vehicle_type varchar(20) DEFAULT NULL COMMENT 车辆类型轿车/商务/货车, buy_date date DEFAULT NULL COMMENT 购置日期, mileage decimal(10,2) DEFAULT 0.00 COMMENT 当前总里程公里, driver_id bigint(20) DEFAULT NULL COMMENT 绑定司机ID, status tinyint(4) DEFAULT 0 COMMENT 状态0空闲 1出车中 2维修中 3停用, insurance_expire_date date DEFAULT NULL COMMENT 保险到期日, annual_check_expire_date date DEFAULT NULL COMMENT 年检到期日, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_plate_number (plate_number) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车辆信息表;注意几个细节plate_number加上唯一索引防止重复录入同一个牌照这是历史遗留数据里最常见的问题。保险到期和年检到期单独拎出来是为了后面写到期提醒的定时任务。用一条SQL就能把30天内到期的车辆捞出来WHERE insurance_expire_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY)。mileage用decimal(10,2)别用float浮点类型在里程累加时会出现精度误差逼死强迫症。3.2 用车申请单与派车记录表状态字段决定整个审批流转申请单是核心业务表。我的设计思路是把申请和派车出车揉在一张表里用status字段串起完整生命周期0待审批、1已通过待派车、2已派车(出车中)、3已完成、4已驳回、5已取消。CREATE TABLE vehicle_apply ( id bigint(20) NOT NULL AUTO_INCREMENT, apply_no varchar(32) DEFAULT NULL COMMENT 申请单号, applicant_id bigint(20) NOT NULL COMMENT 申请人用户ID, applicant_name varchar(50) NOT NULL COMMENT 申请人姓名, dept_name varchar(100) DEFAULT NULL COMMENT 所属部门, use_type varchar(20) DEFAULT NULL COMMENT 用车类型公务/接待/其他, start_time datetime NOT NULL COMMENT 预计用车开始时间, end_time datetime NOT NULL COMMENT 预计用车结束时间, destination varchar(200) DEFAULT NULL COMMENT 目的地, reason varchar(500) DEFAULT NULL COMMENT 用车事由, vehicle_id bigint(20) DEFAULT NULL COMMENT 审批通过后分配的车辆ID, driver_id bigint(20) DEFAULT NULL COMMENT 分配的司机ID, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0待审批 1已通过 2已派车 3已完成 4已驳回 5已取消, approver_id bigint(20) DEFAULT NULL COMMENT 审批人ID, approve_remark varchar(255) DEFAULT NULL COMMENT 审批意见, actual_start_mileage decimal(10,2) DEFAULT NULL COMMENT 出车里程, actual_end_mileage decimal(10,2) DEFAULT NULL COMMENT 归还里程, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用车申请表;设计时有一个细节我要特别说清楚申请阶段不该先绑定车辆。很多初学者会把vehicle_id放在提交申请的环节但真实业务里应该是提交申请时不选具体车辆等审批通过后由调度员来指定车辆和司机。这个先后顺序一旦搞反了就会出现车被占了但申请没通过的脏数据。出车归还的时候要记录actual_start_mileage和actual_end_mileage归还时用end - start算出本次实际行驶里程顺手把车辆表的mileage更新掉这个操作后面在Service层里做事务控制两步必须同时成功。3.3 维修保养表、加油记录表数据量大了之后的分表思路维修保养和加油记录都是高增长的流水数据。单表设计其实不难CREATE TABLE vehicle_maintenance ( id bigint(20) NOT NULL AUTO_INCREMENT, vehicle_id bigint(20) NOT NULL, type tinyint(4) DEFAULT 0 COMMENT 类型0维修 1保养 2违章处理, cost decimal(10,2) DEFAULT 0.00 COMMENT 费用, maintenance_date date DEFAULT NULL, description varchar(500) DEFAULT NULL COMMENT 内容描述, operator_id bigint(20) DEFAULT NULL COMMENT 操作人, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT维修保养记录表;这里想提一个往后走的优化思路当一张表超过几百万行时单纯靠索引也不一定撑得住到了这一步可以按年份做分表比如vehicle_maintenance_2025、vehicle_maintenance_2026查询时在Mapper里根据当前年份动态拼表名。这样做不复杂但很多人等到表大了才追悔莫及提前说一句。4. 后端落地SpringBootMyBatis的登录、权限和车辆模块代码数据库设计好接下来就是敲后端代码。这一节我会把登录鉴权、车辆列表查询和状态流转这几个核心部分讲透直接给可以跑的代码思路。4.1 项目工程结构一个整洁三人组的划分方式我建工程时习惯按controller / service / mapper三层来切顶多加一个config包放配置类、一个entity包放实体。2025年了还是建议不要花大力气去整DDD那种大而全的分层中小型管理系统用简洁三层最实际com.example.vehicle ├── controller # 接收HTTP请求参数校验 ├── service # 业务逻辑事务控制都在这里 │ └── impl ├── mapper # MyBatis接口 ├── entity # 数据库实体类 ├── common # 统一返回结果、异常处理、工具类 │ ├── Result │ ├── ResultCode │ └── BusinessException ├── config # 拦截器配置、跨域配置等 └── VehicleApplication.javacommon/Result这个统一返回体值得说两句。前后端分离的项目里后端一定不要直接返回裸数据或裸字符串统一包装成{ code: 200, message: success, data: ... }这种结构。前端axios拦截器里直接判断code出错弹提示代码干净太多。4.2 MyBatis的编写方式注解和XML怎么选在SpringBoot 3.x环境里用MyBatis我采用的是XML文件方式因为车辆管理系统涉及联表查询和动态SQLXML里写where标签处理多条件查询比注解拼字符串舒服得多。核心配置就三样spring: datasource: url: jdbc:mysql://localhost:3306/vehicle_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4allowPublicKeyRetrievaltrue username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.vehicle.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里有两个重要的经验map-underscore-to-camel-case: true加上之后数据库的plate_number字段就能自动映射到实体类的plateNumber不用每个字段都写resultMap。但如果实体类里有多余的字段比如申请表里的vehiclePlateNumber就必须单独写resultMap否则会映射不上。log-impl配成StdOutImpl开发时所有SQL都会打印到控制台排查拼接错误效率极高。上线前记得关掉不然日志会非常吵。再看一个车辆列表的条件查询XML写法。这里要支持按车牌号模糊搜、按状态精确搜、按车辆类型搜并且要关联司机姓名select idselectVehiclePage resultTypecom.example.vehicle.entity.VehicleVO SELECT v.*, d.real_name AS driverName FROM vehicle_info v LEFT JOIN user d ON v.driver_id d.id where if testplateNumber ! null and plateNumber ! AND v.plate_number LIKE CONCAT(%, #{plateNumber}, %) /if if teststatus ! null AND v.status #{status} /if if testvehicleType ! null and vehicleType ! AND v.vehicle_type #{vehicleType} /if /where ORDER BY v.create_time DESC /select使用where标签的好处是当所有条件为空时它不会生成多余的WHERE关键字当第一个条件成立时它会自动把拼接出来的AND去掉前导。这个标签能治很多新手条件拼接多出一个WHERE的毛病。这里的LEFT JOIN用的是LEFT而不是INNER是因为有些车辆可能暂时没有绑定司机但车辆信息仍然要显示出来。新手很容易在这种关联查询里选错join类型我帮你们提前踩过了。4.3 登录鉴权用JWT还是用Session车辆管理系统既然是前后端分离我推荐直接用JWT不用在服务端存Session。流程很简单登录接口校验用户名密码和验证码。校验通过后用jjwt库生成一个token把userId、用户名、角色塞进claims里。前端把token存到localStorage每次请求在axios拦截器里放到Authorization请求头。后端写一个拦截器从请求头里取token并解析解析通过就把userId放到ThreadLocal里供业务代码使用。关键依赖用jjwt 0.11.5这个版本比较稳dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency生成token的核心代码大概是这个意思SecretKey key Keys.hmacShaKeyFor(secret.getBytes()); String token Jwts.builder() .setSubject(userId.toString()) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 2)) .signWith(key, SignatureAlgorithm.HS256) .compact();有一个细节值得注意secret的长度必须不少于32个字节如果用了一个很短的字符串当密钥运行时会直接抛Key length not sufficient的异常。我一开始就被这个坑过以为是jjwt的用法问题最后发现就是密钥太短。角色权限控制在拦截器里做。比如/api/apply/**这个路径需要登录用户都能访问但/api/vehicle/**的删除接口只有管理员能访问。可以在拦截器的preHandle里解析出role再判断权限放行或返回403。这种方式对项目来说够用了没必要上Spring Security整套。4.4 派车状态流转一整个事务要怎么控制派车这个操作是车辆管理系统里最容易出错的地方。完整的派车动作包含这么几步修改申请单状态从已通过变成已派车修改车辆状态从空闲变成出车中把申请的车辆ID和司机ID回填到申请单。这三步必须在一个事务里。如果第2步成功、第1步失败数据库就会出现申请单还是待派车但车被占用了的脏状态。在Service层处理时直接加Transactional(rollbackFor Exception.class)注解即可。但注意如果事务方法内部catch了异常而没往外抛事务是不会回滚的。所以业务代码里遇到异常我一般推荐直接throw自定义的BusinessException让全局异常处理器去兜底。Transactional(rollbackFor Exception.class) public void dispatchVehicle(Long applyId, Long vehicleId, Long driverId) { VehicleApply apply applyMapper.selectById(applyId); if (apply null || !apply.getStatus().equals(ApplyStatus.APPROVED)) { throw new BusinessException(申请单状态异常无法派车); } VehicleInfo vehicle vehicleMapper.selectById(vehicleId); if (vehicle null || !vehicle.getStatus().equals(VehicleStatus.IDLE)) { throw new BusinessException(该车辆当前不可派发); } apply.setVehicleId(vehicleId); apply.setDriverId(driverId); apply.setStatus(ApplyStatus.DISPATCHED); applyMapper.updateById(apply); vehicle.setStatus(VehicleStatus.ON_TRIP); vehicleMapper.updateById(vehicle); }5. 前端工程Vue3项目初始化、路由守卫和接口对接后端接口设计好了前端部分我按Vue3ViteElement Plus这套组合来讲。作为管理后台类项目前端的核心工作其实就三块搭建工程、封装请求、写页面组件。5.1 用Vite初始化Vue3项目三行命令搞定相比WebpackVite的启动速度简直快得感人。初始化项目的命令npm create vuelatest这个命令会带你走一遍交互式配置选项里可以勾上TypeScript、Vue Router、Pinia等。如果你是纯Java后端想快点上手我建议TypeScript暂时先不勾保留JavaScript就够了减少一层学习成本。然后安装Element Plus和axiosnpm install element-plus npm install axiosmain.js里做全局注册import { createApp } from vue import App from ./App.vue import router from ./router import ElementPlus from element-plus import element-plus/dist/index.css const app createApp(App) app.use(router) app.use(ElementPlus) app.mount(#app)5.2 路由和权限守卫没有登录就给我弹回登录页管理系统的前端必须有路由守卫。没登录的情况下用户直接访问/dashboard这种页面时必须被重定向到登录页这个逻辑用Vue Router的beforeEach来做router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })菜单栏里动态渲染的页面一般有两种组织方式一种是直接把菜单配置写死在侧边栏组件里另一种是从后端接口动态获取菜单权限。对于车辆管理系统这种业务固定、角色少的项目写死菜单完全够用没必要为动态菜单徒增复杂度。5.3 axios封装请求头、响应拦截器、错误提示一次搞定axios的封装我每次项目都要写一遍已经把统一模式固定下来了import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } ) export default request注意baseURL我配成了/api而不是完整地址这样就需要在Vite的配置里做代理转发避免开发时跨域问题// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这一步非常关键。很多人在前端直接写axios.post(http://localhost:8080/api/login)运行时会发现浏览器报跨域如果后端没配CORS就会卡很久。配代理就解决了而且上线后nginx里再做一层同样的转发整个链路就通了。5.4 车辆列表页面表格、弹窗表单、状态Tag三件套一个车辆管理页面基本由三个部分拼成搜索区、表格区、弹窗表单区。表格里的状态字段用Element Plus的el-tag动态变色看起来既直观又专业el-table-column label状态 width100 template #default{ row } el-tag :typerow.status 0 ? success : (row.status 1 ? warning : danger) {{ row.status 0 ? 空闲 : (row.status 1 ? 出车中 : 维修中) }} /el-tag /template /el-table-column新增编辑弹窗里要放一个表单字段包含车牌号、品牌、车型、车辆类型、购置日期。记住表单校验这里一定要加必填项校验不然用户提交空白表单后端还得做一遍校验体验很糟糕。前端组件划分建议是表格和弹窗不要硬塞在一个组件里拆成VehicleList.vue和VehicleFormDialog.vue两个组件列表组件引表单组件通过v-model控制弹窗显示隐藏提交成功后父组件调用列表查询接口刷新。这种拆分是组件化最基本的样子看代码的人也舒服。5.5 申请审批页面的交互细节时间重叠校验是个隐藏难点申请页面需要用户选择用车时间段然后提交一个在此时间段内没有冲突车辆的校验。后端查询接口可以这样设计传入开始时间和结束时间查询所有状态为已通过、已派车且时间段有重叠的申请单把这些申请单关联的车辆ID排除掉剩下的就是可用车辆SELECT * FROM vehicle_info WHERE status 0 AND id NOT IN ( SELECT vehicle_id FROM vehicle_apply WHERE status IN (1, 2) AND start_time lt; #{endTime} AND end_time gt; #{startTime} )这里的时间重叠判断条件是核心两条记录冲突的条件是A.startTime B.endTime AND A.endTime B.startTime这个条件反直觉但数学上是完全正确的。我见过不少新手写startTime B.startTime AND endTime B.endTime去判结果边界情况漏了一堆。这个公式可以直接抄凡是做预约类功能都适用。6. 本地跑通与部署上线启动步骤和踩坑排查清单代码写完整不代表项目跑得起来。最后这部分我把从零到能访问页面的完整流程和高频问题排查串一遍这些内容看着琐碎但实际折腾人。6.1 从拉代码到浏览器看到登录页的完整路径本地启动MySQL服务执行vehicle_db.sql脚本创建数据库和5张表注意确认字符集是utf8mb4不然存中文会乱码。用IDEA打开后端工程等待Maven下载依赖。如果你网络不好Maven的setting.xml里建议加阿里云镜像否则等依赖能等到天荒地老。修改application.yml里的数据库账号密码改成你自己的。确保MySQL端口是3306如果被占用或改过端口连接串里也要同步改。直接运行VehicleApplication.java的main方法看到Started VehicleApplication日志就成功。在vehicle-front目录下执行npm install安装前端依赖然后npm run dev启动Vite开发服务器。浏览器访问http://localhost:3000看到登录页就说明前端正常。用管理员账号登录数据库脚本里预置一条进入系统首页。这串流程看着简单但第2步和第6步确实最容易出问题最大原因就是网络。6.2 我实际踩过的高频报错和对应解法直接把最常碰到的几个问题列成表格方便你排查时快速定位报错现象根本原因解决办法Access denied for user rootlocalhost数据库账号密码写错检查yml配置中的password注意MySQL8默认认证插件问题Unknown database vehicle_db没执行建表SQL先连上MySQL执行CREATE DATABASE vehicle_db CHARACTER SET utf8mb4再导入Invalid bound statement (not found): xxxMapper.selectPageMapper接口和XML没有对应上检查mapper-locations路径是否正确XML里namespace是不是接口全限定名Failed to configure a DataSource没有引入JDBC相关依赖或配置缺失确认pom里有spring-boot-starter-jdbc且yml里有datasource配置Key length not sufficientJWT密钥长度不够把secret字符串加长到至少32字节前端请求404后端接口路径和前端请求路径不一致打开控制台看Network里请求的URL逐字对着后端Controller的RequestMapping检查前端请求跨域报错后端没开CORS或前端没走代理开发环境优先用Vite代理方案不要死磕后端CORSnpm install时报ERESOLVE错误依赖版本冲突用npm install --legacy-peer-deps重试跨域这个问题我再多说一嘴。如果在后端直接加CrossOrigin或配置CORS全局类开发时是方便了但上线后如果前端是部署在nginx、后端是单独服务还是要靠nginx转发所以不如从开始就习惯前端代理生产nginx反代这套方案一劳永逸。6.3 部署到服务器用Maven打包加nginx反代本地跑通后如果要把项目部署到服务器给公司内网用流程也不复杂后端打包在项目根目录执行mvn clean package -DskipTests得到vehicle-admin.jar。把jar包上传到服务器直接java -jar vehicle-admin.jar --spring.profiles.activeprod启动或者用systemd配开机自启。前端构建npm run build生成的dist目录上传到服务器。nginx配置一个server块把/指向dist目录/api转发到后端的8080端口。nginx关键配置片段server { listen 80; server_name your-server-ip; root /usr/share/nginx/html/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里try_files $uri $uri/ /index.html一定要写上。Vue项目用的是前端路由如果用户直接在浏览器输入/dashboard并刷新nginx找不到这个物理文件会直接404加这一行把所有路径都回退到index.html由前端路由接管问题就解决了。这是部署Vue项目最经典的一个坑。还有一个小细节nginx里proxy_pass如果写成http://127.0.0.1:8080后面的/api路径会原样透传后端接口就能正常收到请求如果你在proxy_pass末尾加了/路径会被替换掉接口就直接404。这个差异我第一次部署时也踩了。7. 这套项目后续可以怎么扩展最后说一下扩展方向。车辆管理系统这种基础项目最大的优势在于业务边界清晰你想加什么模块都能顺着现有结构往下长增加数据统计看板在首页显示车辆总数、今日出车次数、维修费用趋势等后端用GROUP BY和日期函数做聚合查询前端用ECharts画图。这一步能让你在简历上多写一条数据可视化能力。对接企业微信/钉钉审批用车申请提交后把审批通知推到企业微信群或钉钉工作通知。核心是调官方API拿access_token然后推消息模板。这个扩展听着高级其实就是一个HTTP请求的事。接入GPS车辆定位比较大的改造需要车载终端上报经纬度后端定时拉取轨迹前端用地图比如Mapbox方案画轨迹。这个方向适合想冲击更高薪资的岗位方向。源码本身不是终点能否把它改成符合自己业务场景的定制化系统才决定这个项目的价值上限。我自己的体会是拿到一套源码先把表和状态流转吃透再从头敲一遍核心接口比背十遍面试题都管用。希望这篇文章能让你少走几步弯路。
返回列表