☰
校车调度管理系统实战:SpringBoot+Vue3+MyBatis-Plus+MySQL全解析
2026/10/9 5:54:01 网站建设 项目流程

临近毕业季,“校车调度管理系统”这个题目几乎每年都会出现在毕业设计选题清单上。它比纯电商、纯后台管理更有场景感,排班、调度、核销、统计这些模块串起来,刚好覆盖了一个完整管理系统从CRUD到权限、从业务逻辑到部署运维的整个链路。最近不少同学在后台问这套源码的细节,今天干脆把整个项目梳理一遍:从为什么选这套技术栈,到数据库怎么设计、前后端怎么分工、部署怎么避坑,全部讲透,让你不仅能跑起来,还能在答辩时把设计思路说得明明白白。

这套系统我实际开发用的是标题里的组合——SpringBoot2做后端接口、Vue3写管理端页面、MyBatis-Plus操作数据库、MySQL8.0存数据。选它不是因为图新鲜,而是这套组合在目前企业项目和毕设场景里都足够主流,网上资料多,真踩了坑也容易搜到答案。下面我会按照一个完整的项目开发顺序,把每个环节拆开讲,所有步骤都是自己实测过能跑通的方案。

1. 技术选型解析:为什么是SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0

1.1 后端框架:SpringBoot2依然是毕业设计与小团队项目的最优解

很多人纠结要不要直接用SpringBoot3,我的建议是:除非你有明确理由,否则这套系统用SpringBoot2。原因很实在——SpringBoot2的生态极其成熟,绝大多数第三方starter、教程文档、面试题题库都基于2.x版本,遇到问题搜解决方案时命中率最高。SpringBoot3虽然新,但要求JDK17起步,一部分学校的实验环境还停留在JDK8,版本冲突会白白消耗大量调试时间,保守选择能少走弯路。

同时,SpringBoot2内嵌Tomcat,打包成jar后一条命令就能跑,这给后续部署省了很大事。配合统一返回结果类Result<T>和全局异常处理器@RestControllerAdvice,前端拿到的数据格式是一致的 JSON,错误信息也有统一结构,真出问题的时候前后端联调定位很快。

1.2 前端框架:Vue3组合式API写管理系统比Vue2舒服太多

前端用Vue3而不是Vue2,是因为管理系统这类中后台页面天然适合组合式API。比如乘车记录查询页,你需要同时管理查询条件表单、分页参数、表格数据、加载状态,在Vue2的Options API里,这些逻辑分散在data、methods、computed里,改一个功能要在几个地方来回跳;换成Vue3的setup函数,同一个查询流程的数据和操作可以写在一起,维护起来非常直观。

我这套项目用的是Vite + Vue3 + Element Plus + Pinia + Vue Router + Axios的前端全家桶。Vite开发服务器的冷启动速度是真快,改完代码热更新几乎无感知,比Webpack时代的等待体验强太多。状态管理选Pinia不选Vuex,主要看中它的API设计更简洁,写起来像普通函数一样,没有Vuex那种commit、dispatch的繁琐流程,对新手相当友好。

1.3 ORM与数据库:MyBatis-Plus把单表操作简化到了极致

数据库操作用MyBatis-Plus,核心就一句话:单表CRUD不用写SQL了。继承一个BaseMapper<T>,常用的增删改查、分页查询、条件筛选全都有了。配合LambdaQueryWrapper,写查询条件的时候有IDE的代码提示,不会因为手写字符串字段名而拼错。比如我要查某个班级某天所有的乘车记录,一行条件构造器就能搞定。

MySQL8.0则是目前主流云数据库和新装环境的默认版本,它比5.7多了窗口函数、公共表表达式这些实用的分析能力,统计模块里计算“各班乘车次数排行”这类需求时,一条SQL就能完成,不用在Java代码里做内存计算。另外MySQL8.0默认字符集是utf8mb4,存用户昵称里的emoji表情不会再乱码,也是选它的一个现实原因。

2. 数据库设计:校车调度系统最核心的十张表

2.1 业务梳理:先搞清楚有哪些角色和数据流

拿到需求别急着建表,先把角色和数据流捋清楚。校车调度管理系统有四种角色:学生、教师、司机、管理员。学生和教师是乘车人,司机负责驾驶车辆,管理员负责排班、调度和整体管理。核心业务场景有三条线:一是固定线路校车的日常排班,二是临时用车的调度申请,三是每次乘车的核销记录。围绕这三条线,数据表可以拆成“用户体系”和“调度业务”两组。

