☰
SpringBoot+Vue驾校管理系统全栈实战:从需求分析到部署交付
2026/10/7 10:51:05 网站建设 项目流程

前阵子帮一个本地驾校做了套内部管理系统。说起来不算什么高深项目,但这类业务系统特别考人——需求琐碎、角色分明、字段多、状态流转复杂,而且每天都有真实的人在使用。最初驾校手里是一摞Excel表,学员信息散落在好几台电脑里,约课靠打电话,训练记录靠手填,月底统计报表更是让人头大。

后来我把这套系统重新用 SpringBoot + Vue 这套前后端分离架构梳理了一遍,交付时带着 MySQL 脚本、完整源码和一套说明文档,才算把这块硬骨头啃下来。这期间踩了不少坑,也沉淀了一套可以反复复用的落地套路。这篇博文就把整套实现思路完整拆开:从业务需求盘点到后端模块设计、数据库表关系、前端页面组织,再到打包部署和文档交付,最后把开发过程中真正遇到的坑也一并讲清楚。

这套东西适合谁?如果你正在做毕业设计,或者公司打算从零搭一套内部管理后台,又或者想完整走一遍 SpringBoot 全栈项目的实战流程,都可以直接照着这套思路来。它不矫情、不含糊,是那种可以真正上线运行的代码结构,不是光有 Demo 的玩具工程。

1. 为什么驾校需要一套管理系统:业务场景与需求盘点

1.1 传统驾校管理的核心痛点

驾校这个行业,业务链路其实非常长。招生报名只是第一步,后面跟着学员建档、缴费记录、科目学习、教练安排、学时统计、约考登记、考试结果、车辆维护……每一条线都是一个独立流程,但它们又彼此纠缠在一起。Excel 表最要命的地方在于,人跟人之间共享信息靠传文件,改没改、谁改的、什么时候改的,完全说不清。

我接手时对方提的第一个需求是“把学员档案管好”,但深入聊下去就发现,真正让他们白天焦头烂额的其实是一件事:约课的冲突管理。一个教练一天最多带几个学员,哪个学员练了多久,哪些学时还没打完,如果都靠人工记忆去排,必然出错。系统上线后最大的变化不是“录入更方便”,而是“所有状态在一个地方流转,责任清晰”。

1.2 系统使用角色与权限边界

做管理系统的第一件事,永远是把人分清楚。驾校管理系统至少有三种角色,再加上一个可以临时扩展的招生营销角色。

角色核心诉求主要操作面
系统管理员全局掌控、数据维护所有模块的增删改查、数据统计、教练排班、车型管理
教练员查看自己的任务、录入训练结果查看课表、确认约课、填写训练记录、上报车辆故障
学员查看个人进度、自助约课查看课程信息、约课/取消约课、查看学时、查看考试结果

实际项目里很多找上门的“软件外包客户”会把前台客服也算一个角色,但因为权限逻辑可以复用,我通常只在代码里预留一张角色表,具体菜单权限用 RBAC(基于角色的访问控制)去做。这样后面想扩展“招生专员”角色,只要在数据库里加一条记录,再分配菜单权限就行,不用改代码。这个设计决策很关键,千万别把角色写死成枚举散落在各处。

1.3 功能清单与技术边界

把需求收敛成一张功能清单,方便后面拆表、拆接口:

  • 系统管理:用户管理、角色管理、菜单管理、操作日志
  • 学员管理:学员列表、报名建档、证件上传、学员状态流转(在读/暂停/结业/退学)
  • 教练管理:教练信息、教练状态、带教学员分配
  • 约课管理:按教练查看时间槽、学员预约、取消、过期处理
  • 学时记录:训练开始结束时间、训练科目、教练填写评语
  • 考试管理:科目一/二/三/四约考记录、考试成绩登记、不合格自动进入补考流程
  • 车辆管理:车辆档案、年检到期提醒、维修记录
  • 统计报表:学员通过率、教练工作量、月度收入汇总

技术边界上,我选了 Vue 3 + Element Plus 做前端,要求 Node 16 以上跑得动;后端 SpringBoot 2.7.x + MyBatis-Plus + MySQL 8,JDK 用 1.8 或者 11 都行;Redis 6 做登录状态缓存和验证码存储;接口文档用 Knife4j 生成,交付时直接给 HTML 文档。

提示:SpringBoot 版本不要追最新,2.7.x 是兼容性最稳的一个版本线。我见过不少朋友直接上 SpringBoot 3.x,结果 MyBatis-Plus 和部分旧依赖一起报错,最后卡在环境上,项目本身的逻辑反而没时间写。

2. 后端工程落地方案:SpringBoot 模块化设计

