
看到这个标题点进来的同学应该都是奔着“毕设/课设”来的。SpringBootVue 汽车票网上预订系统管理平台这个题目在学校里出现频率非常高因为它业务链路完整、技术栈主流、演示效果直观关键还不会像电商系统那样堆砌一堆营销功能做起来有边界感。说白了这是一套“标准到不能再标准”的前后端分离项目但正因为它标准才值得把它背后每一个设计细节都吃透。这篇文章我就按自己实际做过这类项目的思路从选型到数据库再到前后端实现和源码部署把这个系统彻底拆干净顺带把那些“SpringBoot版本太高”“MySQL SSL连接错误”“Vue打包放不进SpringBoot”之类的坑一次性填平。1. 项目定位与技术选型先搞清楚这套系统要解决什么问题1.1 为什么汽车票预订系统适合当毕设和课设很多人选题目有个误区上来就想要“独特”“新颖”结果要么功能太虚要么数据模型复杂到答辩时自己都讲不清楚。汽车票预订系统不一样它的核心业务非常实在旅客要查班次、买车票车站要管车辆、排班、看订单。这本质上就是一个“信息管理 在线交易”的组合既能演示普通用户的完整操作流又能体现管理员的维护场景工作量和难度刚好卡在课设要求的中上位。更关键的是这个方向容易扩展。今天你做一个基础版拿到的表结构是用户表、班次表、订单表明天你想升级可以加坐位图、支付回调、短信验证码每一步都有明确的业务含义。我见过不少同学用“汽车票系统”做底子最终演化出带数据分析、座位锁定的完整项目这在写简历时是很自然的一句话“独立设计并实现一个包含座位库存管理、订单状态流转的业务系统。”所以别嫌题目普通能把普通题目做扎实本身就是能力。1.2 技术选型背后的理由SpringBootVueMySQL为什么是经典组合这个组合几乎成了国内Java后端项目的默认配置不是没有原因的。SpringBoot最大的价值是“开箱即用”内嵌Tomcat、自动装配、自带健康检查学生不需要去折腾复杂的SSM配置文件把精力放在业务逻辑上。Vue则胜在轻量和组件化前端页面可以拆成组件维护配合Element UI这类组件库两天搭出一个像样的管理后台完全不成问题。MySQL不用多说关系型数据库能很好地表达用户、班次、订单之间的关联而且学校机房、个人电脑普遍都装了。还有一个隐藏原因岗位匹配。Java开发岗位看简历最常出现的技术组合就是SpringBootMySQL前端能写Vue的同学更是加分。用这套系统做项目面试时聊技术栈完全不虚不会像纯JSP项目一样给人留下“过时”的印象。1.3 角色与功能模块用户端、后台管理端各管什么我在设计功能时会先把角色理清楚因为角色直接决定页面和接口的数量。这套系统通常分三类角色普通用户、管理员、超级管理员很多毕设版本只分用户和管理员足够了。角色核心功能模块关键操作普通用户注册登录、班次查询、在线购票、我的订单、个人资料查票、下单、取消订单、退票管理员用户管理、车辆管理、班次管理、站点管理、订单管理新增/编辑/停用班次、查看订单、处理退票超级管理员管理员账号维护、基础数据统计分配账号、查看报表用户端的核心是“查票—购票—订单”这条链路管理端的核心是“资源维护—排班—订单监管”。这两块功能不要混在一起前端路由要分模块后端接口也要分前缀比如/api/user/**和/api/admin/**这样权限控制才有清晰边界。2. 数据库设计把用户、班次、订单串起来的基础2.1 核心数据表从实体关系开始拆数据库是整个系统的地基也是答辩时老师最喜欢追问的地方。汽车票系统的实体关系并不复杂我建议先画一张草稿用户买票班次由车辆和站点组成。最简单的设计只需要五张表用户表、站点表、车辆表、班次表、订单表。数据表主要字段说明userid、username、password、real_name、phone、id_card、role存用户和管理员用role字段区分stationid、station_name、city、address站点信息比如“杭州站”“宁波站”busid、plate_number、bus_type、seat_count车辆信息座位数会直接影响余票scheduleid、bus_id、depart_station_id、arrive_station_id、depart_time、arrive_time、ticket_price、remaining_seats、status班次表核心业务表ordersid、order_no、user_id、schedule_id、passenger_name、passenger_id_card、seat_number、order_status、create_time订单表记录每笔交易有些版本还会把订单和乘客拆开支持一笔订单多个乘客但毕设阶段建议一个订单对应一个班次下的一个乘客逻辑更清晰也够用。班次表里的depart_station_id和arrive_station_id都指向站点表的id这就是典型的一对多外键关系一个班次属于一辆车所以bus_id外键指向车辆表。2.2 关键状态与约束订单状态、余票字段怎么设计表结构谁都会建但状态字段设计得好不好直接反映你有没有真实业务经验。订单表我建议必加order_status字段取值范围固定为待支付、已支付、已出票、已取消、已退票。不要用整数 0、1、2 代替直接用字符串可读性好查询也方便。如果是课设周期短可以把“待支付”和“已支付”合并只保留“已出票”和“已取消”两个状态逻辑更简单。余票字段我强烈推荐使用remaining_seats而不是每次都用total_seats - 已售数量去算。后一种做法你的订单表必须严格准确一旦出现异常订单就会导致余票永远错误。直接在班次表维护一个余票数每次下单成功后减一取票时判断“余票大于0”这是多数业务系统实际采用的做法。注意状态字段和余票字段都建议加索引。用户查自己的订单、管理员筛订单状态这两个查询频率极高。2.3 索引、事务与一个容易被忽略的并发问题订单表必须有唯一索引order_no因为打印出来的订单号如果重复演示时会非常尴尬。自增ID只适合内部使用不适合直接暴露给用户这也是为什么我要单独生成order_no。它可以用“时间戳随机数用户ID尾号”拼接保证唯一且可读。真正体现业务水平的是下单时的并发控制。汽车票不像普通商品余票是有限的两个用户同时抢最后一张票数据库层要保证只有一个成功。我常用的写法不是查一遍再改一次而是直接在SQL层面做条件更新UPDATE schedule SET remaining_seats remaining_seats - 1 WHERE id ? AND remaining_seats 0。如果影响行数为0说明余票不足直接提示“票已售罄”。这个方法不需要加锁也不需要Redis对于毕设系统完全够用而且面试时说出来非常加分。3. 后端SpringBoot实现登录、查票、下单这类接口怎么写3.1 工程结构与关键配置让代码看起来像企业项目很多课程设计的通病是代码全堆在Controller里一个类几百行答辩时一问Service层是什么都答不上来。我建议按标准四层结构来拆Controller接收参数、Service处理业务、Mapper操作数据库、Entity映射表。即使你用的是MyBatis-Plus也可以遵循这个分层代码一多你就会感谢当初的结构。pom.xml里核心依赖把这些加上dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/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里最容易踩坑的是数据库连接串。MySQL 8.0默认开了SSL本地开发时经常报“SSL connection error”加上useSSLfalse即可还要处理时区问题否则控制台会报 “The server time zone value” 错误server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/bus_ticket?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里多说一句“版本太高”是很常见的坑SpringBoot 3.0开始强制要求JDK17而很多同学的电脑里是JDK8。如果你用的是SpringBoot 2.7.x配合JDK8完全没问题如果项目pom里已经写了3.x版本你又不想升级JDK那请直接降低SpringBoot版本或者安装JDK17。3.2 用户登录与权限控制JWT方案从请求到拦截完整走一遍毕设系统的权限控制不需要做得很花哨用JWT把“登录状态”和“角色”传下去就够了。登录接口流程是前端把用户名密码传过来后端查询用户表校验密码通过后生成一个token把用户ID和角色塞进去设置过期时间返回给前端。前端拿到token后存在localStorage里每次请求都带上Authorization请求头。后端用一个拦截器统一校验Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } }管理员接口再加一层角色判断如果当前请求路径以/api/admin开头而token里的role不是管理员则直接返回403。注册接口、登录接口、班次查询接口要放行否则用户连登录都进不来。这个逻辑请务必在拦截器里认真写因为答辩时老师一定会问“你这个系统的权限是怎么控制的”。3.3 核心订票接口一个事务把余票和订单一起搞定购票是整个系统的核心业务我建议单独写一个purchaseTicket方法并加上Transactional事务注解。逻辑分几步根据前端传的scheduleId查班次判断班次状态和发车时间是否有效更新余票使用前面说的UPDATE ... WHERE remaining_seats 0生成订单号插入订单记录最后返回订单详情给前端。Transactional public Order purchaseTicket(Long userId, Long scheduleId, String passengerName, String passengerIdCard) { Schedule schedule scheduleMapper.selectById(scheduleId); if (schedule null || schedule.getStatus() 0) { throw new BusinessException(班次不存在或已停运); } if (schedule.getDepartTime().before(new Date())) { throw new BusinessException(该班次已发车); } int rows scheduleMapper.reduceRemainingSeats(scheduleId); if (rows 0) { throw new BusinessException(余票不足购票失败); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setScheduleId(scheduleId); order.setPassengerName(passengerName); order.setPassengerIdCard(passengerIdCard); order.setOrderStatus(已支付); orderMapper.insert(order); return order; }这里有三个细节值得注意。第一余票更新和订单插入必须在同一个事务里否则会出现“票扣了但订单没生成”或“订单生成了但票没扣”的不一致状态。第二reduceRemainingSeats的SQL是核心不要先select再update两条语句之间其他请求可能已经把票买走了。第三订单状态我没设“待支付”因为课设系统一般不接真实支付直接默认“已支付”可以少一个状态流转。如果有人问为什么不接支付你回答“预留了支付接口当前演示采用模拟支付”这完全说得通。4. 前端Vue实现页面、路由、请求封装与联调细节4.1 页面结构与技术选型Vue2还是Vue3前端选型取决于你的后端版本和Node环境。如果后端是SpringBoot 2.x我建议用Vue2 Element UI这套组合最成熟网上的解决办法最多踩坑成本最低如果你愿意折腾用Vue3 Element Plus也没问题但要注意Element Plus的组件用法和Vue2有差异。对课设来说稳定性优先选你熟悉的版本比选新版本重要得多。页面层面我建议拆成两个区域前台用户端和后台管理端。前台页面包括登录注册页、班次查询页、下单页、我的订单页后台管理页包括数据概览、用户管理、车辆管理、班次管理、订单管理。目录结构上把页面放在src/views下按user和admin分文件夹组件放到src/components这样逻辑清晰写路由时也方便。4.2 路由与请求封装登录状态怎么贯穿全站前端请求封装直接影响后期开发效率。我习惯在src/utils/request.js里创建一个axios实例设置基础路径为/api然后在请求拦截器里自动加上tokenimport axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )路由守卫也很重要。在Vue Router里加一个全局前置守卫判断目标页面是否需要登录。如果用户没登录就访问“我的订单”“后台管理”直接跳转到登录页。后台管理路由还要额外判断本地存的角色字段不是管理员就提示无权限。这个逻辑能挡住大部分“没登录却能打开页面”的演示事故。经验之谈路由模式建议使用hash模式。history模式虽然地址栏好看但打包后放到SpringBoot里运行时刷新页面容易404还需要额外配置转发规则。课设阶段别折腾这个。4.3 前后端联调跨域、接口对不上、参数类型不匹配本地开发时前端默认跑在http://localhost:3000后端跑在http://localhost:8080端口不同就一定有跨域问题。最简单的方案是后端写一个全局CORS配置类允许所有来源访问更贴近企业做法的是在前端vue.config.js里配置devServer转发把/api开头的请求转发到后端端口。前端开发环境下请求地址写相对路径/api/user/login这样代码里不用写死IP后期部署到服务器也不会因为地址变了而全部改写。联调阶段最容易遇到三类问题后端返回的日期格式前端解析不了、后端返回的字段名和前端定义的不一致、接口路径大小写对不上。我的建议是让后端先设计一个统一的返回对象Result所有接口都返回{ code, message, data }前端在axios的响应拦截器里统一判断code是否为200而不是每个页面都写一遍成功失败逻辑。日期格式则在application.yml里全局配置好前端就不会收到“2025-06-01T12:00:00.00000:00”这种奇怪的东西。5. 源码部署实录从环境检查到第一次启动成功5.1 版本匹配先看SpringBoot版本再选JDK很多同学拿到源码后第一件事就是双击启动结果一屏红色报错。大多数问题不是源码有问题而是本机环境版本不匹配。这里列一个我实测下来比较稳的组合组件推荐版本说明JDK8 或 17必须看SpringBoot版本2.x用83.x用17Maven3.6.3 或 3.8.x别用太新3.9有时和旧项目不兼容MySQL5.7 或 8.05.7.44安装过程相对简单8.0记得改密码规则Node.js14 或 16配Vue2最稳18以上容易出现依赖兼容问题IDEIDEA 2021新版本IDEA启动配置方式类似改端口看application.yml拿到一个陌生源码时我建议先看pom.xml里的SpringBoot parent版本再查本机java版本两个对齐后再启动后端。如果SpringBoot版本太高比如3.2.x而你只有JDK8你会看到类似“UnsupportedClassVersionError”的报错这就是热词“springboot版本太高”背后最典型的场景。5.2 本地启动五步走从导入SQL到前后端跑通把一套源码跑通我的固定顺序是这样的你照着做一般不会乱。第一步先把sql目录下的建库脚本在MySQL里执行一遍。执行前确认你用的是哪个数据库版本脚本里如果有ENGINEInnoDB DEFAULT CHARSETutf8mb4这类语句基本问题不大。如果脚本里带了外键导入时要注意表顺序先建父表再建子表。第二步修改后端application.yml里的数据库账号密码以及MySQL 8.0和5.7的连接串差异。用5.7的话驱动可以保持com.mysql.cj.jdbc.Driver但连接串最好同样加上useSSLfalseserverTimezoneAsia/Shanghai能省很多莫名其妙的报错。第三步启动后端。在IDEA里直接运行主类观察控制台日志看到 “Started Application in x.xxx seconds” 就是成功了。我习惯启动后先用浏览器调一下/api/health或Swagger地址确认接口层已经工作再进入前端环节。第四步前端项目打开终端先执行npm install如果速度很慢或者报错就用npm config set registry https://registry.npmmirror.com切换镜像源。安装成功后执行npm run dev本地访问前端地址用测试账号登录。第五步如果你想打包成一个单机可运行的项目在前端项目执行npm run build会生成dist目录把dist里所有文件复制到后端src/main/resources/static目录下然后重新打包后端。这样启动后端后直接在8080端口访问就能看到前端页面不再需要单独起前端服务。这就是“vue打包放进springboot中”的做法。5.3 常见问题速查表MySQL、Vue打包、端口占用一次说清我汇总一下做这类项目最高频的报错和解决办法很多热搜词对应的就是下面这些场景。现象原因解决方案MySQL SSL connection errorMySQL 8.0默认启用SSL本地连接不匹配连接串加useSSLfalseThe server time zone valueMySQL时区与JVM不一致连接串加serverTimezoneAsia/ShanghaiUnsupportedClassVersionErrorJDK版本低于SpringBoot要求降SpringBoot版本或装对应JDK端口被占用Tomcat启动失败8080被其他进程占用改server.port或找到占用进程结束Maven依赖下载慢/失败默认中央仓库不稳在settings.xml配置阿里云镜像npm install报错ERESOLVENode版本过高/依赖冲突降低Node版本清npm缓存重装Vue打包后访问空白静态资源路径不对publicPath: ./或改hash模式刷新页面404history模式路由在Tomcat里无匹配改hash模式或配置路径转发跨域报错CORS前后端端口不同后端配置CORS类或前端devServer转发后端接口返回401但登录接口正常拦截器放行规则没写好检查拦截器排除路径放行/api/user/login等这里面有一个容易被忽视的坑前端请求的接口路径如果与后端RequestMapping斜杠不一致比如后端是/api/user/login前端请求了/api/users/login会直接404。联调时一定要先用Postman或Apifox把后端接口测通再连前端避免两头猜。6. 从“能交差”到“有亮点”扩展方向与答辩准备6.1 低成本加分扩展选座、验证码、统计报表基础版本做完后如果想在答辩时压过同组同学可以做几个“低成本高感知”的扩展。第一个是座位图选择在班次表里加一个已售座位数组字段前端渲染一个网格已售座位置灰用户选座后下单整个演示效果瞬间就不像课设了。第二个是图形验证码后端用Java生成图片登录页面加一层校验成本低但能体现你对“防机器人攻击”有意识。第三个是数据统计用ECharts画一个“近七日售票趋势”和“热门线路Top5”管理员首页直接出图这在答辩时是最抢眼的画面。这些扩展里我个人最推荐座位图因为它把“汽车票”这个业务特点发挥出来了。火车票、飞机票都有选座汽车票虽然没有那么严格但作为系统设计可以按“座位号”落地。实现上只需要在订单表增加seat_number字段班次表增加sold_seats字段存已卖座位号每次下单时判断目标座位是否在已卖列表里。6.2 答辩高频问题老师会盯着你不放的地方答辩场上老师不一定全程看演示但一定会围绕几个点提问。我列一下出现概率最高的问题以及建议回答的思路。“你这个系统是怎么防止超卖的”——答使用数据库条件更新UPDATE schedule SET remaining_seats remaining_seats - 1 WHERE id ? AND remaining_seats 0利用MySQL行锁保证同一时刻只有一个请求能扣减成功。“用户登录的状态是怎么保存的”——答后端生成JWT前端存在localStorage请求时通过Authorization头传递后端拦截器统一解析校验。注意不要在回答里说“session”因为前后端分离项目用session会绕。“订单取消后余票怎么恢复”——答取消订单时开启事务先将订单状态改为已取消再执行UPDATE schedule SET remaining_seats remaining_seats 1 WHERE id ?两步在同一事务里保证一致性。如果你没做这个功能现在就去加上这是老师最常设的陷阱。“为什么用MySQL而不用MongoDB”——答系统数据是典型的结构化关系型数据用户、班次、订单之间有明确关联需要事务支持MySQL更合适。我个人在实际操作中的体会是这些问题的回答不需要背得多深但一定要把你自己写的代码里关键逻辑讲清楚。哪怕你只是用了MyBatis-Plus的selectById也要知道它底层执行了什么SQL。老师不怕你功能少就怕代码不是你写的。如果你准备的时间紧优先把事务、权限、超卖这三个点弄明白再把自己写过的类和方法过一遍答辩基本稳了。另外演示的时候请一定准备好两套数据一套空库数据用来演示后台新增班次一套带历史订单的数据用来演示列表和图表。顺手在浏览器里开一个无痕窗口先把用户端购票流程走完再切换到管理员端看订单两个角色切换得越流畅老师对你的印象分就越高。这套系统后续还能往很多方向扩展比如接入真实支付接口、增加短信通知、把订单模块改成可配置的发车时间范本等等。不管你是不是毕业设计我都建议你把下单、事务、权限这段逻辑自己动手敲一遍因为这才是将来工作里真正会用到的东西。