用户体系拆成一张user表就够了,用用户类型字段区分学生、教师、司机和管理员,没必要建四张表。调度业务则需要车辆表、司机表、路线表、班次表(排班表)、调度单表、乘车记录表。这里要特别注意,司机虽然是用户的一种,但和车辆、班次都有直接关联,单独建一张司机表还是把司机信息放在用户表里,会影响后面SQL联查的复杂度,我选择了把司机信息单独抽出来,关联user表的id,这样统计司机出车次数非常方便。

2.2 核心表结构:字段设计和索引规划

下面这套表结构是实际跑过业务验证过的,直接复制可以少踩很多坑:

-- 用户表 CREATE TABLE `sys_user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `username` VARCHAR(50) NOT NULL COMMENT '登录名', `password` VARCHAR(100) NOT NULL COMMENT 'BCrypt加密密码', `real_name` VARCHAR(50) NOT NULL COMMENT '真实姓名', `user_type` TINYINT NOT NULL DEFAULT 0 COMMENT '0学生 1教师 2司机 3管理员', `phone` VARCHAR(20) NULL COMMENT '联系方式', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; -- 车辆表 CREATE TABLE `bus` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `plate_no` VARCHAR(20) NOT NULL COMMENT '车牌号', `seat_count` INT NOT NULL DEFAULT 0 COMMENT '座位数', `bus_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1运行中 2维修', `driver_id` BIGINT NULL COMMENT '绑定司机id', PRIMARY KEY (`id`), UNIQUE KEY `uk_plate_no` (`plate_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='校车信息表'; -- 路线表 CREATE TABLE `route` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `route_name` VARCHAR(50) NOT NULL COMMENT '路线名称', `start_point` VARCHAR(100) NOT NULL COMMENT '起点', `end_point` VARCHAR(100) NOT NULL COMMENT '终点', `mileage` DECIMAL(6,2) NULL COMMENT '里程公里', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='校车路线表'; -- 班次表(排班表) CREATE TABLE `schedule` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `route_id` BIGINT NOT NULL COMMENT '路线id', `bus_id` BIGINT NOT NULL COMMENT '车辆id', `depart_time` TIME NOT NULL COMMENT '发车时间', `arrive_time` TIME NOT NULL COMMENT '预计到达时间', `run_date` DATE NOT NULL COMMENT '运行日期', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待发车 1已发车 2已到站 3已取消', PRIMARY KEY (`id`), KEY `idx_run_date` (`run_date`), KEY `idx_route_bus` (`route_id`, `bus_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班次排班表'; -- 乘车记录表(核销表) CREATE TABLE `ride_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `schedule_id` BIGINT NOT NULL COMMENT '班次id', `user_id` BIGINT NOT NULL COMMENT '乘车人id', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0正常乘车 1缺勤 2已取消', `check_time` DATETIME NULL COMMENT '核销时间', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_schedule_user` (`schedule_id`, `user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='乘车核销记录表';

这里有两个设计细节值得展开。第一,表之间我全部用逻辑外键,不建物理外键约束,靠业务代码控制关联关系的正确性。原因很简单:项目跑起来之后,带外键的表做初始化、造测试数据、批量删除都会受到约束限制,而逻辑外键配合索引在性能上没有明显差别,维护成本反而低很多。

第二,ride_record表里的uk_schedule_user联合唯一索引非常关键。它保证了同一个用户对同一个班次只能有一条乘车记录,就算用户手抖连续点两次“确认乘车”,数据库层面也会拦下第二次插入,返回内置唯一键冲突错误,从而省掉了在应用层加锁的复杂度。这一招是防止脏数据和重复提交最省事的方案。

2.3 统计报表的数据来源:别为报表过度设计

系统里还有一个“乘车统计”模块,要展示每日乘车人数、各路线满载率、班级用车排行榜。有人会为这些指标单独建统计表,我的建议是初期完全没必要。统计指标都可以通过ride_record、schedule、route这三张表实时计算出来,数据量在几万条的时候查询毫无压力。单独建统计表意味着写数据时要考虑同步更新,读数据要判断统计是否过期,复杂度翻倍但收益很小。等真出现性能问题,再引入离线统计或缓存调度也不迟。

3. 后端核心实现:SpringBoot2整合MyBatis-Plus与业务模块

3.1 项目骨架与依赖配置

后端项目我建议按模块分包:controller、service、mapper、entity、dto、config、common、security。搞清职责的边界,后面扩展功能不会乱。其中common放统一返回结果和异常结构,security放JWT工具和登录拦截逻辑,dto放前端请求参数对象。

核心依赖不需要太多,梳理如下:

<dependencies> <!-- Web启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.18</version> </dependency> <!-- MyBatis-Plus启动器 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- JWT工具 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!-- Lombok 简化代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>

对应的application.yml里,有几个配置项值得专门说明。数据库连接串必须带serverTimezone=Asia/Shanghai,否则MySQL8.0驱动默认按UTC时区连接,查出来的时间会比本地晚8小时。allowPublicKeyRetrieval=true是因为MySQL8.0的caching_sha2_password认证插件在第一次连接时需要拿到RSA公钥,不开启这个参数,连接时会直接报Public Key Retrieval is not allowed,这个坑我见过太多次了。

spring: datasource: url: jdbc:mysql://127.0.0.1:3306/school_bus?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

MyBatis-Plus配置里我开启了map-underscore-to-camel-case,这样数据库下划线字段自动映射到实体的驼峰属性,不用手动写一大堆ResultMap映射。开发阶段打开SQL日志StdOutImpl有个好处——所有执行的SQL都会打印到控制台,排查条件构造器生成的语句时一目了然。上线前记得把日志级别调成error,不然刷屏刷到怀疑人生。

3.2 登录认证与权限控制

权限这块没有引入Spring Security,而是用JWT加拦截器的轻量方案。为什么不用Security?虽然功能全面,但这套系统的权限粒度只需区分登录与未登录、管理员与普通用户,用Security会引入一整套AuthenticationManager和过滤器链配置,代码量和理解成本明显偏高。JWT方案的逻辑非常直白:登录成功用JJWT生成token返回前端,前端每次请求在Authorization请求头带上token,后端写个拦截器验证token有效性和用户角色,验不过就直接返回401。

JWT工具类内部,我用用户的id和userType作为claims载荷,并且设置了一个7天的过期时间。写拦截器的时候有个细节要注意:预检请求OPTIONS必须直接放行,否则前端跨域请求会因为拿不到正确响应而失败。同时在拦截器里把校验通过的用户信息放入ThreadLocal或Request属性,后续controller就能直接从请求里拿到当前用户,不用每次查库。

有一个很容易被忽视的坑:如果业务上需要强制用户下线或者封禁某个账号,只靠JWT过期时间是不够的。最简单粗暴也最有效的做法是在用户表里加一个status状态字段,每次请求拦截器里去查一次用户状态,状态异常就拒绝并让用户重新登录。这样的代价是多了一次数据库查询,但换来了安全性的确定性。

3.3 排班与临时调度:核心业务的状态控制

排班模块本质上是批量新增schedule记录。后台管理员选择一条路线、关联一辆车、填写发车和到达时间、选定运行日期,后端先要校验这辆车在同一个时间区间内有没有已经安排过其他班次,避免一车兼跑多条线造成冲突。校验逻辑用MyBatis-Plus条件构造器就能完成:查同一辆车、同一天、时间区间有重叠且状态不是取消的班次,如果存在就抛出业务异常。

临时调度则走一套“申请-审核-派车”的流程。教师或学生提交临时用车申请,填写人数、出发地、目的地、时间;管理员看到申请后,选择空闲车辆和司机完成调度;调度完成之后自动生成一条班次记录。这类状态流转的代码,我的经验是给每个状态定义一个常量,用一个小型状态机方法集中处理“当前状态能否跳转到目标状态”,不是在业务代码里到处写if判断,不然过一个月你自己都看不懂哪里漏了状态校验。

乘车核销是学生上车后扫码或手动点“确认乘车”的入口。这里关键的幂等设计前面说过,靠数据库唯一索引兜底。Service层写完插入逻辑后,捕获DuplicateKeyException,返回友好的“你已登记过本次乘车”提示,而不是让用户看到一串生硬的唯一索引报错信息。

3.4 统计模块:MyBatis-Plus写复杂SQL的补充方案

MyBatis-Plus的条件构造器适合单表查询,多表联查的统计需求就得补充原生SQL。我习惯直接在Mapper接口上写@Select注解,稍微复杂一点的就放到XML里面。比如查“各班乘车次数排行”,SQL大概是:

SELECT u.class_name AS className, COUNT(r.id) AS rideCount FROM ride_record r JOIN sys_user u ON r.user_id = u.id WHERE r.status = 0 AND r.check_time BETWEEN #{startTime} AND #{endTime} GROUP BY u.class_name ORDER BY rideCount DESC

@Select注解写简单查询非常高效,但是遇到几个表关联、动态排序、多个可选查询条件的时候,还是XML文件维护起来更清晰。我在这套项目里把通用Mapper注入的CRUD能力当作一块砖,把原生SQL当作另一块砖,哪里方便用哪块,不要抱着“必须只用一个”的心态把自己框死。

3.5 事务与异常处理:别让一个空指针毁了整次请求

校园车调度过程中,生成班次、记录审批日志、更新车辆状态往往在一个事务里完成。Service层方法加@Transactional(rollbackFor = Exception.class)是最基本的要求,重点是要指定rollbackFor,不然Exception异常默认不会触发回滚,数据就写了一半。

异常处理方面,我写了一个全局异常处理器,细分了几种场景:参数校验失败抛出MethodArgumentNotValidException,返回400;业务规则不满足抛出自定义BusinessException,返回200 + 业务错误码;其他未捕获异常返回500并记录日志。这么做的价值在于:前端只需判断返回体里的code字段是否为200,其他统一弹错误提示即可,不用每个接口分别处理异常状态码。

4. 前端Vue3管理后台:组件化搭建和业务页面实现

4.1 Vite项目搭建与基础结构组织

前端项目直接使用Vite的create-vue模板初始化,选上TypeScript和Vue Router。项目结构按“视图 + 组件 + 请求 + 状态”来分:views放页面级组件,components放可复用的业务组件,api文件夹按模块存放后端接口请求函数,store放Pinia状态,router配置路由。如果某些页面里有两个子模块都用到同一个搜索表单组件,把它抽出来放components,能少写不少重复代码。

登录页和主布局是两个基本入口。登录页调用/login接口拿到token和用户信息后写入Pinia,同时把token持久化到localStorage,刷新页面后Pinia重新加载时从localStorage恢复登录态,这样就不会出现F5之后跳回登录页的尴尬。

4.2 Axios请求封装与路由守卫

Axios封装是整个前端最值得认真写的地方。我在request.js里做了统一处理:请求拦截器中从localStorage拿token并设置到Authorization请求头;响应拦截器中判断返回码,非200统一用Element Plus的ElMessage弹错误提示,401时自动清除登录态并跳转登录页。这一层做好之后,业务代码里请求成功之后只需关心正常返回数据,错误处理不用每处重复写。

路由守卫分成两层逻辑。全局前置守卫检查访问的页面是否需要登录,没有token就直接跳登录页。对于管理员专属页面再判断当前用户的角色,不匹配就跳转403页面。页面级的权限控制不能只靠前端隐藏菜单,后端接口同样必须校验权限,前端只是给用户更好的体验,真正的安全边界永远在服务端。

4.3 排班管理页与动态表单:Vue3响应式的高频实战

排班管理页是前端最复杂的核心页面。管理员需要一个表单来选择路线、车辆、日期、时间,还要能动态添加多天“批量排班”。这里正好用上Vue3动态增删表单行数据的能力:用reactive定义scheduleList数组,每行绑定一个班次对象,点“新增一天”就push一条新记录,点“删除”就splice掉对应索引。

<script setup lang="ts"> import { reactive } from 'vue' const scheduleForm = reactive({ routeId: undefined, busId: undefined, items: [{ runDate: '', departTime: '', arriveTime: '' }] }) function addScheduleItem() { scheduleForm.items.push({ runDate: '', departTime: '', arriveTime: '' }) } function removeScheduleItem(index: number) { if (scheduleForm.items.length === 1) { return } scheduleForm.items.splice(index, 1) } </script>

这里有一个Vue3新手最容易踩的坑:如果用ref定义数组,修改数组里对象的某个属性时,因为ref内部用reactive包装对象,所以属性本身能被追踪;但如果你是先取出来再赋值给局部变量、或者直接arr[index] = xxx整体替换某个元素,新旧对象没有深度比较,页面很可能不更新。遇到这种问题,优先用splice或Object.assign去改,或者干脆都用reactive包数组,避免心智负担。

页面上用Element Plus的el-table展示已有班次列表,搜索区支持按路线和日期筛选。表格列里有个操作列,不同状态显示不同按钮:待发车状态的班次可取消,已发车状态可标记到站。维护前端状态的时候,我做了个小优化:表格数据请求回来之后,在data里额外维护一个statusMap对象,把班次状态码映射成中文标签和标签颜色,用el-tag渲染,比在模板里写一堆三元表达式清晰得多。

4.4 Tabs多页签与后台布局的小技巧

后台布局里我用Element Plus的el-tabs做了一个多页签导航,方便管理员在多个页面之间快速切换。页签的数据来源是路由的meta标题,每次路由切换时把当前页签加入,关闭页签时如果关闭的是当前页面就自动回退到最后一个页签。

一个小细节:默认的el-tabs样式在最外层会有一条很明显的下划线,和后台顶部导航风格不搭,我通过样式覆盖把标签栏改成胶囊风格。具体做法是给el-tabs加一个自定义类名,用::v-deep深度选择器覆盖.el-tabs__nav-wrap::after的边框和.el-tabs__item.is-active的背景色。顺便说一句,如果你在开发调试时发现改完样式页面没生效,多半是Vite的样式热更新在个别场景下抽风,刷新页面基本就能解决,别急着怀疑自己写错选择器。

5. 环境部署与MySQL8.0踩坑记录

5.1 Docker安装MySQL8.0并完成初始化

部署这一步,开发环境强烈建议用Docker跑MySQL8.0,好处是环境一致性好,换电脑、换系统都不影响,团队协作时大家用的数据库版本完全一致。

docker run -d \ --name school_bus_mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root \ -e TZ=Asia/Shanghai \ -v /data/mysql8/conf:/etc/mysql/conf.d \ -v /data/mysql8/data:/var/lib/mysql \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci

这里三个参数是专门为了规避后续麻烦:TZ=Asia/Shanghai保证容器内时区正确,character-set-server=utf8mb4让所有新建数据库默认支持中文和表情符号,collation-server=utf8mb4_unicode_ci设置合理的排序规则。如果你导入别人给的SQL文件之后出现Unknown collation: utf8mb4_0900_ai_ci的报错,就是因为MySQL8.0某些版本的默认排序规则和MySQL5.7导出的SQL对不上,需要在导入前改用utf8mb4_unicode_ci。

没有Docker的情况下,Linux服务器可以直接用包管理器装MySQL8.0,安装之后还要注意改一下root用户的认证插件:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;

这个命令把root账号改成老兼容的密码插件,否则部分版本的Navicat连接会直接报认证失败。而像DBeaver这类新版客户端用caching_sha2_password连接没问题,但也要在连接设置里勾选“允许公钥检索”。

5.2 后端打包部署与前端构建

后端打包用Maven命令:

mvn clean package -DskipTests

生成的目标目录下会有一个xxx.jar,上传到服务器后执行:

nohup java -jar school-bus-server.jar > server.log 2>&1 &

前端构建也简单:

npm run build

产物在dist目录,配置Nginx静态服务并反向代理/api前缀到后端端口。

server { listen 80; server_name bus.example.com; root /var/www/school_bus/dist; index 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; } location / { try_files $uri $uri/ /index.html; } }