2.1 技术栈选型与目录结构

后端这一层,我不会用任何花哨的微服务架构。一个驾校管理系统最多几十个并发,单体应用就是最优解。但单体不代表乱写,工程目录要按业务垂直切分,方便后续维护。

我的标准目录长这样:

com.example.driving ├── config # 跨域、Knife4j、MyBatisPlus配置 ├── controller # 接口层 │ ├── admin │ ├── coach │ └── student ├── service # 业务逻辑层 ├── mapper # 数据访问层 ├── entity # 实体类 ├── dto # 接收参数的DTO,不要直接拿实体接前端数据 ├── vo # 返回给前端的视图对象 ├── common # 统一返回结果、异常处理、基础工具 └── interceptor # 登录拦截器、权限拦截器

很多新手喜欢把实体类直接当返回参数往外抛。短期看没问题,但这会带来两个隐患:一是密码、手机号这种敏感字段容易在不经意间被序列化出去;二是如果表结构变了,前端接口也得跟着变。所以我在项目一开始就约定:接收参数用 DTO,返回数据用 VO,实体类只跟数据库打交道。这个约定后期非常省心。

2.2 登录鉴权与权限控制的实现

登录这块我采用 JWT + Redis 的组合。用户登录成功后生成一个 token,前端每次请求在请求头带上Authorization,后端拦截器统一校验。

Redis 在这个方案里负责两件事:一是把 token 拉黑,实现退出登录;二是存验证码,防止登录暴力破解。JWT 本身是无状态的,签发之后服务端无法主动让它失效,所以只依赖 JWT 而不加 Redis 的话,“强制下线”功能很难做。

核心代码大概长这样:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && JwtUtil.validateToken(token)) { String userId = JwtUtil.getUserId(token); request.setAttribute("userId", userId); return true; } response.setStatus(401); return false; } }

在 WebMvcConfig 里把需要放行的路径(比如登录接口、验证码接口)配置成白名单,其他接口一律走拦截器。权限控制对菜单、按钮两级做:菜单权限通过后端返回的路由列表控制,按钮权限通过注解控制,比如@PreAuthorize("hasAuthority('student:delete')")。

2.3 核心业务接口的抽象与复用

驾校系统的核心业务,可以抽象成四个字:查、约、记、统。

  • 查:学员、教练、车辆、缴费信息的列表查询,带关键词搜索 + 分页
  • 约:学员约教练的时间段,包含冲突检测
  • 记:训练学时的录入,状态变更都打日志
  • 统:柱状图、饼图、导出 Excel 用的统计接口

接口设计上,我习惯统一这种风格:

POST /api/admin/student # 新增学员 PUT /api/admin/student # 更新学员 GET /api/admin/student/{id} # 详情 POST /api/admin/student/query # 分页查询(带条件) DELETE /api/admin/student/{id} # 删除

分页查询用 POST 而不是 GET,原因是查询条件往往很长,POST 可以把 JSON 直接丢给后端,也方便未来加复杂组合条件而不改请求方式。MyBatis-Plus 的Page对象配合 LambdaQueryWrapper,两行代码就能搞定大部分分页查询。

2.4 文件上传、导入导出与统计报表

学员报名通常要上传身份证照片、驾驶证照片,所以写一个FileController是必须的。本地开发时把文件存储到项目指定的磁盘目录,application.yml里配一个file.upload-path,将来要迁到 OSS 也方便,只需要替换一个 service 实现类。

导出报表我用 Hutool 的 ExcelWriter,一句话就能生成 xlsx 文件:

ExcelWriter writer = ExcelUtil.getWriter(true); writer.addHeaderAlias("name", "姓名"); writer.addHeaderAlias("phone", "手机号"); writer.write(rows, true);

统计接口返回的数据结构要和前端图表约定清楚。饼图要[ {name, value} ],柱状图要{ columns: [...], rows: [...] },这类约定写在项目文档里,联调时能省一半沟通成本。

3. 数据库设计的实战推演:从字段到表关系

3.1 核心表结构与字段说明

数据库是这类管理系统的地基,设计得好不好,直接决定后面的接口写起来顺不顺手。我建的表包括:

  • sys_user(登录账号)
  • student(学员信息)
  • coach(教练信息)
  • vehicle(车辆信息)
  • course_reservation(约课记录)
  • study_record(学时记录)
  • exam_record(考试记录)
  • training_class(培训班次)
  • payment_record(缴费记录)

拿最核心的student表举例,字段不用太复杂,但该有的必须有:

