资讯动态

SpringBoot+Vue+MyBatis构建车辆管理系统:设计、实现与部署详解

发布时间:2026/9/14 23:54:51 来源:尧图企业网站定制
做车辆管理系统这套东西其实比想象中有意思得多。先说结论SpringBoot Vue MyBatis MySQL 这个组合到今天依然是中小型后台管理系统里最成熟的方案之一。市面上很多所谓“车辆管理系统源码”要么乱得没法看要么功能砍到只剩增删改查。我这次整理的这个项目从车辆档案、司机管理、调度记录到维修保养、油耗统计都有覆盖前端用 Vue 3 Element Plus后端 SpringBoot 提供 RESTful API数据层走 MyBatis 操作 MySQL整套东西跑起来之后拿来改改就能接真实业务。如果你正准备找一套能学习的完整项目或者想在自己的毕设、企业内训、小团队自用系统里快速落一座可扩展的框架这篇会很有参考价值。我先把话说清楚这套系统的代码不是那种“能跑就行”的玩具工程。它在设计上有几条明确的取舍后端按 controller / service / mapper 分层业务逻辑和 SQL 分开数据库表结构做了必要的三范式拆解加冗余字段查询性能有保证前端封装了统一的 request 工具登录态用 token 管理分页、搜索、下拉联动这些后台系统的常见交互都是完整实现的。下面我会从整体设计讲到数据库表结构再到前后端具体实现最后把我在实际联调和部署时踩过的坑一并列出来按项目复现的路线走一遍你能少走不少弯路。1. 项目概览一个车辆管理系统到底要管什么1.1 功能模块拆解车辆管理系统这个名字看着简单真正要落地的功能拆开其实能分成六大块车辆档案管理登记车辆的基础信息车牌号、品牌型号、车辆类型、座位数、发动机号、车架号、购买日期、车辆状态可用/维修中/已报废等。这里最容易被忽略的是车辆状态的管理车辆一多哪些能调度、哪些在维修、哪些已经到期该年检系统都得有明确的标识和提醒入口。司机信息管理司机姓名、手机号、驾照类型、驾照有效期、入职时间、状态在职/离职/停职。这块要注意驾照有效期系统里面通常会加一个到期提醒的逻辑没到期的列表置灰或者标红。调度管理这是车辆管理系统的业务核心。申请用车、审批、派车、归还、里程登记形成一个完整的闭环。调度记录表里要关联车辆ID、司机ID、使用人、起始时间、结束时间、起始里程、结束里程、用途说明。维修保养管理记录每次保养或维修的时间、类型保养/小修/大修、费用、维修厂、经办人以及车辆当前的保养状态。这里有个细节维修中的车辆在调度模块选车时应该禁用不能又派出去干活。油耗与费用管理记录每次加油的数量、金额、单价、当前里程数用于计算百公里油耗和车辆运营成本。统计报表月度用车统计、车辆使用率、司机出车排行、维修费用比等。报表不需要太复杂前端用 ECharts 画柱状图和饼图就够用。1.2 技术栈选型背后的考量这套系统选择 SpringBoot Vue不是因为它们“最新最火”而是因为它们在实际工程里的生态足够完善。后端用 SpringBoot 的原因很简单约定大于配置起步快。SpringBoot 内置 Tomcat打包出来一个 jar 直接 java -jar 就能跑。它对 MyBatis 的整合也做得很好mybatis-spring-boot-starter 一引入就自动配置了 SqlSessionFactory不用像 SSM 时代那样手动配一堆 bean。而且 SpringBoot 的自动装配机制让整个人力成本摊得很薄写业务逻辑时基本不用关心框架底层。前端选 Vue 3 Element Plus核心原因是 Vue 的响应式数据绑定让表格、表单这类后台页面写起来特别顺手。Element Plus 是 Element UI 的 Vue 3 版本表格组件、表单校验、对话框、消息提示这些后台管理系统的标配组件开箱即用。组件库本身又是按需引入的打包体积可控。MyBatis 最大的优势是 SQL 控制力。车辆管理系统里有不少多表关联查询比如调度记录要关联车辆、司机两张表统计报表要按月份分组聚合用 MyBatis 写 XML 里的 SQL 可以精确控制每一条语句复杂查询时比 Hibernate / JPA 更直接、更好调优。MySQL 则是不用多说的标准答案轻量、免费、社区活跃8.0 版本对 JSON 类型和窗口函数的支持也让查询设计更灵活。2. 数据库设计与建模先定好地基再盖楼2.1 MySQL 初始化与字符集配置我先单独把 MySQL 的初始配置拎出来讲因为很多人在这里踩坑。MySQL 8.0 安装完后默认的字符集是 utf8mb4这个一般不用改。但连接时如果没有显式指定编码和时区往往会出现中文乱码或者 Server returns invalid timezone 的报错。连接串建议这样写jdbc:mysql://localhost:3306/vehicle_manager?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue注意两个容易被忽视的参数allowPublicKeyRetrievaltrue是 MySQL 8.0 使用 caching_sha2_password 认证时需要的否则连接会报 Public Key Retrieval is not allowedserverTimezoneAsia/Shanghai是解决时间字段差 8 小时问题的关键。如果你用 Navicat 或者 DataGrip 建库建议用下面的语句把库和账号一起建好CREATE DATABASE IF NOT EXISTS vehicle_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER vehicle% IDENTIFIED BY vehicle123; GRANT ALL PRIVILEGES ON vehicle_manager.* TO vehicle%; FLUSH PRIVILEGES;2.2 核心表结构与字段设计数据库我设计了 6 张核心表vehicle_info车辆信息表、driver_info司机表、dispatch_record调度记录表、maintain_record维修保养表、fuel_record加油记录表、sys_user系统用户表。下面是重点表的核心字段设计。vehicle_info 表字段名类型说明idbigint主键自增plate_novarchar(20)车牌号唯一索引brand_modelvarchar(50)品牌型号vehicle_typetinyint1-轿车 2-SUV 3-商务 4-货车engine_novarchar(50)发动机号frame_novarchar(50)车架号purchase_datedate购买日期statustinyint1-可用 2-维修中 3-已报废create_timedatetime创建时间这里要注意status字段使用 tinyint 而不是 varchar。一方面数据库存储更省空间另一方面前端 ele-tag 组件可以根据数字状态映射成不同颜色的标签显示“可用 / 维修中 / 已报废”就是一行 map 的事。千万别把状态存成中文后期要改状态名或者做多语言就痛苦了。dispatch_record 表加了几个比较关键的字段driver_id、vehicle_id、use_date用车日期、start_mileage起始里程、end_mileage结束里程、purpose用途、status0-待审批 1-已审批 2-已拒绝 3-已归还。这里的设计意图很明确一张表跑完整个调度的生命周期。审批通过后状态改 1归还车辆时填结束里程再改 3所有流程都有记录可查。2.3 索引设计与 SQL 性能兜底车辆管理系统的数据量级通常到不了大数据量但查询场景很固定所以索引策略相对简单。我建了三个核心索引ALTER TABLE dispatch_record ADD INDEX idx_vehicle_date (vehicle_id, use_date); ALTER TABLE dispatch_record ADD INDEX idx_driver_date (driver_id, use_date); ALTER TABLE fuel_record ADD INDEX idx_vehicle_time (vehicle_id, create_time);联合索引的设计逻辑是车辆 日期、司机 日期这两个维度的查询在系统里出现频率最高。例如统计某辆车这个月出了多少次车走 idx_vehicle_date 索引直接覆盖利用SQL 不需要回表。还有一点在给 MySQL 加索引时注意区分哪些是等值查询、哪些是范围查询把等值字段放联合索引前面这是最基础的索引优化规则。3. 后端落地SpringBoot MyBatis 的正确打开方式3.1 工程结构规划与基础配置我习惯按功能模块分包而不是按技术分层分包。特别是在车辆管理系统这种业务边界清晰的场景里模块化分包能让你快速定位某块功能的全程链路。项目结构大致是这样com.example.vehicle ├── config # 配置类CORS、MyBatis、拦截器 ├── controller # 接口层 ├── service # 业务逻辑层 ├── mapper # MyBatis Mapper接口 ├── entity # 实体类 ├── dto # 数据传输对象 ├── vo # 视图对象给前端返回的封装结构 ├── common # 通用类统一返回结果、全局异常处理 └── utils # 工具类JWT、日期等SpringBoot 版本这边我用的是 2.7.x而不是 3.x。选 2.7.x 是综合考量的结果SpringBoot 3.x 要求 JDK 17且 javax.servlet 包名改成了 jakarta.servlet很多老的第三方依赖和网上的示例代码不再兼容。如果你是在校生或者要参考网上的大量历史资料SpringBoot 2.7.x JDK 8/11 的组合是最稳的。当然如果是全新项目且团队已经用上 JDK 17SpringBoot 3.x 也完全没问题只是遇到兼容问题时要有心理准备。3.2 统一返回结果与全局异常处理前后端分离的项目后端接口一定要有一个统一的返回结构否则前端每个接口都要适配不同的返回值。我这里定义了一个 Result 类public class ResultT { private Integer code; // 200 成功500 失败 private String message; // 提示信息 private T data; // 返回数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }全局异常处理用 RestControllerAdvice 实现。这里有个容易被忽略的点数据校验异常和业务异常要分开处理。业务异常如“该车辆正在维修中无法调度”用自定义的 BusinessException 抛出在全局异常处理器里返回 code500 和业务提示信息参数校验异常如 NotBlank 触发的 MethodArgumentNotValidException则返回 code400。这样前端可以根据 code 判断是提示错误信息还是重新校验表单体验差很多。3.3 MyBatis 的映射文件与缓存机制MyBatis 的 Mapper XML 我分成了两类写单表操作尽量用注解或者 MyBatis-Plus 的 Wrapper复杂多表关联和统计报表放在 XML 里写。举个例子调度记录的分页列表要关联车辆表、司机表查询结果是多张表拼出来的用 XML 的 resultMap 做映射最清晰。select idselectDispatchPage resultMapDispatchVO SELECT d.id, d.use_date, d.start_mileage, d.end_mileage, d.purpose, d.status, v.plate_no, v.brand_model, dr.name AS driver_name FROM dispatch_record d LEFT JOIN vehicle_info v ON d.vehicle_id v.id LEFT JOIN driver_info dr ON d.driver_id dr.id where if testeq.vehicleId ! null AND d.vehicle_id #{eq.vehicleId} /if if testeq.status ! null AND d.status #{eq.status} /if if testeq.startDate ! null and eq.startDate ! AND d.use_date gt; #{eq.startDate} /if if testeq.endDate ! null and eq.endDate ! AND d.use_date lt; #{eq.endDate} /if /where ORDER BY d.create_time DESC /selectMyBatis 的缓存机制也是经常在面试里被问到的点。一级缓存是 SqlSession 级别的默认开启同一个 SqlSession 里执行两次相同的查询会命中缓存但 Spring 整合 MyBatis 后每次请求通常都是新建 SqlSession所以一级缓存的作用范围很有限。二级缓存是 Mapper 级别的默认关闭要开启需要在 XML 里加cache/标签。我对这个项目的建议是默认不要开启二级缓存。车辆管理系统的数据实时性要求高调度状态一变就要立刻生效缓存反而容易造成数据不一致。等真有高并发查询同一个表的场景再针对性地做 Redis 缓存比用 MyBatis 的二级缓存灵活得多。3.4 批量操作与接口性能优化车辆管理系统里最容易遇到性能问题的场景是一次性导入车辆或司机数据这时候千万别在 Java 里 for 循环调用单条 insert要改用 MyBatis 的批量插入。insert idbatchInsert parameterTypelist INSERT INTO vehicle_info (plate_no, brand_model, vehicle_type, status) VALUES foreach collectionlist itemitem separator, (#{item.plateNo}, #{item.brandModel}, #{item.vehicleType}, #{item.status}) /foreach /insert但批量插入这里有一个不算少见的问题如果你用的是 ExecutorType.SIMPLE默认values 拼接成一个巨大的 SQL如果一次性插 5000 条SQL 长度会超过 MySQL max_allowed_packet 的限制。解决办法有两个一个是每 500 条拆一次批执行 commit 后再继续另一个是在 MyBatis 配置里改批量执行器但改了之后要注意它的缓存刷新策略和普通模式不一样容易在某些操作上报 Cache is not enabled 的错。最稳妥的做法其实是控制 batchSize1000 条一批配合事务一起提交既保证性能也避免单一 SQL 过大。3.5 登录鉴权与 JWT 落地后台管理系统登录鉴权我选的是 JWT 拦截器的方式不引入 Spring Security因为一个内部管理系统的权限需求没那么重引入安全框架反而把配置复杂度抬得很高。JWT 的实现流程是登录成功后后端用用户的 ID 和用户名生成 token 返回给前端前端把 token 存到 localStorage每次请求在 header 里带上 Authorization: Bearer 后端拦截器解析 token 并校验有效性。这里有一个细节值得注意JWT 本身不依赖服务端保存会话所以无法主动让某个 token 失效如果用户修改了密码或者被管理员禁用原来的 token 依然可以用到过期。我在这个项目里的做法是给 sys_user 表加了一个 token_version 字段用户密码被修改时 token_version 1生成 token 时把 token_version 也签发进去拦截器校验时比对当前 token_version不一致就判定为无效。这个方案既轻量又能解决 JWT 无法强制失效的问题非常实用。4. 前端实现Vue 从 0 到能跑的完整链路4.1 创建项目与依赖安装前端我用 Vite 创建 Vue 3 项目Vite 比 Vue CLI 快太多了开发模式下启动基本秒开。创建命令npm create vitelatest vehicle-web -- --template vue cd vehicle-web npm install npm install vue-router4 pinia element-plus axios echarts说说为什么前端依赖选这些vue-router 4 是 Vue 3 对应的路由版本pinia 是 Vue 3 官方推荐的 state 管理库axios 是 HTTP 请求库echarts 是图表库统计报表页面要用。Element Plus 安装后最好按需引入不要全量引入按需引入能砍掉至少 30% 的打包体积。如果你用的是 Vite 插件方式在 vite.config.js 里配置 unplugin-vue-components 和 unplugin-auto-import 即可。4.2 路由设计、状态管理与接口封装前端路由我按模块拆分登录页是独立的登录成功后进入主布局。主布局里是侧边栏菜单 内容区域的结构const routes [ { path: /login, component: Login }, { path: /, component: Layout, redirect: /dashboard, children: [ { path: dashboard, component: Dashboard }, { path: vehicle, component: VehicleList }, { path: dispatch, component: DispatchList }, { path: maintain, component: MaintainList }, { path: fuel, component: FuelList }, ] } ]路由守卫是必须的否则未登录也能直接访问页面router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path ! /login !token) { next(/login); } else { next(); } });前端接口封装是最容易被人忽视的部分。很多人图省事直接在每个页面里写 axios.get结果后端改了接口路径前端十几个页面挨个手动改非常痛苦。我封装了一个统一的 request.js主要做了三件事统一请求前缀、请求拦截器里自动带上 token、响应拦截器统一处理 code 码和错误提示。service.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?.status 401) { localStorage.removeItem(token); router.push(/login); } ElMessage.error(网络请求异常); return Promise.reject(error); } );这样处理之后页面里调用接口时只需要关心返回的数据错误提示和登录跳转都被统一接管了。页面代码能瘦一大圈。4.3 核心页面实现要点列表页的原型非常重要。车辆管理的列表页长这样顶部是搜索栏车牌号、状态、购买日期范围中间是工具栏新增、导出下面是一个 el-table 显示车辆数据分页用 el-pagination。核心的搜索逻辑是搜索条件变化时查询参数重置到第 1 页再调用接口这个细节不做好会出现“你在第 5 页点了搜索结果数据变成了第 1 页的内容但分页器还停在 5”。表单页这边新增和编辑共用一个弹窗组件。我用v-ifisEdit控制表单标题打开弹窗时如果是编辑就调用后端接口填充数据。这里要注意回显字段的格式比如时间字段后端返回的是2025-01-05 12:00:00el-date-picker 需要的是 Date 对象或者2025-01-05这种格式直接在表单里初始化字段时做一次格式化不要等到提交时才处理否则时间字段会出现回显了但校验不过的诡异问题。4.4 与后端联调的跨域与参数问题前后端分离开发模式联调时最核心的问题就是跨域。我的处理方案很简单后端加一个 CORS 配置类指定允许的来源和请求头。前后端都在本地时Vite 默认跑在 5173 端口SpringBoot 跑在 8080 端口CORS 如果不放开浏览器控制台会直接给你报一堆红。写一个 WebMvcConfigurer 的实现类重写 addCorsMappings 方法即可Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }跨域问题解决之后还有一个参数传递的坑很典型。比如调度记录的列表查询要传 vehicleId、status、startDate 三个查询参数。前端 axios 默认会把对象序列化成 JSON但后端如果用 RequestParam 接收就要求前端用 params 传参而不是 data。项目里我统一约定GET 请求的查询参数全部用 paramsPOST 请求的表单数据用 data。这个约定定死之后前后端联调时就不会出现“参数我传了但你那边收不到”这种反复浪费时间的问案。5. 打包部署与线上排查5.1 前端打包后的路径与 404 问题前端开发完成之后npm run build 会生成 dist 目录里面是静态文件。这里非常多的人会遇到一个问题把 dist 里的文件部署到服务器上打开首页一片空白或者资源加载 404。这个问题的根源在于 Vite 的 base 配置。如果你的系统部署在域名根路径比如 https://example.com那默认的 base: / 就能正常工作但如果部署在子路径比如 https://example.com/vehicle/就必须把 base 配置成 /vehicle/否则页面引用的 JS/CSS 路径全部找不到资源。处理完资源路径接着会遇到 vue-router 的 history 模式问题。如果你用了 history 模式对我建议用 history 而不是 hash因为路由地址好看而且没有 # 号在 Nginx 服务器上需要配置 try_files 把所有路径都重定向到 index.html否则你在页面上点刷新或者直接访问某个子路由Nginx 找不到对应的文件就会返回 404。Nginx 的配置大概是这样location / { try_files $uri $uri/ /index.html; }5.2 SpringBoot 生产环境配置后端打包部署我分成两步。第一步把 SpringBoot 项目打成 jar 包mvn clean package -DskipTests第二步用 java -jar 运行java -jar vehicle-system.jar --spring.profiles.activeprod为了让开发环境和生产环境的配置分离我在 resources 目录下建了 application.yaml公共配置、application-dev.yaml开发环境、application-prod.yaml生产环境。生产环境的数据库地址、账号密码、日志级别都放在 prod 配置里打包时用 --spring.profiles.activeprod 指定环境。千万别把生产数据库密码写在 application.yaml 里然后提交到 Git这是很多人犯过的低级错误脱了裤子挨打的事。生产环境我还额外配了一个启动脚本用 nohup 后台运行并输出日志到文件方便排查问题nohup java -jar vehicle-system.jar --spring.profiles.activeprod vehicle.log 21 5.3 后端打包后前端资源如何整合这个项目里有个可选做法把前端 build 出来的 dist 目录里的静态文件直接复制到 SpringBoot 的 src/main/resources/static 目录下然后把前端的历史模式路由改成 hash 模式。这样整套系统最终只需要跑一个 SpringBoot 服务不需要单独部署 Nginx。适合那种“不想维护前后端两个服务”的场景。不过我的建议是前后端还是分开部署更规范。毕竟前端静态文件走 Nginx 有缓存控制、gzip 压缩等天然优势后端 jar 包只管 API职责清晰。之前做过一个项目贪图省事把前端塞进 jar 包里结果前端要发版时还得重新打包整个后端服务操作烦琐不说一旦后端部署挂了前端也跟着挂了。这个坑我踩过一次就长记性了。6. 实际项目中踩过的坑与排查速查表6.1 SpringBoot 版本太高的兼容性问题标题热搜词里有“SpringBoot版本太高”这个痛点确实存在。SpringBoot 3.x 推出后不少人新建项目直接用了最新版结果发现很多旧项目的代码跑不起来。除了前面提到的 javax 变 jakarta还有更隐蔽的问题MyBatis 和老版本的分页插件 PageHelper 在 SpringBoot 3.x 下需要更新到专门适配的版本PageHelper 需要 6.0.0连接 MySQL 的驱动也从 mysql-connector-java 变更为 com.mysql:mysql-connector-j包名由 com.mysql.cj.jdbc.Driver 变为 com.mysql.cj.jdbc.Driver但坐标变了。如果你在搭建项目时遇到诡异报错先看下 SpringBoot 版本、JDK 版本和依赖的版本号是否匹配这是排查这类问题最快的方式。6.2 MyBatis 中数字字符比较的隐患热搜词里有一条“mybatis 单个数字字符比较”这个问题很刁钻但确实容易踩中。假设你要判断一个 status 字段是否等于数字 1如果在 XML 里写if teststatus ! null and status ! 然后传入的 status 是前端传过来的数字字符串 1这时 MyBatis 的 OGNL 表达式里status ! 会对字符串和空字符串做判断但问题是1和空字符串比较时会被转成字符比较逻辑结果可能和你想象的不一样。更经典的问题是if teststatus ! 当 status 是单个字符时OGNL 会把字符和字符串的比较按 ASCII 码解释导致条件判断失效。最稳妥的写法是显式判断不要吝啬写全条件if teststatus ! null and status.toString() ! AND status #{status} /if或者直接在 Java 代码里把查询参数的类型定死。使用 Integer 类型接收 status就完全没有这个烦恼。所以你能看到很多经验丰富的老程序员的 Mapper 参数都写得很细这不是啰嗦是用教训换来的严谨。6.3 MySQL 创建索引与慢查询排查如果你在实际运行中发现某个页面加载特别慢先别急着改代码先用 EXPLAIN 看看 SQL 的执行计划。我见过太多人一上来就给 SQL 加索引但加的位置不对加了等于白加。EXPLAIN 里重点看 type 字段如果看到 ALL说明是全表扫描这时候才需要优化索引。EXPLAIN SELECT * FROM dispatch_record WHERE vehicle_id 1 AND use_date BETWEEN 2025-01-01 AND 2025-01-31;type 从好到差的顺序大概是这样system const eq_ref ref range index ALL。平时查询只要能达到 range 或 ref 级别就算合格了。另外还有一个很实用的排查方法在 MyBatis 的配置文件里打开 SQL 日志输出可以看到每一条 SQL 实际执行了什么mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl联调时打开它线上环境关掉。这样你就能在控制台直接看到传参和完整 SQL排查问题效率翻倍。6.4 Vue 打包后布局异常不少人在前端本地开发时页面正常打包部署到服务器之后布局乱了最典型的现象是字体图标显示为方框、样式错乱。这类问题绝大多数是路径导致的Element Plus 的字体文件默认是从相对路径加载的如果你部署到子路径但没有正确配置 Vite 的 base字体文件 404图标自然就变成了方框。还有一个可能是 CSS 的 scoped 属性在某些情况下打包后顺序错乱导致样式互相覆盖这个问题在 Element Plus 和自定义样式混用的时候偶尔出现。排查时先用浏览器开发者工具看 Console 面板的 404 报错锁定是不是资源路径问题再检查样式覆盖问题。6.5 常见问题速查表现象根本原因快速解决接口报 Public Key Retrieval is not allowedMySQL 8.0 认证机制连接串加 allowPublicKeyRetrievaltrue时间字段差 8 小时未指定时区或默认时区是 UTC连接串加 serverTimezoneAsia/ShanghaiSpringBoot 3.x 启动报 javax 不存在JDK 17 Jakarta API用 SpringBoot 2.7.x 或全局替换 jakarta 包名MyBatis 批量插入失败或超慢循环单条插入 / SQL 过大foreach 批量插入 每 500~1000 条提交一次Vue 打包后图标显示方框字体文件路径错误配置正确的 Vite base 路径刷新子路由出现 404Nginx 不支持 history 路由location 加 try_files $uri $uri/ /index.htmlMyBatis 数字字符比较结果不对OGNL 表达式自动类型转换用 Integer 类型参数或 toString() 显式判断数据库查询慢但不知道是哪个 SQL未开启 SQL 日志MyBatis log-impl 设为 StdOutImpl这套系统从数据库建表到前后端联调整个流程走下来基本上就把主流 Java Web 项目的开发套路串明白了。说句实话车辆管理系统本身并不复杂真正有价值的是你在实现每个模块时对细节的处理方式比如状态字段怎么设计、批量操作怎么做、跨域和鉴权怎么落地。明白了这些以后再做类似的订单系统、仓库系统、办公系统无非是换一套业务字段而已架构和思路是通用的。最后补一句我在部署这套系统时的一点体会别把代码写得“看起来高级”当成目标把业务流程梳理顺、把接口定义好、把异常处理干净这套系统的价值就已经超过市面上大部分教学项目了。如果你要把它二次改造成别的管理系统优先改数据库表结构和前端菜单后端架构基本不用动。希望这篇能帮你把这个项目真正跑起来并理解它的每一步。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价