需要强调try_files $uri $uri/ /index.html这一行,Vue打包出的单页应用路由是前端路由,刷新/schedule页面时服务器如果找不到对应文件会返回404,加上这个回退配置,所有路由都交还给index.html去处理。

开发环境的跨域问题,我就在后端WebMvcConfigurer里配置了允许跨域,开发时前端Vite代理也不是必须的。但上线后前后端如果同域部署,走Nginx反代即可,跨域配置可以保留也不会有副作用。

5.3 服务器资源规划与日志管理

毕设项目的服务器配置不需要太高,1核2G的云主机完全够跑一套SpringBoot加Nginx。但是有两点要注意:第一,Java进程默认堆内存可能按物理机的四分之一分配,小机器上建议显式加参数-Xms256m -Xmx512m,避免内存不足导致进程被杀;第二,nohup启动的日志会越来越大,写一个简单的Shell脚本定期按日期切割日志文件,或者直接用系统自带logrotate做轮转,能避免后期磁盘被打满。

6. 常见问题与排查技巧实录

6.1 高频报错速查表

下面这些问题是做这套系统最容易遇到的,我按出现频率整理成了一张表:

问题现象根本原因解决方案
MySQL连接报 Public Key Retrieval is not allowedMySQL8.0使用caching_sha2_password密码插件JDBC URL加allowPublicKeyRetrieval=true
数据库查询出的时间比实际少8小时驱动连接时区未指定URL加serverTimezone=Asia/Shanghai
MyBatis-Plus分页不生效未注册PaginationInnerInterceptor插件新建MybatisPlusConfig配置类并注册分页插件
前端登录请求报跨域错误后端未配置允许跨域实现WebMvcConfigurer重写addCorsMappings
ID字段自动填充,新增时却是null未配置全局ID生成策略yml里配置id-type: assign_id
用Postman测试接口出现中文乱码响应的Content-Type缺字符集确认代码中应用了StringHttpMessageConverter
Vue3页面修改数组值不更新响应式数组索引赋值不被劫持改用splice方法或重新整体赋值

