多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于SpringBoot+Vue的驾校管理系统设计与部署全解析

基于SpringBoot+Vue的驾校管理系统设计与部署全解析 前前后后折腾了两周总算是把这套驾校管理系统从零到一完整跑通并部署到了服务器上。这是一套标准的前后端分离项目后端基于SpringBootMyBatis数据库用的MySQL前端采用Vue全家桶。整体业务覆盖了驾校日常运营里最常见的几个场景学员报名建档、教练信息管理、预约练车、考试安排与成绩录入再加上管理员后台必备的用户登录与权限控制。对正在准备毕业设计、或者刚学完SSM和Vue想找个完整项目练手的人来说这套系统的源码结构和部署流程都有直接的参考价值。这篇就把整个项目的设计思路、数据库表结构、核心代码实现、前后端联调方式以及从本地启动到服务器上线的完整过程全部摊开来讲。不光告诉你每一步怎么做还会解释为什么要这么做——很多坑不走到那一步是真的想不到。1. 项目整体设计与实现思路1.1 业务需求拆解驾校管理到底要管什么做系统之前必须先弄清楚业务角色和业务流程。驾校里日常打交道的主要有三类人管理员或者叫教务/超管、教练、学员。围绕这三类角色系统要解决的核心问题有几个学员报名后需要建档记录姓名、身份证号、电话、报名日期、报考车型后续所有操作都基于这份档案。教练需要维护自己的基本信息、准教车型、可预约的时间段管理员还要能调整教练的工作状态。学员练车之前要预约时间和教练预约不能冲突教练也要看到自己当天的安排。考试要分科目记录包括科目一、科目二、科目三、科目四的考试日期和成绩最终判断学员是否通过。管理员还要有数据统计能力比如每月新增报名人数、各科目通过率。这些需求落到功能模块上就是系统管理、学员管理、教练管理、车辆信息管理、预约管理、考试管理、统计报表七个大块。项目里没有塞一堆华而不实的功能每个模块都是驾校真实运营需要用到的这样无论是做毕业设计还是自己接私活二次开发业务逻辑都能经得起追问。1.2 技术选型为什么是这套组合技术栈选SpringBootVueMyBatisMySQL不是因为“大家都用”而是每一项都能对上需求。后端用SpringBoot图的是它的自动配置能力和生态成熟度。以前用SSH或者纯SpringMVC搭项目光配置文件就写一堆SpringBoot把内嵌Tomcat、自动装配这些事情全部接管了一个可运行的jar包丢到服务器上就能起服务。项目里需要做登录鉴权、拦截器、全局异常处理SpringBoot都有成熟方案。MyBatis在这个项目里比JPA更顺手。驾校管理系统的查询条件变化多比如学员列表可能要按姓名、状态、报名日期范围组合筛选这种场景用SQL非常直观可控。MyBatis的动态SQL可以灵活拼条件同时SQL是手写的索引怎么走完全可以自己掌控。MySQL则是国内中小项目应用最广的数据库运维资料多服务器上装一个用起来也不挑配置。我承认MyBatis-Plus开发效率确实高但既然标题写的是MyBatis那就按原生MyBatis讲清楚这样你对SqlSessionFactory、Mapper绑定这些底层机制反而能理解得更透。前端用Vue核心原因是组件化和开发效率。配合Element UI做后台界面表格、表单、分页、弹窗这些后台管理系统的高频组件都不用自己从零写。项目采用前后端分离等于把前端页面和后端接口彻底解耦前端可以独立开发调试后端接口也可以单独测试部署的时候前端跑Nginx、后端跑Java进程互不干扰。1.3 整体目录结构与模块划分后端包结构我按职责做了分层没有搞复杂的微服务架构单体应用对这类系统完全够用controller接收请求做参数初步校验调用serviceservice业务逻辑比如预约冲突判断、学员状态流转mapper数据访问层接口mapper.xmlMyBatis的SQL映射文件entity数据库表对应的实体类dto前端传来的参数对象跟实体类分离common统一返回结果、异常处理、常量定义configWeb配置、拦截器注册、CORS配置等utilJwtUtil这类工具类前端结构则是标准的Vue CLI工程views按页面划分比如Login.vue、StudentList.vue、Appointment.vue、ExamManage.vuerouter集中管理路由和前置守卫api目录下集中存放所有axios请求utils里封装request.js。这种划分方式看代码不累每个人负责一个模块也很清晰。2. 数据库设计与核心业务模块解析2.1 表结构设计与核心SQL数据库我总共设计了六张核心表系统用户表sys_user、学员表student、教练表coach、车辆表car、预约表appointment、考试记录表exam。如果按这个项目再扩展收费功能也可以加一张费用记录表但核心业务流程这几张就够了。学员表是系统的中心字段设计上有几个关键点。姓名、身份证号、手机号是基础字段另外用一个status字段表示学员当前状态0表示在读、1表示已结业、2表示已退学。身份证号要做唯一索引因为一个学员只能有一条档案。报名时间用datetime类型。预约表需要同时关联学员和教练所以用student_id和coach_id两个外键。预约日期和时段要放在一起做唯一约束防止同一个人在同一时间段重复预约。考试表则用student_id关联学员subject字段表示科目score字段记录成绩result标记是否通过。建表的时候注意两点第一字符集一定要用utf8mb4不要用utf8不然学员名字里有生僻字或者表情符号就会乱码第二ID字段统一用BIGINT自增以后数据量大了不用换表结构。这里贴出学员表和预约表的建表SQL作为参考CREATE TABLE student ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(32) NOT NULL COMMENT 学员姓名, id_card varchar(18) NOT NULL COMMENT 身份证号, phone varchar(11) DEFAULT NULL COMMENT 手机号, car_type varchar(8) DEFAULT C1 COMMENT 报考车型, status tinyint(4) DEFAULT 0 COMMENT 0在读 1结业 2退学, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_id_card (id_card) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学员信息表; CREATE TABLE appointment ( id bigint(20) NOT NULL AUTO_INCREMENT, student_id bigint(20) NOT NULL, coach_id bigint(20) NOT NULL, appoint_date date NOT NULL COMMENT 预约日期, time_slot tinyint(4) NOT NULL COMMENT 时段 1上午 2下午 3晚上, status tinyint(4) DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, remark varchar(200) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_student_slot (student_id, appoint_date, time_slot), KEY idx_coach_date (coach_id, appoint_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT练车预约表;2.2 登录权限模块与角色体系登录权限是我最早动手做的模块因为所有页面都需要先过了登录这一关。项目里没有引入Spring Security原因很简单这套系统的权限需求只是“管理员能进管理后台操作教练能看自己的排班和预约学员能提交预约申请”不需要复杂的OAuth或者细粒度的数据权限。用JWT配合拦截器已经能非常好地解决鉴权问题。用户表sys_user里设计一个role字段后台就用这个字段区分用户类型。密码不是明文存储BCrypt加密之后存进数据库这个很重要——真实项目里密码绝对不能明文落库否则数据库只要泄露一次所有账号就全完了。JWT的token在登录成功时生成包含用户ID、用户名、角色三个核心信息签名密钥维护在配置文件里。2.3 预约练车与考试流程这两块硬骨头预约练车的核心问题是怎么防止冲突。一个教练同一时间只能带一个学员一个学员同一时间也只能有一个预约所以插入预约数据之前先查一遍有没有冲突。这里我用的办法是在appointment表建唯一索引student_id, appoint_date, time_slot数据库层面直接杜绝重复预约。代码里先做一次查询校验给用户友好提示数据库唯一约束兜底双保险。教练那边的冲突则通过索引idx_coach_date来控制教练某天的某个时段一旦已经被预约再次插入就无法成功。考试流程的设计思路是科目状态推进。每个学员可以有多条考试记录按subject区分科目。管理员录入成绩时更新考试成绩表和学员的当前进度字段。科目一和科目四是理论考试科目二科目三是实操系统的逻辑统一用subject字段区分不做复杂处理。3. 后端核心实现SpringBootMyBatis实战3.1 工程初始化与核心配置创建SpringBoot工程我用的是Spring InitializrJava版本用的8SpringBoot版本选了2.7.x——这里特别提醒一句别一上来就用SpringBoot 3.x因为它要求JDK 17而且很多第三方组件的兼容性还是2.x更稳等这个项目跑通了你再升级不迟。pom.xml需要引入的核心依赖如下我用注释标明了各自的作用dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependencyapplication.yml配置里最值得讲的是MyBatis的驼峰映射。数据库字段是下划线命名create_timeJava实体是驼峰命名createTime如果不开启map-underscore-to-camel-case查出来的createTime就是null。很多人遇到实体字段全是null的第一反应是SQL写错了其实往往是这行配置没加。还有JDBC连接串的参数我直接给出了完整的配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/driving_school?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.drivingschool.entity configuration: map-underscore-to-camel-case: true jwt: secret: your-secret-key-please-change-me expire-days: 73.2 统一返回结果与全局异常处理所有接口返回值我都封装成了统一的Result对象结构是code、message、data三个字段。code为200表示成功其他值表示各种业务异常。这么做的好处是前端可以统一处理响应逻辑不用每个接口单独判断。无论成功返回数据还是抛出异常前端axios拦截器拿到的都是结构一致的JSON处理逻辑可以收敛到一处。全局异常处理用的是RestControllerAdvice配合ExceptionHandler。捕获异常之后返回Result对象而不是直接把异常堆栈抛给前端。业务异常我自定义了一个BizException参数校验失败、数据不存在、业务冲突等场景统一抛出由全局处理器转成规范的错误响应。这个设计在做毕业设计答辩的时候很加分因为它直接体现了一个开发者对工程化异常处理的理解。3.3 JWT登录与拦截器实现JWT的工具有两个方法生成token和解析token。生成token时把用户ID和角色放进Claims失效时间按配置的天数计算。登录成功后前端拿到token存在localStorage里之后的每一次请求都在请求头里带上Authorization字段。后端拦截器继承HandlerInterceptorAdapter重写preHandle方法。从请求头取出token解析成功就把用户信息放入ThreadLocal方便后续的controller和service里直接获取当前登录用户。解析失败或没有带token直接返回401状态码让前端跳到登录页。拦截器注册时要注意配置放行路径。登录接口、静态资源、OPTIONS预检请求必须放行否则前端跨域预检直接就被拦截了用户根本登不进去。我这里贴出WebConfig的核心片段Configuration public class WebConfig implements WebMvcConfigurer { Autowired private JwtInterceptor jwtInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/**) .excludePathPatterns( /api/auth/login, /api/auth/register, /error, /static/** ); } }3.4 Mapper层与动态SQL实践MyBatis的Mapper层我讲的细一点因为这是整个项目里最容易出问题也最体现功力的部分。每个Mapper接口对应一个XML文件XML的namespace必须和接口全限定名一致方法名必须和SQL语句的id一致这两点错了直接启动报绑定异常。学员列表查询是动态SQL的典型场景。前端传入name、status、beginTime、endTime这些可选条件后端用MyBatis的where加if标签动态拼接SQL不需要为每种组合单独写一个方法。这里有一个细节模糊查询时不要直接写%开头的条件而是在SQL里用CONCAT拼select idselectStudentList resultTypecom.drivingschool.entity.Student SELECT * FROM student where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if teststatus ! null AND status #{status} /if if testbeginTime ! null AND create_time gt; #{beginTime} /if if testendTime ! null AND create_time lt; #{endTime} /if /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select动态SQL里还有一个点必须强调永远用#{}而不是${}。${}是字符串拼接用户输入的内容直接嵌进SQL里等于把SQL注入的刀递到别人手里。而#{}是预编译参数占位符无论输入什么内容都被当成参数值处理。项目里除了order by这种没法预编译的场景外全部用#{}。分页这里我没有引入PageHelper插件直接用的LIMIT偏移量和每页条数。这个思路在数据量不大的场景下没有任何问题代码也简单。如果后续数据量过十万再考虑PageHelper或者基于游标的分页方案。4. 前端Vue搭建与前后端接口联调4.1 Vue工程结构与基础配置前端我用Vue CLI创建默认选Vue 2.6版本配合Element UI这个组合的文档最齐全。注意如果你用Vue 3Element UI要换成Element Plus很多组件的写法有变化。创建完成后通过npm安装element-ui、axios、vue-router、vuex四个依赖。开发阶段最大的痛点是跨域。前端开发服务器跑在8081端口后端接口跑在8080端口浏览器会直接拦截跨域请求。解决办法有后端加CORS配置也有前端配置开发代理。我强烈建议开发环境用vue.config.js的proxy方案因为生产环境有Nginx统一处理跨域开发环境用代理模拟的路径规则和生产环境一致不会出现两套逻辑const { defineConfig } require(vue/cli-service) module.exports defineConfig({ devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })4.2 Axios二次封装与登录状态管理axios如果不做封装每个页面都要重复处理加载状态、错误提示、token注入代码会非常散。我在src/utils/request.js里统一创建了axios实例设置了baseURL、超时时间、请求拦截器和响应拦截器。请求拦截器把localStorage里的token取出来放进headers.Authorization字段。响应拦截器统一处理状态码200直接返回data401跳到登录页并清空本地登录信息其他错误码弹出提示。这样页面里写请求时就非常干净只需要关心业务数据。登录页面拿到token后除了存localStorage我还会存一份用户信息到Vuex里防止刷新页面后用户信息丢失。菜单的显示隐藏也根据用户角色动态控制admin看到全部菜单教练看到工作台和排班页面学员看到预约中心和考试信息页面。4.3 路由守卫与菜单权限控制路由守卫是这个项目里前端必须理解透的概念。项目配置了一个全局前置守卫router.beforeEach每次路由跳转前检查localStorage里有没有token。没有token且去往的不是登录页就强制跳转到登录页有token但访问的是登录页就跳回首页。这个逻辑放在路由配置里统一处理而不是每个页面单独判断代码量省下一大截。菜单的动态控制我采用的是前端根据角色字段v-if判断的方案。点击左侧菜单跳转到真实路径后端对每个接口也会校验角色即使有人手动输入URL跳转也无法绕过权限拿到数据。前后端两层校验配合起来才算完整。4.4 学员管理页与预约看板实战学员管理页是整个项目前端代码的样板模块。页面结构是搜索区、表格区、分页区、新增编辑弹窗。搜索区用el-form配合el-input和el-select搜索按钮触发query方法重置页面到第一页。表格区用el-table渲染学员列表状态列用el-tag根据不同状态值显示不同颜色——这个细节看起来很基础但用户看到彩色状态标签的体验感和纯文本完全不同。预约页面比较复杂。学员选择教练和时间段之前表格会先展示该教练未来一周的可约时段。后端封装了一个接口接收coach_id和日期范围返回已近满的时间段列表。前端把不可选时段在时间选择器里禁用掉从入口避免二次点击。这个交互设计很直接不需要用户先提交再报错。写前端时反复提醒自己页面能简单尽量简单但用户提交的每一项数据都要做必填校验。学员姓名为空不允许提交手机号格式不正确不允许提交预约日期不能是过去时间。前端校验拦截一部分问题后端的参数校验再兜底千万不能只做一头。5. 部署上线全流程从源码到服务器5.1 本地环境准备与数据库导入本地把项目跑起来的条件并不苛刻JDK 1.8、Maven 3.6以上、MySQL 8.0、Node 14以上。数据库部分我准备了一个init.sql脚本包含建库建表语句和默认管理员账号数据。用数据库客户端执行整个脚本之后driving_school库就建好了。如果用命令行数据库导入也完全没问题mysql -u root -p init.sql前端依赖安装时如果网络状况不好npm install可能会卡住。解决办法是配置registry为国内镜像源再执行安装命令。注意改完镜像源之后最好把node_modules整个目录删掉重新安装不然容易出现一半依赖来自原源一半来自新源的情况。5.2 生产构建与jar包运行前端打包执行npm run build产物在dist目录。构建完成后从dist目录结构里能看到static文件夹和index.html这个目录就是后续Nginx要指向的静态资源根目录。后端打包用Maven打包命令-DskipTests跳过测试以免构建过程被环境问题卡住。打包完成后target目录下会生成一个可运行的jar包。这个jar包内部内嵌了Tomcat所以服务器上只要有JDK环境就能直接运行mvn clean package -DskipTests java -jar driving-school-1.0.0.jar --spring.profiles.activeprod后台运行我推荐用systemd管理Java进程配置一个service文件启动、停止、开机自启都方便。不要直接用nohup加方式挂在后台一旦进程挂了没人知道systemd至少能在进程崩溃后直接拉起并记录日志。5.3 Nginx反向代理与前端静态托管生产环境前端页面放在Nginx里后端接口通过location /api代理到Java服务端口。这样一个域名加一个端口搞定全部访问浏览器请求的都是同源的/api路径也就不会有跨域问题。Nginx配置如下这里有几个细节要重点说明server { listen 80; server_name your-domain.com; gzip on; gzip_types text/plain application/javascript text/css application/json; root /opt/driving-school/dist; index index.html; location / { try_files $uri $uri/ /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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }location /的try_files配置是历史模式路由下必须写的否则用户刷新某个子页面时会得到404。location /api/的proxy_pass后面没有带路径所以原来的/api会被完整传给后端。如果你的后端接口路径不想要/api前缀就把proxy_pass写成http://127.0.0.1:8080/这样前端请求/api/auth/login会被转发到http://127.0.0.1:8080/auth/login路径里的/api会被替换掉。这段逻辑建议操作的时候打开Nginx的access.log观察实际转发请求一清二楚。5.4 服务体系部署的额外考量如果服务器内存比较紧张Java进程启动时可以通过JVM参数限制内存使用比如-Xms256m -Xmx512m。MySQL数据库在低配服务器上也可以把innodb_buffer_pool_size调小一些。另外数据库连接串里的serverTimezone必须和服务器时区保持一致服务器时区不对会导致查出来的时间差八个小时很多人部署后时间不对都是栽在这上面。服务器系统防火墙的安全策略需要把80端口和数据库端口放行但数据库的3306端口建议只对应用服务器内网开放别暴露在公网上。生产中数据安全是第一位的千万不要图方便开着公网访问数据库。6. 常见问题排查与避坑实录6.1 后端启动失败与数据库连接类错误项目跑起来首先遇到的大部分问题都出在数据库连接上。报错信息里最常见的是时区和SSL相关。加入serverTimezoneAsia/Shanghai能解决时区报错useSSLfalse能解除SSL警告。还有MySQL 8的驱动默认会要求allowPublicKeyRetrieval这个参数也最好显式加上否则会报Public Key Retrieval is not allowed。端口被占用的情况也很常见使用SpringBoot内置Tomcat报端口占用时先看下是不是有其他Java进程占用了8080直接改配置里的server.port即可。数据库导入SQL脚本时报语法错误十有八九是用了不兼容的问价版本。用MySQL 8的客户端连接MySQL 5.7的服务器或者反过来都会出现莫名其妙的语法错误。建议本地和服务器都用MySQL 8版本保持一致省掉研究兼容性的时间。6.2 前端启动与联调中的跨域问题前端开发环境接口报跨域首先确认vue.config.js修改后是否重启了开发服务器。proxy配置不是热更新的改完代理之后必须CtrlC再重新npm run serve否则怎么刷新都没效果。其次确认请求路径是不是以 /api 开头代理规则是按路径前缀匹配的路径不对代理不生效。生产环境联调时如果前端能打开但接口全部请求失败重点看Nginx的error.log。502 Bad Gateway说明Nginx连不上后端端口先确认Java进程还活着再确认proxy_pass端口和Java进程端口一致。404的话检查location /api的路径拼接规则看转发后路径是否和后端Controller的映射路径能对应上。6.3 MyBatis绑定异常与其他日常坑启动项目时出现Invalid bound statement (not found)错误几乎都是这三个原因XML文件的namespace跟Mapper接口的全限定名不一致接口里的方法名跟XML里的statement id不一致或者XML文件没有被打包到classes目录。第三个原因在Maven项目里最常见解决方法是确认pom.xml里把src/main/resources下的xml文件包含了或者在打包时排除资源文件的配置里把xml排除规则去掉。Mapper层查询出的实体字段全是null检查map-underscore-to-camel-case是否开启。想调试SQL语句在application.yml里加上日志配置打印SQL日志开发时非常有用。6.4 部署后的低配环境优化部署到低配服务器后首次访问页面可能会明显卡顿。前端资源加载慢的问题用Nginx开启gzip压缩能有效缓解。如果还有富余的时间建议把Nginx的静态资源缓存也配置上index.html不缓存但JS和CSS等带hash的文件可以长缓存。后端接口响应慢排查下是数据库查询慢还是Java处理慢数据库慢看SQL走了索引没有用EXPLAIN验证一下。前后端分离项目部署完毕后最后检查一件事把后端的application.yml里所有调试开关关掉SQL日志关掉错误堆栈不要直接返回给前端。这些设置不调整相当于把自己系统的内部结构暴露给用户。最后分享一个我自己的使用体会做这套项目的时候我一开始把数据库外键约束建得特别全结果后面删除学员时得手动清理预约和考试记录逻辑绕来绕去删起来提心吊胆。改成逻辑删除is_deleted字段之后才舒服很多。另外如果你打算把这套系统拿去做毕业设计答辩或者演示建议提前在数据库里预置好一批演示数据启动完打开页面就有内容看这个细节在验收环节特别加分。
返回列表