字段名类型说明
idbigint自增主键
sys_user_idbigint关联登录账号
namevarchar姓名
id_cardvarchar身份证号
phonevarchar联系电话
sextinyint性别
statustinyint0在读 1暂停 2结业 3退学
entry_datedate报名日期
class_idbigint所属班次

唯一值得注意的是,student和sys_user是一对一关系,但两者要解耦。学员信息里的姓名、身份证这些自然属性放 student 表,登录名、密码、角色关联放 sys_user 表。这样将来如果学员毕业了要清理账号,不会误删业务数据,反之亦然。

3.2 约课与学时记录的数据流转

约课这块是最容易出 bugs 的地方。我的设计是:教练先维护自己的可约时间段(time slot),学员选择时间段提交预约;预约记录的状态字段是0待确认、1已确认、2已完成、3已取消。

关键点在插入当天约课记录之前,要查一次该教练在这个时间段是否已有其他status != 3的记录,存在则拒绝重复预约。这个校验必须放在数据库层面加一个唯一索引作为兜底,我当时的做法是给(coach_id, slot_date, slot_time)建联合唯一索引,否则并发情况下两条请求可能同时通过应用层校验,产生数据冲突。

学时记录表要记录教练 ID、学员 ID、训练日期、开始时间、结束时间、训练科目、教练评语。实际业务中,学时常数和约课记录不是一回事:一个约课时间段可能因为学员迟到只练了半个钟头,所以学时表应该在训练完成后由教练单独填写,不要直接引用约课记录里预设的时长。

3.3 考试与结业流程的状态机设计

考试记录的状态机值得单独讲。驾考的科目一、科目四是在电脑上考理论,科目二、科目三是上路实操,每个科目都要记录考试日期、考试地点、成绩、是否合格。用状态机控制:

  • 初始状态:待考试
  • 合格:标记通过,进入下一科目
  • 不合格:进入补考流程,状态置为待补考
  • 补考通过:继续下一科目
  • 全部科目通过:学员状态自动变为“结业”

最简单的做法就是加一个current_subject字段,从 1 到 4 递增。当科目四合格后,更新学员状态为结业。这种状态流转逻辑可以写一个单独的ExamService.nextStep(studentId)方法统一处理,尽量不要在 Controller 里散落 write 逻辑。

4. 前端 Vue 工程:管理后台的页面组织与交互实现

4.1 前端目录结构与路由规划

前端我选择 Vue 3 + Element Plus + Axios + ECharts。目录结构按业务模块划分,而不是按技术类型划分:

src ├── api │ ├── student.js │ ├── coach.js │ ├── reservation.js │ └── login.js ├── assets ├── components ├── layout ├── router ├── store └── views ├── login ├── dashboard ├── student ├── coach ├── reservation ├── exam └── system

路由设计上采用动态路由:登录后先从后端拉取当前用户有权限的菜单,再通过router.addRoute动态注册。这样管理员和教练登录后看到的后台完全不一样,而不是靠按钮隐藏来欺骗用户。

4.2 表格页、表单页、统计页的通用写法

后台管理系统百分之八十的页面本质是:搜索表单 + 表格 + 分页 + 新增/编辑弹窗。把这些页面组件化之后,后端管理系统的开发速度会翻倍。我是这么封装的:

// api/student.js export function queryStudentPage(data) { return request({ url: '/api/admin/student/query', method: 'post', data }) }

页面里统一用TablePage组件接收配置项:searchFields、tableColumns、api,然后内部自动处理加载状态、分页、刷新。新增编辑弹窗统一用DialogForm包裹,表单校验规则写在配置里。这套封装写完之后,后面每加一个模块基本就是复制一个目录,改改配置和字段,半小时出一个小模块。

4.3 前后端联调的接口约定

前后端联调最害怕各说各话。我在项目文档里固定了两条约定:

第一,接口返回格式统一:

{ "code": 200, "message": "操作成功", "data": {} }

Axios 响应拦截器里统一判断code,code 不是 200 的话直接弹出 message 内容。业务上不需要每个接口再手动处理错误分支。

第二,分页结构统一:

{ "total": 100, "records": [] }

Element Plus 的el-pagination正好接收这两个字段,前端拿到后直接塞进去即可,不用再做二次转换。这儿有个细节:MyBatis-Plus 的分页默认字段是records和total,后端 VO 就按这个格式返回,不要顺手改成list,不然前后端来回对齐字段会浪费很多时间。

5. 项目打包部署与文档交付:让源码真正跑起来

5.1 本地开发环境的准备

一个完整项目交付给别人的第一关,永远是环境。我把环境要求写进了 README 的第一段:

  • JDK 1.8 或 11
  • Maven 3.6+
  • MySQL 8.0
  • Redis 6.x
  • Node.js 16+
  • 开发工具:IDEA + VSCode 即可