分页插件不生效这个问题再补充一句:很多人只是加了MyBatis-Plus依赖,没有创建配置类,导致Page参数传进去之后查询出来的rows是全表数据。正确做法是创建一个MybatisPlusConfig,注入MybatisPlusInterceptorBean,然后通过addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL))注册分页拦截器,一个都不能少。

6.2 MyBatis-Plus自动填充不生效的排查方法

自动填充是常见到无法回避的坑。实体里写了@TableField(fill = FieldFill.INSERT),也实现了MetaObjectHandler,但插数据时createTime还是null,核心原因是你的handler类没有交到Spring容器里。检查两个点:第一,handler类是否加了@Component注解;第二,实体字段名是否跟handler里setFieldValByName写的字符串完全一致。这两个地方极容易写错,而且不报编译错误,只能靠数据观察。

另一个让我记忆深刻的坑是逻辑删除和唯一索引打架。如果用户表设置了deleted逻辑删除字段,再给用户名加数据库唯一索引,用户被删除后再次注册同名账号时,数据库里实际上还躺着那条逻辑删除的老记录,新插入同样用户名的记录必然触发唯一键冲突。解决方法是把唯一索引改成联合索引(username, deleted),或者删除用户时把username做一次匿名化处理比如加时间戳后缀,保证数据库中不存在重复的活记录。

