
前阵子帮一个本地驾校做了套内部管理系统。说起来不算什么高深项目但这类业务系统特别考人——需求琐碎、角色分明、字段多、状态流转复杂而且每天都有真实的人在使用。最初驾校手里是一摞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 8JDK 用 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 响应拦截器里统一判断codecode 不是 200 的话直接弹出 message 内容。业务上不需要每个接口再手动处理错误分支。第二分页结构统一{ total: 100, records: [] }Element Plus 的el-pagination正好接收这两个字段前端拿到后直接塞进去即可不用再做二次转换。这儿有个细节MyBatis-Plus 的分页默认字段是records和total后端 VO 就按这个格式返回不要顺手改成list不然前后端来回对齐字段会浪费很多时间。5. 项目打包部署与文档交付让源码真正跑起来5.1 本地开发环境的准备一个完整项目交付给别人的第一关永远是环境。我把环境要求写进了 README 的第一段JDK 1.8 或 11Maven 3.6MySQL 8.0Redis 6.xNode.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前端跑在 5173Vite 默认端口或 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 部署时的常见问题与应急处理我总结几个发生率最高的部署问题哪怕你照着文档一步步走也难免碰到一是数据库时区问题。连接串里必须加serverTimezoneAsia/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 的驾校管理系统实现思路能帮你把项目做得又快又稳。