ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue+MyBatis+MySQL车辆管理系统全栈开发实战

SpringBoot+Vue+MyBatis+MySQL车辆管理系统全栈开发实战 做过车辆管理的朋友应该深有体会车辆信息散落在Excel表里、驾驶员和车辆绑定关系模糊、用车记录全凭纸质台账、月底统计里程油耗翻到头晕。这种状态下别说做管理决策连“现在到底有几辆车在外头跑”这种基础问题都答不上来。所以我用SpringBootVueMyBatisMySQL这套前后端分离方案完整做了一套车辆管理系统从数据库设计到接口开发再到前端页面和最终部署全程踩了不少坑也沉淀了不少经验。这篇博文就是把这个项目的完整过程拆开来讲项目源码和部署步骤都会覆盖到适合刚学完SpringBoot和Vue基础、想找个完整项目练手的人也适合已经在做类似系统但想优化架构的开发者。前后端分离这个词听了很多年但真正把项目拆开以后你会发现难点根本不在“前端写页面、后端写接口”这种分工上而在联调、权限、部署、异常处理这些交界地带。这套车辆管理系统本质上就是一套标准的权限CRUD加审批流但正因为它足够“标准”反而能把你平时在碎片化教程里学到的那些知识点全部串起来——JWT认证、动态菜单、MyBatis的多表联查、车辆状态的流转、Excel导入导出每一块都是实打实能写进简历的内容。1. 选型复盘为什么是SpringBootVueMyBatisMySQL这套组合很多新手拿到项目需求第一反应是“用什么框架都行能跑就行”。但等你真正维护过一套半吊子系统就会明白技术选型不是跟风而是为了在开发效率和后期维护之间找平衡点。这套车辆管理系统我确定用SpringBootVueMyBatisMySQL是在明确了几个业务特征之后才做的决定。1.1 业务特征决定了后端框架车辆管理这个场景核心是车辆信息、驾驶员信息、用车申请、保养维修、保险违章这几类实体之间的关联和流转。这类系统的特点是CRUD多、查询条件多、统计报表多但并发量很低事务要求也不算复杂。SpringBoot在这种场景下几乎是统治级的优势——它内置了Tomcat、自动装配机制让配置量降到最低、生态里能直接找到JWT、Excel导入导出、定时任务等各种组件不用重复造轮子。如果用SSHStrutsSpringHibernate那套老架构光配置文件就能写到手软如果非要用微服务那套纯属杀鸡用牛刀光服务拆分、注册发现、配置中心就能拖垮一个两个月交付的项目。1.2 为什么选MyBatis而不是JPA这个选择是我写这套系统时考虑最久的一点。JPAHibernate的优势是自动建表、对象关系映射但它的致命弱点是复杂查询不直观。车辆管理系统里有大量“按车牌、按时间段、按状态、按司机姓名”自由组合的筛选条件还有月度里程统计这种聚合SQL。用JPA写动态查询要么拼接JPQL要么用Specification写一堆 Builder 代码看着都头疼。MyBatis的做法就是把SQL写在XML里用where、if标签做动态条件复杂SQL怎么优化一目了然。团队里有SQL基础的人都能快速上手。再加上车辆表的字段往往比较多用MyBatis可以精确控制查询列避免JPA那种select *附带一堆关联对象的效率问题。唯一要记住的是MyBatis的SQL要自己维护表结构变更时XML也要同步改这属于用可控性换便利性的取舍。1.3 前端锁定Vue以及配套的生态组件前端选Vue理由很简单学习曲线平缓模板语法友好对从后端转前端的开发非常友好。这套系统页面量不大但交互状态不少——车辆列表的筛选、车辆详情里的Tab切换、用车申请的审批流程节点展示Vue的响应式数据绑定机制能把这些状态管理得很清晰。配套组件我是按这套思路选的UI库用Element UI现在推荐Element Plus但如果是Vue 2项目Element UI依然很能打路由用Vue RouterHTTP请求用axios并封装了拦截器处理JWT令牌和全局错误提示。状态管理用了Vuex其实这套系统的全局状态很少只有登录用户信息是真正需要共享的但用Vuex统一管理登录态和权限列表后续扩展会从容很多。表格的展示、表单的校验、日期的选择这些Element UI已经全部覆盖自己需要动手写的业务代码其实并不多。1.4 MySQL的隐藏价值运维简单和SQL生态成熟MySQL在这里是最不需要犹豫的一环。车辆管理系统的数据量撑死到几十万行MySQL在这种量级下性能绰绰有余加上MyBatis天然和MySQL契合导入导出、备份恢复都有成熟工具链。唯一要注意的是字符集建库时一定要指定的utf8mb4这个我在后面踩坑部分会单独说。这套选型组合等于把“框架选型”这个环节的成本降到最低让精力集中在业务逻辑本身。任何项目启动前先用一个小时把技术栈和业务特征匹配一下比上来就撸代码有效得多。2. 从需求到表结构车辆管理系统的数据库是怎么设计的数据库设计是整个系统里最不能赶工的部分。表结构一旦定下来后面改字段的成本会呈指数上升。我画车辆管理系统的数据库时第一个动作不是打开Navicat建表而是把业务里所有名词写下来理清它们之间的关系。2.1 核心实体车辆、驾驶员和用户体系的关系车辆管理系统最核心的实体是“车辆”围绕它展开的是“驾驶员”“用车记录”“保养记录”“保险记录”“违章记录”。这五个实体关系看起来简单但设计上有几个容易踩坑的取舍。车辆和驾驶员是什么关系很多人直觉认为“一辆车配一个固定驾驶员”但实际业务里经常出现“这个司机请假了、别的司机临时出车”的情况所以我在设计中拆出了“驾驶员表”和“派车记录表”车辆的“当前驾驶员”字段只是冗余展示真实关联关系通过派车记录体现。这样能保留出车的完整历史而不是被“当前状态”覆盖掉。再看用户体系。车辆管理系统里有两类人一类是系统使用者管理员、审批人、普通员工一类是业务对象驾驶员。这两类角色我分开建表人员表sys_user管登录和权限驾驶员表driver管资格证、驾龄、状态。别把这两个概念混在一起否则后面做权限控制时会非常痛苦。2.2 建表SQL里的细节字段类型、默认值和索引策略下面这张车辆信息表的设计是整套表结构的样板CREATE TABLE vehicle ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, plate_number varchar(20) NOT NULL COMMENT 车牌号, brand varchar(50) DEFAULT NULL COMMENT 品牌, model varchar(50) DEFAULT NULL COMMENT 车型, color varchar(20) DEFAULT NULL COMMENT 颜色, seat_count int(11) DEFAULT NULL COMMENT 座位数, vin varchar(50) DEFAULT NULL COMMENT 车架号, engine_no varchar(50) DEFAULT NULL COMMENT 发动机号, buy_date date DEFAULT NULL COMMENT 购入日期, mileage decimal(10,2) DEFAULT 0 COMMENT 累计里程公里, status tinyint(4) DEFAULT 0 COMMENT 状态0可用 1出车中 2维修 3停用 4报废, 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), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车辆信息表;这里有几个设计决策值得说明。status字段我用了tinyint而不是枚举类型因为业务状态未来可能新增比如“外借中”“待年检”数字比枚举更好扩展而且MyBatis里映射数字类型比映射枚举更简单。车牌号加了唯一索引这是实体最自然的业务主键防止重复录入。mileage用decimal(10,2)而不是float或double是因为浮点数在累加过程中会有精度问题车辆里程一旦上了两三位数误差会被放大用定点数一劳永逸。还有一个容易忽略的是create_time和update_time两个字段。凡是业务表我都加上这两个字段create_time用DEFAULT CURRENT_TIMESTAMP自动生成update_time用ON UPDATE CURRENT_TIMESTAMP在更新时自动刷新。这样MyBatis的插入和更新SQL里就少操心两个字段排查数据同步问题时这两个时间戳也是最重要的线索。2.3 多表关系如何处理申请记录和审批流的表设计用车申请是车辆管理系统里逻辑最复杂的部分涉及申请、审批、派车、还车多个环节。我用一张主表和一张状态流转表来设计CREATE TABLE vehicle_apply ( id bigint(20) NOT NULL AUTO_INCREMENT, vehicle_id bigint(20) NOT NULL COMMENT 车辆ID, driver_id bigint(20) NOT NULL COMMENT 驾驶员ID, apply_user_id bigint(20) NOT NULL COMMENT 申请人ID, start_time datetime NOT NULL COMMENT 计划出发时间, end_time datetime NOT NULL COMMENT 计划归还时间, start_mileage decimal(10,2) DEFAULT NULL COMMENT 出发里程, end_mileage decimal(10,2) DEFAULT NULL COMMENT 还车里程, purpose varchar(255) DEFAULT NULL COMMENT 用车事由, status tinyint(4) DEFAULT 0 COMMENT 0待审批 1审批通过 2派车中 3已还车 4已拒绝 5已取消, PRIMARY KEY (id), KEY idx_vehicle_id (vehicle_id), KEY idx_apply_user_id (apply_user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用车申请表;审批流我没用单独的流程引擎因为这种简单线性状态机申请→审批→派车→还车用一张表的状态字段就完全能表达引入Activiti那是给自己找麻烦。状态字段从0变到5每一步在代码里校验前置状态即可。多表联查时MyBatis的XML里写联表SQL会比注解更清晰。比如“查询用车记录列表同时显示车牌号和司机姓名”这种需求JOIN一次车辆表和驾驶员表即可select idselectApplyList resultTypemap SELECT va.*, v.plate_number, d.real_name AS driver_name, u.real_name AS apply_user_name FROM vehicle_apply va LEFT JOIN vehicle v ON va.vehicle_id v.id LEFT JOIN driver d ON va.driver_id d.id LEFT JOIN sys_user u ON va.apply_user_id u.id where if testplateNumber ! null and plateNumber ! AND v.plate_number LIKE CONCAT(%, #{plateNumber}, %) /if if teststatus ! null AND va.status #{status} /if /where ORDER BY va.create_time DESC /select用LEFT JOIN而不用INNER JOIN是因为即使某些关联记录被删除申请记录本身也应该保留下来这在数据审计时很重要。分页我建议直接用PageHelper别自己写LIMIT因为手写分页常常忘掉条件里的排序字段和总条数查询PageHelper拦截器能自动拼接count查询保持业务SQL干净。3. 后端实现SpringBootMyBatis的认证、CRUD与统计报表后端部分我只挑三个最关键的点来讲JWT登录认证、车辆CRUD接口怎么写得干净、以及统计报表类SQL怎么用MyBatis落地。这三个点覆盖了后端的核心骨架。3.1 JWT认证的落地方式前后端分离项目里Session已经很难用了——前后端域名不同、请求跨域、移动端没有Cookie机制所以令牌放在请求头里最合适。我采用JWT方案理由很简单无状态、无服务端存储、过期时间好控制。后端实现不复杂我在pom.xml里引入jjwt依赖然后建一个JwtUtil工具类public class JwtUtil { private static final String SECRET_KEY vehicle-manage-secret-key; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000; // 24小时 public static String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); } }拦截器用HandlerInterceptor拦截所有/api/**请求前置处理里从请求头取Authorization: Bearer xxx解析失败直接返回401。我在这里踩过一个坑JWT的SECRET_KEY不能太短HS256算法对密钥长度有要求太短会直接抛WeakKeyException。建议至少用32字节以上的随机字符串。3.2 车辆CRUD的接口设计规范写接口时我给自己定了三条规矩第一URL用资源名词复数比如/api/vehicles第二查询参数统一封装成分页查询对象第三返回结果统一用Result包装类包含code、message、data三个字段。RestController RequestMapping(/api/vehicles) public class VehicleController { Autowired private VehicleService vehicleService; GetMapping public Result pageList(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, VehicleQuery query) { PageInfoVehicle pageInfo vehicleService.pageList(pageNum, pageSize, query); return Result.success(pageInfo); } PostMapping public Result add(RequestBody Valid Vehicle vehicle) { vehicleService.add(vehicle); return Result.success(); } PutMapping(/{id}) public Result update(PathVariable Long id, RequestBody Vehicle vehicle) { vehicle.setId(id); vehicleService.update(vehicle); return Result.success(); } DeleteMapping(/{id}) public Result delete(PathVariable Long id) { vehicleService.delete(id); return Result.success(); } }新增车辆时要做两个校验一是车牌号唯一性这个靠数据库唯一索引兜底但业务层也要在插入前查一次避免把SQL异常抛给前端二是VehicleQuery里封装了plateNumber、status、startDate、endDate等筛选条件给Mapper传这个对象XML里用if判断并拼接条件。这样接口不需要因为查询条件增加而不断改方法签名。还有一个细节是删除车辆。业务上车辆通常不能物理删除因为历史记录里还关联着行驶记录和报销记录所以我用的是逻辑删除——表里加一个deleted字段查询时统一过滤删除操作只是做个UPDATE。3.3 统计报表MyBatis动态SQL和聚合函数的组合拳车辆管理系统最常用的两个统计一是月度出车次数和行驶里程二是车辆维修保养频率。这类统计如果在前端做要拉全量数据到浏览器再聚合数据量大以后页面卡死正确做法是后端的聚合SQL直接算好前端只负责展示。select idselectMonthlyMileage resultTypemap SELECT DATE_FORMAT(start_time, %Y-%m) AS month, COUNT(*) AS apply_count, IFNULL(SUM(end_mileage - start_mileage), 0) AS total_mileage FROM vehicle_apply WHERE status IN (2, 3) -- 派车中和已还车的记录 AND start_time BETWEEN #{startDate} AND #{endDate} GROUP BY month ORDER BY month DESC /selectIFNULL这一手很重要。如果某个月没有任何还车记录SUM返回NULLJava拿到后容易在数值计算时报空指针在SQL层面用IFNULL兜底成0前端渲染就不需要一堆v-if判断了。3.4 MyBatis的小知识TypeHandler的自定义场景默认情况下LocalDateTime在MyBatis里和数据库的datetime字段互转没有问题但如果你接手的是老项目数据库里存的是varchar时间字符串那就要考虑自定义TypeHandler了。我在这个项目里遇到一个场景车辆的“年检时间”在业务上只需要精确到天数据库存的是date但前端传过来的是带时分秒的字符串。我在TypeHandler里做了统一处理插入时截断时分秒查询时只渲染年月日。这块圈子里的网上热词也总提到TypeHandler的工作流程其实它就是一个转换器核心是重写setParameter和getResult两个方法。能用好它能让你的实体类不因为数据库字段格式而被迫写出奇怪的代码。4. 前端实现Vue从登录到业务页面的完整落地后端接口准备好以后前端的工作量其实也不小。Vue项目我用的Vue CLI脚手架创建Axios统一封装Element UI撑起整个后台管理系统。这里按前端业务流走下去最顺。4.1 登录流程与Axios拦截器登录页的表单校验交给Element UI的rules规则提交后调用/api/auth/login接口换取JWT。拿到的token我存在localStorage里同时同步到Vuex的state中以便路由守卫判断登录状态。// 全局Axios实例 const service axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器附带token service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }); // 响应拦截器统一处理错误 service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { Notification.error({ message: res.message }); return Promise.reject(new Error(res.message)); } return res; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } Notification.error({ message: error.response?.data?.message || 请求失败 }); return Promise.reject(error); } );这里有个关键点我把baseURL设成/api开发时通过Vue CLI的devServer.proxy把请求转发到localhost:8080生产时由Nginx统一转发。前端不会直接写后端地址这样打包出来的文件换环境部署不用改代码。4.2 路由与权限控制前端路由采用静态路由动态路由结合的方式。登录页、404页面是静态路由需要登录后才能访问的页面在beforeEach路由守卫中校验登录状态。更进一步菜单的显隐是后端根据用户角色返回的菜单列表前端拿到后动态添加路由。这个做法在若依那类开源框架里很常见我在这里也沿用了。router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next(/login); } else { next(); } });个人体会是如果项目只需要一个“能登录系统的页面”这里的动态权限可以先不做首次开发省很多事。但你的简历或者演示里需要“不同角色看到不同菜单”这种效果那动态路由就是值得展示的点。4.3 车辆列表和表单页的构建细节车辆列表页是后台系统最典型的页面筛选表单在上表格数据在下分页器在底。筛选表单用el-form的inline模式排成一行点击“查询”时重新请求第一页点击“重置”时恢复默认值并重新加载数据。表格列里比较麻烦的是状态列的展示。后端返回的是0/1/2/3/4这样的数字前端不能直接渲染需要一个转换函数const statusMap { 0: { text: 可用, tagType: success }, 1: { text: 出车中, tagType: warning }, 2: { text: 维修, tagType: danger }, 3: { text: 停用, tagType: info }, 4: { text: 报废, tagType: danger } };这个映射放前端而不是后端返回中文是因为后端只负责数据展示逻辑交给前端接口可以复用到任何终端。表格里我还加了“详情”按钮点击弹出el-drawer展示车辆详细信息包括最近三次的维修记录和保险记录这些数据通过/api/vehicles/{id}/details接口获取前端拆成一个小标签页组件。表单页需要注意的点是编辑和新增复用同一个el-dialog组件用v-if区分是编辑还是新增。回填数据时因为后端返回的日期格式是yyyy-MM-dd HH:mm:ss表单里的el-date-picker需要格式化后再绑到v-model上否则会出现回显乱码。这个问题看起来很初级但我在实际项目里被困扰了很久。4.4 Vue处理动态路由和播放分组等话题顺带一提网上关于“Vue动态路由”的热度一直很高在管理系统里它的核心价值就是“菜单跟着权限走”。我实现时用了router.addRoutes动态添加路由并配合Vuex里的menuList生成侧边栏菜单。这套玩法如果你之前没做过可以先在小项目里尝试但这是一定要掌握的Vue工程化能力。这套车辆管理系统里页面本身没有视频播放的需求不过有技术讨论提到Vue播放m3u8免安装的方案——那一般是Video.js或hls.js的活和车辆管理系统关系不大。真正通用的原则是Vue项目尽量把业务逻辑拆成组件页面越薄系统越稳。5. 前后端联调、打包与生产部署全流程这一章直接给部署可用的完整步骤。我把开发环境的联调、生产环境的打包发布分开讲因为很多项目挂在“我本地能跑”这一步一部署就露馅。5.1 开发环境联调最常见的坑前后端分离项目本地联调最容易出问题的是跨域。后端如果没配CrossOrigin或全局CORS配置前端访问localhost:8080的接口时会报CORS错误。我在后端写了一个全局CORS配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里要特别注意allowCredentials(true)和allowedOriginPatterns(*)的组合如果通配符和Credentials冲突部分浏览器会不接受。更稳妥的做法是前端用代理开发时后端根本不需要开CORS让前端服务器转发请求即可这样也更能模拟生产环境的Nginx转发逻辑。前端Vue CLI的vue.config.js配置module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };5.2 后端打包部署后端用Maven打成一个可执行的Jar包我习惯用mvn clean package -DskipTests跳过测试。这里提一个SpringBoot版的坑不同SpringBoot版本对JDK版本有要求SpringBoot 2.x用JDK 8或11都没问题SpringBoot 3.x强制要求JDK 17以上。如果你本机默认JDK是17但手头项目是SpringBoot 2.x打包时可能会遇到编译问题需要统一在pom.xml里指定java.version。生产环境我用systemd托管这个Jar包[Unit] DescriptionVehicle Management API Afternetwork.target [Service] Userroot WorkingDirectory/opt/vehicle ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar /opt/vehicle/vehicle-api.jar Restartalways [Install] WantedBymulti-user.targetRestartalways很重要接口服务意外挂掉时能自动拉起比裸敲一条java -jar靠谱得多。5.3 前端打包与Nginx配置前端构建用的是npm run build产物是一个dist目录静态文件放到服务器的/usr/share/nginx/html/vehicle下面。Nginx配置里最核心的是把/api开头的接口请求反向代理到后端服务server { listen 80; server_name vehicle.example.com; root /usr/share/nginx/html/vehicle; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 前端Vue Router history模式需要此配置 location / { try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html这行是Vue Router history模式部署的生命线——不写它刷新子页面时Nginx会返回404因为物理路径下根本没有那个文件。如果你不想和服务端扯这个也可以改用hash模式URL会多一个#号但省心很多小项目我建议先hash上规模再切history。5.4 MySQL安装与数据初始化MySQL 8.x的安装网上教程一堆这里只强调初始化部分的三个关键动作建库指定utf8mb4字符集、设置时区为东八区、创建专用账号而不是直接用root。CREATE DATABASE vehicle_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER vehicle_app% IDENTIFIED BY Vehicle123456; GRANT ALL PRIVILEGES ON vehicle_db.* TO vehicle_app%; FLUSH PRIVILEGES;项目自带的sql/initschema.sql里有两张表的数据是必须的一张是系统用户表要有一条admin账号一张是角色表要配置好超级管理员角色。数据库导入的常见坑是字段注释里的中文乱码用Navicat导入如果乱码多半是连接时没用UTF-8编码连接URL需要加上characterEncodingutf8jdbc:mysql://localhost:3306/vehicle_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiserverTimezoneAsia/Shanghai这个参数是针对MySQL 8.x的强制要求不填会因为服务器时区偏差导致LocalDateTime查询结果差8小时而且MySQL 8.x的SSL连接报错也需要在URL里加useSSLfalse才能解决。这些都是我实际部署时一个一个试出来的。6. 上线之后踩过的坑和优化建议系统能跑通只是第一步真正细心打磨是从第一次真实使用开始。我在这个项目上线后遇到了一批典型问题挑几个有代表性的讲讲帮你少走弯路。6.1 日期查询的时区问题系统上线第一天管理员反馈“按照日期范围查询用车记录查出来的数据差一天”。排查后发现我在前端选的日期是2025-01-15传到后端被解析成2025-01-15T00:00:00但MySQL服务器时区是UTC存储时被转成了2025-01-14T16:00:00导致查不到当天的记录。解决办法是双管齐下MySQL连接URL加serverTimezoneAsia/Shanghai同时在创建数据库连接时确保MySQL服务端的time_zone也设置为8:00。另外日期范围查询时结束日期在前端最好传当天的23:59:59而不是00:00:00不然当天数据永远查不出来。6.2 MyBatis查询N1问题的出现和消除车辆列表页需要显示“最近一次保养时间”我在Mapper里先查出车辆列表然后在循环里逐条调用selectLastMaintenance方法应用卡顿半秒到一秒。这就是典型的N1查询一次列表查询加N次关联查询数据量一上来数据库压力全在循环查询上。解决办法是先查出车辆ID列表然后在SQL里用WHERE vehicle_id IN (...)一次将保养时间查出来映射成MapLong, LocalDateTime在Java内存中做一次关联。这个优化在车辆数量几千条时感受最明显响应时间从八九百毫秒降到一百多毫秒。6.3 密码加密和备份策略管理员和业务用户都使用明文密码是开发初期最容易犯的错误一旦数据库泄露全部账号都会失守。车辆管理系统我用了Spring Security自带的BCryptPasswordEncoder注册时对密码加密登录时用matches方法校验。哪怕数据库被人拖走里面的密码也无法直接反解。备份方面我用mysqldump定时任务每天凌晨2点全量备份到指定目录保留最近7天的备份文件0 2 * * * mysqldump -u vehicle_app -pVehicle123456 vehicle_db | gzip /backup/vehicle_$(date \%Y\%m\%d).sql.gz别觉得“项目数据量小就不需要备份”车辆管理系统的数据都是真金白银的实际业务记录丢一次就是事故。6.4 关于Spring Boot高版本和版本兼容的提醒如果你创建项目时直接选了Spring Boot 3.x会发现传统的springfox生成Swagger文档的方式已经失效需要换成springdoc-openapijavax.*包也变成了jakarta.*。网上关于“SpringBoot版本太高”的呼声一直都在很多老项目模板没法直接兼容。我的建议是车辆管理系统这类中小型项目用Spring Boot 2.7.x是当前生态兼容性最稳的选择部署Java 8或11就行如果你有长期学习规划想跟上新特性再上Spring Boot 3.x。不必追新能用、能维护、能稳定交付才是一个务实开发者的第一目标。7. 从这套系统谈一点项目复盘后的个人体会做完这个车辆管理系统最深的感受是技术栈不用复杂关键是把一套CRUD系统做到“业务上能自洽、代码上不给自己埋雷”。SpringBoot、Vue、MyBatis、MySQL这套组合每一个环节都有大量网上资料可以查但真正连接它们的是你对业务流转的理解和细节处的取舍。比如车辆管理里“驳回申请后要能重新申请”“车辆维修期间不能被申请”这类规则你得把状态机设计清楚而不是把所有判断堆在Controller里再比如“查询列表时能按车牌模糊搜索”这种需求看似简单却决定了前方后端SQL该如何组装参数。这些才是做项目真正学到的东西。如果你准备自己复刻一套我给你一个建议顺序先画业务状态流转图再建表再写后端接口最后再做前端页面。顺序错了返工成本能让人崩溃。后期还可以扩展的方向也不小车辆油耗记录分析、行驶轨迹查询、维保到期自动提醒甚至对接企业微信通知都是顺手就能加到现有架构上的能力。这套系统的价值不在于它用了什么高级框架而在于它是一个结构清晰、可以持续演进的起点。
返回列表