6.3 前端Edge浏览器下的测试建议

后端接口基本稳定的情况下,最后留出时间专门做一轮浏览器兼容测试。特别是Windows电脑默认的Edge浏览器,它是Chromium内核,大部分功能跟Chrome一致,但中后台页面如果用了比较复杂的Tabs多页签布局和弹窗嵌套场景,某些页面在播放过视频或者切换过系统主题之后,浏览器自身的扩展插件会干扰网页渲染。遇到“明明代码没问题,但页签突然点了没反应”这种诡异情况时,优先排查是否是浏览器插件拦截、硬件加速导致的渲染异常,让用户关掉硬件加速试试。这也是为什么很多人反映“项目在Edge上访问有小问题,但Chrome完全没事”的原因,不一定是代码bug。

6.4 几个临时救场的实用经验

最后一个救命技巧:如果后端接口出了疑难杂症,先打开application.yml里的SQL日志打印,再在拦截器上加一层请求日志输出,就能看到完整的“请求路径 -> SQL执行 -> 返回结果”链路。很多所谓的神秘bug,其实只是参数传参时字段名写错,或者Json格式化时把int类型传成了字符串类型,把日志一关,逻辑立刻就通了。调试Vue3项目时也是一样,打开Vue Devtools的Pinia面板看一下当前登录态和页面数据对不对,十有八九能定位出是状态没赋值还是请求没走到。

这套系统做完之后我的感觉是,毕设项目的核心价值不只是把功能跑通,而是你能不能在答辩的时候把每个设计决策背后的“为什么”讲清楚。为什么数据库表要这样拆,为什么并发场景下靠数据库唯一索引而不是压测时再去加内存锁,为什么前端要封装那一层请求拦截器,这些问题想通了,这个项目的含金量自然就上来了。最后再分享一个小技巧:这套系统做完之后,把ride_record表多造点测试数据,用递归公共表表达式生成连续一个月的时间序列,分页和统计模块的性能验证会更扎实,答辩演示的时候也不至于因为列表数据太少显得空荡荡。

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

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

立即咨询