基于SpringBoot+Vue的驾校预约系统设计与实现
1. 项目背景与需求拆解驾校预约系统到底在解决什么问题驾校预约学习系统说白了就是把驾校里“学员约车、教练排课、车辆调度、学时统计”这一整条业务链搬上线。很多驾校到现在还在用最原始的方式管理预约——学员打电话给前台、前台登记小本子、教练手动核对时间表碰上高峰期一个时间段被重复预约、教练和车辆排期冲突是家常便饭学员投诉多驾校管理也头疼。我做的这套系统核心就是解决这几个场景第一学员端在线预约。学员注册登录后能看到一周内的可约时段选择教练、选择车辆、选择科目二还是科目三提交预约后实时锁定时段不用再打电话排队。第二教练端排课确认。教练登录后可以查看自己的预约列表确认或拒绝预约支持按日期筛选系统自动统计每个教练的带教学时。第三管理端统一调度。管理员可以管理教练档案、车辆信息、学员档案、预约记录还能看到整个驾校的预约总览和学时统计报表哪些时段拥挤、哪些教练空闲一目了然。第四规则约束自动校验。比如一个时段只允许一个学员预约一个教练、车辆不能同时被两个学员占用、学员的剩余学时不足时不能预约新课程这些校验规则由后端统一强制从源头上杜绝了人工管理最容易出的冲突问题。我最初接手这个项目时某驾校反馈的痛点非常具体旺季一天有八十多个预约电话前台登记根本忙不过来而且学员经常临时改时间纸质记录一改就乱。后面我把预约改成线上自助之后前台工作量大减学员满意度也上去了。这就是这类系统的核心价值——不是单纯“把表格搬到网页上”而是用一套规则引擎替人做调度和校验。技术上这套系统的设计目标是后端用Java生态里最成熟的SpringBoot作为业务层框架MyBatis负责数据库访问前端用Vue Element UI搭后台管理界面数据库用MySQL保存全部业务数据。整套系统分管理员端、教练端、学员端三个角色权限互不相同前后端通过RESTful API交互。2. 技术选型解析SpringBoot、Vue、MySQL、MyBatis各自承担什么角色2.1 后端为什么选SpringBoot而不是SSH或SSM很多人问既然用了MyBatis那直接用SSMSpring SpringMVC MyBatis不就行了为什么一定要SpringBoot我一开始也犹豫过但实际开发下来SSM最麻烦的是配置——要手动写web.xml、spring-mvc.xml、spring-mybatis.xml还要配一堆jar包依赖光是环境搭建就要折腾大半天而且不同版本之间的兼容问题特别多。SpringBoot的作用是把这些基础设施全部“约定优于配置”化。内嵌的Tomcat容器让项目可以直接通过java -jar启动不需要单独部署war包starter机制把MyBatis、MySQL驱动、安全框架等常用组件的依赖自动管理起来自动配置功能省掉了大量XML配置。我实测过从一个空的SpringBoot工程到能跑通一个返回JSON的接口只需要三分钟。举个例子SSM时代我们要在mybatis-config.xml里配置别名扫描、Mapper扫描路径、驼峰映射规则在Spring配置里配数据源、事务管理器、SqlSessionFactory。到了SpringBoot只需要在application.yml里写上数据源的四行配置加一个MapperScan注解就完事了。2.2 Vue在前端扮演的角色和Element UI的价值这套系统的前端部分我选择了Vue 2 Element UI组合。驾校预约系统的前端页面主要分两类一类是后台管理页面表格密集、表单多、操作按钮多这类页面恰恰是Element UI最擅长处理的场景另一类是学员预约流程页面稍微有点交互逻辑但也不算复杂。有人问为什么不用更流行的React或者Vue 3。我的理由是驾校预约系统的前端复杂度属于中等偏下用Vue 2 Element UI的成熟组合开发效率最高。Element UI的表格组件自带排序、筛选、分页功能表单组件自带校验日期选择器支持快捷选项这些功能如果全部手写工作量至少要翻三倍。前后端分离的架构也值得一提。后端只负责输出JSON数据前端通过axios发起HTTP请求拿数据渲染页面。这样做的好处是开发阶段可以后端工程师和前端工程师并行工作互不阻塞部署时前端打包成静态文件放在Nginx上后端独立运行各自可以单独扩容。2.3 MyBatis MySQL配合使用的真实体验为什么选MySQL因为驾校预约系统的数据量级完全在MySQL的舒适区内。预约记录一天最多几百条一年也就十万级别MySQL单表处理毫无压力而且MySQL的开源生态成熟、运维成本低驾校这种预算有限的机构最合适。MyBatis在这个项目里负责的是数据库访问层。它对SQL的控制力比JPA强复杂查询可以直接手写SQL优化起来非常方便。比如预约冲突校验这个场景我需要查出某个教练在某个时间段是否已有被确认的预约记录这条SQL用MyBatis的Select注解写在Mapper接口上查询逻辑一目了然。Select(SELECT COUNT(*) FROM appointment WHERE coach_id #{coachId} AND start_time #{endTime} AND end_time #{startTime} AND status IN (CONFIRMED, PENDING)) int countConflictAppointments(Param(coachId) Long coachId, Param(startTime) LocalDateTime startTime, Param(endTime) LocalDateTime endTime);用MyBatis还有一个优势是动态SQL。驾校管理员的预约记录查询页面有很多筛选条件——按日期查、按教练查、按学员查、按状态查如果每个组合都写一条SQL那得写几十条。MyBatis的where和if标签可以动态拼接SQL研发和维护成本都低很多。3. 数据库设计八张核心表撑起整个驾校业务闭环3.1 用户体系的三角色模型设计驾校预约系统的用户不是一个单一实体而是三种角色的集合管理员、教练、学员。在设计数据表时我有两种方案可选方案一是建一张用户表加一个角色字段用role字段区分ADMIN、COACH、STUDENT三种角色。方案二是建三张独立的表分别存储三种角色的信息。我选了方案一。驾校业务里教练和学员都有共同的属性——姓名、手机号、密码、登录账号而且登录鉴权逻辑可以统一处理。如果分表存储登录时还得判断走哪张表反而麻烦。但教练有教练专属字段如教龄、带教科目、可约时段偏好学员有学员专属字段如当前科目、剩余学时这些为什么不放到用户表里因为不是所有人的必备属性用扩展字段会导致用户表出现大量NULL影响维护性。最终设计是这样sys_user表存基础登录信息和角色coach_info表存教练扩展信息student_info表存学员扩展信息。三张表之间通过用户ID关联。CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(255) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(30) NOT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL COMMENT 手机号, role varchar(20) NOT NULL COMMENT 角色ADMIN/COACH/STUDENT, status tinyint(1) DEFAULT 1 COMMENT 状态1启用 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;角色字段加上status状态字段后管理员可以一键禁用某个教练或学员的账号这是最初需求里特别强调的——有学员欠费或者教练离职时要能立刻停止其系统权限。3.2 教练信息表和学员信息表的扩展设计教练信息表的字段我实际开发时踩过一个坑。最初我把“教练的擅长时间段”直接设计成[{week:1,start:08:00,end:10:00}]这种JSON字符串存在单个字段里想着反正只是展示给学员看。后来管理员提出需求预约时段要按教练的排班计划自动生成不能显示教练休息的时间段。这就麻烦了JSON字符串存数据可以但要参与查询运算就很别扭。后面我改成了两张表coach_info存教练的固定属性coach_schedule单独存教练一周七天每天的排班区间。这属于典型的“单表存储拆成关联表”的演进过程。学员信息表相对简单核心字段是当前阶段的科目科目二/科目三、剩余学时课时数/总课时数。学时数在学员报名时由管理员初始化每次预约成功扣减一学时学员在个人中心能实时看到剩余学时。这里有个业务细节一定要提——学员预约科目二和科目三分别扣什么驾校规定科目二、科目三的学时必须分开统计不能混用。所以我设计的是CREATE TABLE student_info ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 关联sys_user.id, current_subject varchar(10) DEFAULT SUBJECT_2 COMMENT 当前科目, total_lesson_count int(11) DEFAULT 0 COMMENT 总购买课时, used_lesson_count int(11) DEFAULT 0 COMMENT 已使用课时, remaining_subject2_count int(11) DEFAULT 0 COMMENT 科目二剩余课时, remaining_subject3_count int(11) DEFAULT 0 COMMENT 科目三剩余课时, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学员扩展表;3.3 车辆、预约、课时记录三张业务表的关联逻辑车辆表car_info记录每台教练车的车牌号、车型、车况状态、所属教练可选。为什么车辆要跟教练可关联可不关联因为有些驾校是“一人一车”有些是“多人轮车”我做成可空的关系适配不同驾校的运营模式。预约记录表appointment是整张业务表里的核心字段包括字段类型说明idbigint主键student_idbigint学员ID关联student_infocoach_idbigint教练ID关联coach_infocar_idbigint车辆ID关联car_infosubjectvarchar科目SUBJECT_2/SUBJECT_3start_timedatetime预约开始时间end_timedatetime预约结束时间statusvarchar状态PENDING待确认/CONFIRMED已确认/CANCELLED已取消/COMPLETED已完成create_timedatetime创建时间这个表一出来预约冲突校验的核心SQL就清晰了查同一个教练或同一辆车在重叠时间区间内是否存在非取消状态的记录。课时记录表lesson_record是审计用的。每次预约成功、取消、完成都会在该表插入一条记录记录操作类型和操作时间。李教练跟我说过一句话很关键“系统里每个操作都要有痕迹不然学员和教练扯皮的时候管理员没有任何依据。”这条表就是争议仲裁的凭证。4. 后端核心功能实现预约冲突校验与事务控制4.1 项目骨架搭建与分层设计后端项目的包结构我按照经典的三层架构来组织但结合了实际业务做了调整src/main/java/com/driving/school/ ├── common/ # 通用工具类、统一返回结果、异常处理 ├── config/ # 全局配置跨域、拦截器、MyBatis配置 ├── controller/ # 接口层接收前端请求返回JSON ├── service/ # 业务逻辑层核心规则判断在这里 ├── mapper/ # MyBatis Mapper接口 ├── entity/ # 数据库实体类 ├── dto/ # 数据传输对象接收前端参数、返回前端数据 └── enums/ # 枚举类角色、预约状态、科目等统一返回结果是我在common包里重点写的一个类。所有接口的返回结构保持一致——code表示业务码200成功、400参数错误、403无权限、500系统异常message返回提示信息data返回业务数据。这样做的好处是前端只要写一套拦截逻辑就能统一处理所有接口的错误提示。顺带说一个建议接口路径命名要有规律。我习惯用/api/student、/api/coach、/api/admin作为模块前缀后面接资源名和动作。比如学员预约就是POST /api/student/appointment管理员查预约列表就是GET /api/admin/appointments?page1size10。清晰的路径命名能让前后端联调时少很多沟通成本。4.2 登录鉴权JWT还是Session我最终选了JWTJSON Web Token方案。前后端分离架构下Session方案需要处理跨域携带Cookie的问题而且集群部署时Session共享要额外引入Redis增加部署复杂度。JWT的方案是用户登录成功后后端签发一个包含用户ID和角色的令牌前端把令牌存在localStorage里每次请求在请求头里带上Authorization: Bearer token后端拦截器校验令牌合法性后放行。这里有一个经验点要分享——令牌有效期怎么设设太短用户频繁重新登录体验差设太长安全风险高。我设置的是一天同时前端拦截401状态码后自动跳转登录页用户每天登录一次是可以接受的频率。角色的权限控制通过在拦截器中解析令牌里的角色字段来实现。管理员能访问/api/admin/**教练能访问/api/coach/**学员只能访问/api/student/**相互之间不能越权。我做了一个简单的注解RequireRole(ADMIN)加在Controller方法上配合拦截器判断比URL匹配更灵活。4.3 预约核心流程与防冲突校验逻辑预约功能是整个系统的核心它的流程是学员选择日期时段和教练后提交预约后端执行四步校验第一步校验学员状态——账号是否被禁用、当前科目是否有效 第二步校验学时余额——剩余学时是否大于0 第三步校验时间冲突——该教练、该车辆在目标时间段是否已有其他预约 第四步校验规则限制——不能预约当天的时段、不能提前超过7天预约。如果全部通过生成预约记录状态PENDING同时扣减学员剩余学时。如果教练在一小时内未确认系统自动取消该预约并退还学时。这里要特别注意第三步的SQL设计。两个时间段重叠的判断条件是startTime 目标结束时间 AND endTime 目标开始时间。这个条件很多人初学时会写反我最开始也写成了startTime 目标开始时间 AND endTime 目标结束时间结果漏掉了一种场景——已有的预约时间段完全包含在目标时间段内的情况。正确的条件是区间重叠判断而不是包含判断。整个预约操作需要保证原子性——校验和插入必须在一个事务里否则两个学员同时提交时可能都通过了校验导致冲突被写进去。我在Service层的方法上加Transactional注解同时利用数据库的唯一约束做兜底。因为MySQL默认的隔离级别是可重复读两个人同时提交同一教练同一时段的预约时可能存在并发问题单靠应用层的事务并不能完全避免。我加的兜底方案是建立唯一约束根据教练ID和开始时间不能重复这个业务规则来约束。有的业务场景允许一个教练同一时段带多个学员比如科目三路考培训那就不能简单做这个唯一约束。我的做法是针对“同一教练同一开始时间”建立唯一索引这个在驾校预约场景里是符合业务规则的——一个教练一次只能带一个学员。ALTER TABLE appointment ADD UNIQUE KEY uk_coach_start (coach_id, start_time);这行约束加上之后即使应用层的事务控制有漏洞数据库层面也会拒绝并发插入冲突记录。这是我在项目排错阶段感悟最深的一个设计——越靠近底层的数据约束越可靠。4.4 MyBatis动态SQL在管理后台的妙用管理员的预约记录查询页面我前面提到了筛选条件非常多。用MyBatis动态SQL实现后一个Mapper方法就能搞定所有组合select idselectAppointments resultTypecom.driving.school.entity.Appointment SELECT a.*, u.real_name AS studentName, c.real_name AS coachName, car.plate_number FROM appointment a LEFT JOIN sys_user u ON a.student_id u.id LEFT JOIN coach_info ci ON a.coach_id ci.id LEFT JOIN sys_user c ON ci.user_id c.id LEFT JOIN car_info car ON a.car_id car.id where if teststudentName ! null and studentName ! AND u.real_name LIKE CONCAT(%, #{studentName}, %) /if if testcoachName ! null and coachName ! AND c.real_name LIKE CONCAT(%, #{coachName}, %) /if if teststatus ! null and status ! AND a.status #{status} /if if teststartDate ! null AND DATE(a.start_time) gt; #{startDate} /if if testendDate ! null AND DATE(a.start_time) lt; #{endDate} /if /where ORDER BY a.create_time DESC LIMIT #{offset}, #{pageSize} /select写这个动态SQL时踩过一个低级坑——if teststatus ! null and status ! 里的字符串比较用单引号还是双引号容易弄混。MyBatis的OGNL表达式里 代表字符串字面量 会被当成OGNL表达式处理写反了直接报错。这个坑排查了我将近一个小时最后查出原因时真想拍自己脑袋。4.5 统计报表功能的实现思路管理员首页有一个数据看板要展示三个核心指标今日预约数、本周学时总数、各教练的带教学时排行。这些数据如果每次打开页面实时查数据库压力会比较大。但这个项目的数据量完全扛得住实时统计我直接用聚合SQL实现SELECT coach_id, COUNT(*) AS appointmentCount, SUM(TIMESTAMPDIFF(MINUTE, start_time, end_time) / 60) AS totalHours FROM appointment WHERE status COMPLETED AND start_time BETWEEN #{startDate} AND #{endDate} GROUP BY coach_id ORDER BY totalHours DESC用TIMESTAMPDIFF函数计算每个预约的分钟数再除以60换算成小时精度足够驾校使用。实测全表数据十万条时这个聚合SQL执行时间在200毫秒内完全不需要额外做缓存层。5. 前端页面实现Vue工程结构与预约流程的交互设计5.1 Vue项目的目录结构与路由设计前端工程我使用了Vue CLI 3创建的标准目录结构做了模块划分src/ ├── api/ # 接口请求封装按模块拆文件 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Vuex状态管理 ├── views/ # 页面组件 │ ├── login/ # 登录页 │ ├── admin/ # 管理员后台页面 │ ├── coach/ # 教练端页面 │ └── student/ # 学员端页面 └── utils/ # 工具函数axios实例封装等路由配置里用路由守卫做权限控制router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else { const role localStorage.getItem(role) if (to.meta.role to.meta.role ! role) { next(/login) } else { next() } } })前端权限控制的意义不在于安全安全还是靠后端而在于用户体验——管理员看不到学员的入口教练登录后不会迷路没有权限的人压根不知道有这个页面存在。5.2 学员预约页面的核心交互设计学员预约页面是整个系统里交互最复杂的页面。它的交互流程是第一步选日期。我用Element UI的el-date-picker组件设置disabled-date函数禁用今天之前的日期同时限制最多可选未来7天。这样用户在日期选择阶段就不会产生非法请求。第二步选教练。教练列表默认按综合评分排序每张教练卡片展示头像、姓名、教龄、带教学员数、评分和可预约标签。点击教练后下方的时间表格刷新为当前教练的可约时段。第三步选时段。时段表格展示一周内的可预约时间片。每个时间片有一个状态——可预约的显示可点击按钮已被预约的显示灰色不可点自己已预约的显示蓝色状态。这个状态数据是后端提供的前端根据预约时间段的集合自动计算每个格子是否可点。考生提交预约后前端弹窗提示“预约成功等待教练确认”同时在前端本地临时更新时段状态避免用户重复点击。第四步是前端Vuex里面维护了一个lessonCount状态预约成功或者取消后调用后端接口重新获取学员的剩余学时数据刷新顶部导航栏的数字展示。这里没有直接在前端做加减是因为后端可能还有其他扣减途径比如管理员手动调整直接重新拉数据能保证同步。5.3 教练端确认与提醒逻辑教练端页面的核心是一个今日预约列表按时间段排序显示学员姓名、科目、车牌号、时间段。教练可以点击确认或拒绝。拒绝时必须填写原因这个原因是给学员看的所以我做了一个前端校验——reason字段为空时禁止提交。这里有一个业务逻辑细节为什么预约是PENDING状态而不是直接确认因为驾校的业务流程里学员选的时间受教练实际安排影响——比如教练临时有事、车辆维修这些情况学员无法预知所以要给教练一个确认的环节。教练不确认的预约到了时间会被系统自动取消学时退回给学员。前端我用了一个定时轮询的方式刷新列表——每30秒请求一次接口。数据量小、轮询频率低不会给后端增加压力但用户体验上能实时感知到预约状态的变化。5.4 axios封装与错误处理的统一规范前端请求的封装看似简单实际容易出现各写各的问题。我统一在utils/request.js里封装了axios实例const service axios.create({ baseURL: /api, timeout: 10000 }) 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) { return res } else { Message.error(res.message || 业务处理失败) return Promise.reject(new Error(res.message)) } }, error { if (error.response error.response.status 401) { localStorage.clear() router.push(/login) } else { Message.error(网络异常请稍后重试) } return Promise.reject(error) } )统一封装的收益在联调阶段体现得最明显。后端接口还没完全写完时前端可以先按约定好的返回结构写页面后端补齐接口后直接就能跑通。因为返回结构是固定的所以前端页面代码不需要做任何改动只需要改后端实现即可。6. 常见问题与排错经验实战中踩过的坑和解决办法6.1 跨域问题的正确处理方式前后端分离开发阶段前端跑在8080端口后端跑在8081端口前端请求后端接口必然触发跨域。我在后端配置了一个全局CORS过滤器Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }这里有一个特别容易踩坑的地方——addAllowedOriginPattern(*)和setAllowCredentials(true)同时使用。如果用了addAllowedOrigin(*)不带Pattern的版本再开启Credentials浏览器会直接报错因为带凭证的跨域请求不允许通配符Origin。必须用addAllowedOriginPattern才可以。生产环境部署时我建议把跨域配置收窄——只允许前端域名访问后端接口不要用*通配。虽然CORS本身不算是安全漏洞但收窄总比敞开好。6.2 时间字段序列化的坑后端用LocalDateTime类型接收和返回时间数据时SpringBoot默认的JSON序列化结果是数组格式比如[2024,6,15,14,30,0]前端根本没法直接用。这个坑几乎每个人都会遇到。解决方法是配置全局的Jackson序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8或者在application.yml里配置spring.jackson.date-format后还需要在实体类的LocalDateTime字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解。实际经验是全局配置对Date类型生效对LocalDateTime类型不完全生效最稳妥的是在字段上显式加注解。前后端联调时还有一个时间相关的坑——时区偏移。前端浏览器的时区是东八区后端如果部署在UTC时区的服务器上前端显示的时间会少8小时。解决方法是后端统一使用Asia/Shanghai时区在application.yml里加spring.jackson.time-zone: GMT8。6.3 预约冲突的并发问题怎么彻底解决我前面说到了数据库唯一索引的兜底方案。这条索引加上以后并发预约同一教练同一时间段的请求在数据库层面会被拒绝后插入的那条会抛DuplicateKeyException。为了让前端展示一个友好的提示而不是一串异常堆栈我在全局异常处理器里单独捕获了这个异常ExceptionHandler(DuplicateKeyException.class) public Result? handleDuplicateKey(DuplicateKeyException e) { return Result.error(400, 该时段已被预约请选择其他时间); }这段代码虽然简单但我认为是整个预约系统最关键的兜底逻辑之一。可能有人会问为什么不彻底靠数据库约束就行了我的回答是数据库约束是最后一道防线应用层的事务控制是常规路径两道防线都踩住才能保证百分之百的话不多说至少在自己可控的范围里做到不出错。6.4 联调阶段最常见的几个问题排查记录整个开发过程里我记录了几个典型的联调问题每个都很折磨人但原因都很简单。第一个问题前端请求成功但拿不到数据。排查后发现是MyBatis的Mapper接口方法名和XML里的id不匹配按照规范两者必须完全一致。我把Mapper方法名改了XML里忘了改导致运行时提示Invalid bound statement (not found)。这类问题找起来很费劲最快的定位方式是看报错信息里的方法名和XML里的id对比。第二个问题前端提交预约后时间变成null。排查后发现是前端传参数时用了time字段名后端实体里对应的是startTime字段对不上MyBatis反射找不到对应的setter方法。这类问题属于前后端字段命名不一致解决方法是制定一份统一的接口文档明确每个字段的前端名称和后端名称。我在实际开发中是用Postman导出的接口文档同步给前端同学对照。第三个问题管理员编辑教练信息时报SQL语法错误。排查后发现是我在XML里写了UPDATE coach_info SET name #{name}但实际字段叫real_name修改后问题解决。这种低级错误偶尔会犯建议在写完SQL后先用数据库客户端工具裸跑一遍SQL确认没问题再放进程序里。第四个问题部署到服务器后功能全部正常但页面加载特别慢。排查后发现是前端静态资源没有压缩图片没有优化。处理方法是开启Nginx的gzip压缩并且把打包后的静态资源上传到CDN加载速度明显提升。6.5 数据库连接池配置的注意点SpringBoot默认使用HikariCP连接池配置文件中有一条参数特别重要——maximum-pool-size。我之前习惯性保持默认值10结果驾校管理员集中导入学员数据文件时数据库连接不够用接口超时。后来调整了这个参数并加了connection-timeoutspring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000为什么不直接调到100因为连接池的大小不是越大越好每个连接都会占用数据库资源反而可能导致数据库性能下降。20对于驾校系统这个量级已经非常充裕了。7. 功能扩展与优化方向这套系统还能往哪里走系统按原始需求做完后我实际运行了一段时间也思考了后续的迭代方向这里分享几个我判断有正向价值的方向。第一个方向是学员端的练车日历和进度追踪。目前学员只能看到剩余学时数但不知道自己科目二总共练了多少次、每次的教练和车辆是什么、自己的常见问题集中在哪些项目。要是能把每次练车的教练反馈和学员自评记录下来的话学员会有很直观的进步感知驾校也能在课程包调整时拿到数据依据。第二个方向是教练端的移动端适配。目前教练端是一个PC端网页教练在车上看的时候很不方便。实际上教练不太可能随身带电脑带手机却是必然。把教练端做成一个H5页面或者微信小程序内嵌页面教练在车边、休息室都能随时查看和确认预约体验会好很多。如果要动手做后端接口可以直接复用现有的一套只是前端布局要做响应式适配。第三个方向是车辆保养提醒和维修记录。驾校的车是高频使用的定期保养的时间和项目都应该有记录。现在的系统只能记录车辆的基本信息如果扩展一个维修保养的子模块管理员在车辆达到保养里程或日期时能收到提醒车辆的档案管理就完整了。第四个方向是学时数据的导入导出。驾校和交通管理部门之间有学时对接的需求现在系统里的统计报表只能在线查看如果能导出Excel或者CSV管理员的日常事务性工作会减少很多。前端可以用JavaScript做表格导出后端也可以直接用EasyExcel生成文件两种方案都是成熟的实现思路。第五个方向是消息通知渠道的扩展。现在系统的预约通知靠站内信或者短信接口在前端轮询如果换成微信模板消息触达率会高很多而且开发成本也比较低。驾校学员普遍使用微信这个方向我个人认为是性价比最高的扩展项。可以扩展的方向非常多但核心原则只有一个——不要为了加功能而加功能。每一个扩展都应该回应一个真实存在的业务痛点否则就是给系统增加复杂度。我在做这个项目时最深的体会就是一个驾校预约系统功能不需要多花哨把预约、确认、统计这三件事做到稳定就已经解决掉驾校80%的管理问题。剩下的20%才是锦上添花的扩展项。如果你正准备自己动手写一套类似的系统我的建议是先理解业务再动手写代码——花两天把驾校的运营流程理清楚比花两天选技术框架有用得多。项目开发过程中多跟校方负责人沟通拿到真实的业务流程和数据模型你写的代码才有落地的价值。而这套系统的每一行代码都是基于真实业务流程敲出来的。