数据库脚本放在sql/driving_school.sql,里面包含建库、建表、初始数据。初始数据必须自带一个管理员账号,比如admin/admin123,方便对方启动后立刻看到系统。我第一次交付时只给了建表脚本没给初始数据,对方连登录都登录不了,体验极差,后来所有项目都默认把初始账号写进脚本。

5.2 后端打包与前端构建的细节

后端打包就一条命令:

mvn clean package -DskipTests

需要注意application.yml里的数据库连接、Redis 地址不要写死成localhost,用${DB_HOST:localhost}这种占位符风格,部署时通过环境变量覆盖即可,既方便本地跑,也方便服务器部署。

前端构建要注意这里:

npm run build

构建产物在dist/目录。部署时我一般不做前后端完全分离,而是把 dist 直接复制到后端项目的src/main/resources/static/下重新打包成一个 jar。这样对方只需要java -jar driving-school.jar一条命令就能把前后端一起启动,运维成本降到最低。

但开发阶段必须配置跨域,后端单独跑在 8080,前端跑在 5173(Vite 默认端口)或 8081。跨域用 CorsFilter 全局放开,并且允许携带 token 请求头:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

5.3 部署时的常见问题与应急处理

我总结几个发生率最高的部署问题,哪怕你照着文档一步步走,也难免碰到:

一是数据库时区问题。连接串里必须加serverTimezone=Asia/Shanghai,否则插入时间比实际时间少八小时,很多人排查半天找不到原因。二是 MySQL 8 的驱动名是com.mysql.cj.jdbc.Driver,老代码里写的com.mysql.jdbc.Driver虽然也能跑,但会在启动日志里刷警告,建议直接改掉。三是前端打包路径问题,Vite 的默认base是/,如果部署在域名子路径下,页面会白屏。统一在vite.config.js里写base: './',让它用相对路径。

6. 这套系统开发过程中的实战心得与避坑指南

6.1 业务逻辑上最容易出错的地方

约课并发和状态更新是我在这个项目里踩得最深的坑。学员端在凌晨集中抢热门教练的时段,接口压力不算大,但数据库层面可能有碰撞。如果刚好有两个学员同时预约同一个时间段,应用层校验通过后,数据库唯一索引会直接报错。前端需要把 duplicate key 异常翻译成“该时段已被预约”的友好提示,不要直接抛 500。

另一个容易错的是删除逻辑。学员、教练这些核心业务表,不要真正 delete 物理删除,而是在表中加deleted字段,逻辑删除。原因很简单:历史数据是有价值的,学员退学后,他的缴费记录和学时记录还要用来统计财务报表。MyBatis-Plus 的@TableLogic注解可以全局实现逻辑删除,一条注解搞定。

6.2 体验层面的优化要点

管理后台的体验好不好,往往不体现在界面美不美,而体现在操作效率。我在这个项目里做了几个小优化,对方使用后反馈非常明显:

  • 学员列表支持身份证号、手机号模糊搜索,报名时用手机号一敲就出结果
  • 教练排班支持按星期批量设置,不用一天一天重复添加
  • 统计报表导出 Excel 时,带上前端当前筛选条件,而不是把所有数据导出来
  • 操作按钮加了权限控制,无权限的按钮直接不渲染,减少误操作

这些东西看起来零零碎碎,但真正决定一个系统“能不能用起来”的,恰恰就是这些细节。

6.3 二次开发与扩展建议

这套系统的架构本身预留了不少扩展点。如果你拿到源码之后打算继续往上加东西,我建议优先做三件事:

第一,把驾校的线上报名小程序接进来,学员在小程序里自助报名、支付订金,后台自动建档。第二,增加消息通知模块,考试日期、预约确认、学时提醒都通过短信或微信模板消息推送给学员,这能大幅降低前台人员的工作量。第三,把车辆年检、保险到期的提醒做成定时任务,在管理后台首页展示到期列表。

如果业务量真的起来了,单靠 MySQL 也够撑很大一阵子。只有当你要做跨校区部署的时候,才需要考虑把报表模块拆出来做成独立的统计服务,平时真不用过早追求微服务化。

最后分享一个我在多个项目里验证过的经验:一套项目的交付质量,从数据库脚本里是否带了初始数据、README 里是否写了环境版本号、接口文档是否和代码同步更新就能看出来。代码写得好是基本功,能把源码、数据库、文档整理成一套任何人拿到都能跑起来的交付物,这才是被人认可的专业度。希望这份基于 SpringBoot + Vue 的驾校管理系统实现思路,能帮你把项目做得又快又稳。

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

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

立即咨询