ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue汽车票预订系统毕设方案:从数据库设计到防超卖实现

SpringBoot+Vue汽车票预订系统毕设方案:从数据库设计到防超卖实现 这几年被我问过的学生里十个里面有八个想做“管理系统”但大多数人的第一反应是图书管理、学生管理这类从大一折腾到现在的老项目。不是说不行而是同质化太严重答辩时老师都懒得问细节。相比之下汽车票网上预订系统管理平台是个很聪明的选择它看起来也是增删改查但真正拆开后会发现SpringBoot Vue Java MySQL 这套全栈组合里最有讲头的知识点它全占了。线路班次查询、余票库存扣减、下单支付状态流转、超时取消释放座位、后台运营管理每一条都能在论文里写出东西来也都能在答辩时应对追问。这篇不聊空话直接拆一套能复现、能拿去写论文做毕设的完整方案。1. 这套系统的定位毕设/课设选择它到底值不值1.1 为什么是SpringBootVueMySQL这个组合先聊选型这是所有做毕设的人第一个纠结的问题。SSMSpring SpringMVC MyBatis确实能实现同样的功能但整套配置对新手不友好Spring 容器配置、事务配置、MyBatis 映射一堆 XML能把一个项目跑通就已经耗掉大半时间了。SpringBoot 把这些配置大幅收敛起步依赖、自动配置、内嵌 Tomcat写个方法就能起服务。别小看这一点对毕设来说时间就是命省下来的时间要留给业务逻辑和论文。前端为什么选 Vue 而不是继续用 JSP前后端分离本来就是主流Vue 的组件化开发和响应式数据绑定配合 Element UI 或 Element Plus 这类现成组件库做出来的页面观感比传统 JSP 模板高一个档次。评审老师看演示的时候页面干净、交互流畅第一印象就加分。而且 Vue 的学习曲线真心不算陡玩过 HTML JavaScript 的人几天就能上手组件、路由和数据请求。数据库选 MySQL 没什么好犹豫的它和 SpringBoot 的搭配简直不要太成熟。网上教程多出问题好排查JDBC 驱动、连接池配置资料一搜一大把。毕设场景下MySQL 单机部署完全够用还能让老师觉得你的基本功扎实。这个组合还有个隐性优势遇到问题能查到“标准答案”。项目做到一半卡住了随便一搜就能找到同类型问题的解决方案这对赶时间的毕设党来说比什么都重要。1.2 汽车票预订场景的核心业务闭环为什么是“汽车票”而不是火车票、机票因为火车票规则复杂还涉及官方接口机票的舱位和退改签规则也很麻烦。汽车票则是一个恰到好处的复杂度比“图书管理”这种纯增删改查更有业务深度又不会像电商平台那样把供应链、促销、库存体系全塞进去。用户端的核心闭环是这样的注册登录后输入出发地、目的地、出发日期系统查询出当天所有匹配线路的班次展示车型、发车时间、票价、余票用户选择一个班次填写乘客信息姓名、身份证号提交订单系统锁定座位并生成待支付订单用户完成支付订单状态变为已出票出发前如果行程有变可申请退票座位释放回库存。管理端的闭环则是另一条线管理员维护线路起点站、终点站、里程维护班次发车日期、发车时间、车牌号、总座位数、票价还能查看所有订单必要时处理退票。这套流程表面是普通的业务系统但里面藏着一个几乎所有电商场景都会出现的核心硬问题库存扣减。汽车票的余票就是库存多个用户同时抢最后一个座位怎么办这个问题一旦在答辩中被追问能把“熟悉业务逻辑”和“只会照着模板写CRUD”的人瞬间区分开。2. 数据库设计汽车票业务最关键的几张表和建模思路2.1 核心表结构与字段设计数据库设计是整套系统的地基也是最值得在论文里展开写的部分。我的习惯是先把表建出来边建边想业务。下面这几张表的字段和注释基本覆盖了汽车票系统的核心。用户表CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, phone VARCHAR(20) DEFAULT NULL, real_name VARCHAR(50) DEFAULT NULL, id_card VARCHAR(18) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) );线路表CREATE TABLE line ( id INT NOT NULL AUTO_INCREMENT, line_name VARCHAR(100) NOT NULL COMMENT 线路名称, start_station VARCHAR(50) NOT NULL, end_station VARCHAR(50) NOT NULL, distance DECIMAL(10,2) DEFAULT NULL COMMENT 公里数, duration_minutes INT DEFAULT NULL COMMENT 预计用时分钟, PRIMARY KEY (id) );班次表CREATE TABLE schedule ( id INT NOT NULL AUTO_INCREMENT, line_id INT NOT NULL COMMENT 所属线路, depart_date DATE NOT NULL COMMENT 发车日期, depart_time TIME NOT NULL COMMENT 发车时间, vehicle_no VARCHAR(20) NOT NULL COMMENT 班次号/车牌号, ticket_price DECIMAL(10,2) NOT NULL COMMENT 票价, total_seats INT NOT NULL COMMENT 总座位数, remain_seats INT NOT NULL COMMENT 剩余座位数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-正常 0-停运, PRIMARY KEY (id), KEY idx_line_date (line_id, depart_date) );订单表CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, user_id INT NOT NULL, schedule_id INT NOT NULL, passenger_name VARCHAR(50) NOT NULL, passenger_phone VARCHAR(20) DEFAULT NULL, passenger_id_card VARCHAR(18) DEFAULT NULL, ticket_num INT NOT NULL DEFAULT 1 COMMENT 购票张数, ticket_price DECIMAL(10,2) NOT NULL COMMENT 下单时的票价快照, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, order_status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待支付 1-已支付 2-已出票 3-已退票 4-已取消, expire_time DATETIME NOT NULL COMMENT 支付过期时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) );有个细节很多人第一次设计时会漏订单里为什么要单独存ticket_price和total_amount而不是下单后动态去班次表查价格这就是典型的“快照字段”设计。班次的票价是运营人员可以调整的如果张三下单时票价 50 元后面管理员把票价改成 60 元再去查班次表就会得到错误的历史订单金额。把金额冗余到订单表里历史订单就不会受到后续价格调整的影响。这个思路在真实电商系统里非常普遍论文里写一句“订单金额采用下单时点快照保障交易数据的稳定性和可追溯性”会让老师觉得你踩过坑、懂业务。为什么不单独建座位表毕设阶段确实没必要。每班车几十个座位单独建 seat 表意味着下单时要定位具体座位号、锁座位、释放座位复杂度直线上升。用remain_seats字段维护库存配合正确的更新语句已经能讲清楚余票控制的核心逻辑。2.2 座位库存与状态变更怎么设计才不坑余票库存就是一个整数字段看似简单但它的更新方式决定了系统在并发下会不会超卖。先看订单状态的设计我建议用整数枚举而不是字符串0-待支付1-已支付2-已出票3-已退票4-已取消状态机的关系一定要在代码里明确待支付 → 已支付用户支付成功待支付 → 已取消超时未支付或用户主动取消已支付 → 已出票支付完成后自动出票已支付/已出票 → 已退票用户申请退票写清楚状态之间允许的流转方向后代码里的判断就不会乱成一团。比如支付接口里必须加条件WHERE order_status 0退票接口必须加条件WHERE order_status IN (1, 2)防止同一个订单被重复支付、重复退票。库存释放同样要注意幂等。无论是超时取消还是退票释放余票之前先执行一条状态更新语句UPDATE orders SET order_status 4 WHERE order_no #{orderNo} AND order_status 0;如果影响行数为 1说明本次操作确实是第一个把订单从“待支付”改成“已取消”的操作这时才执行余票加回。如果影响行数为 0说明订单已经被其他流程处理过了直接跳过不再执行释放。这样一个简单的“状态条件更新 返回值判断”就能挡住重复释放的坑。3. 后端接口实现从登录鉴权到下单防超卖3.1 登录鉴权方式选择与项目分层前后端分离的项目会话管理最推荐用 JWT而不是传统的 Session。道理很简单后端不保存登录状态前端拿到 token 后自己存着每次请求放在 Header 里带给后端。后端写一个拦截器统一校验 token登录接口、静态资源放行其余请求全部检查。拦截器核心逻辑大概是这样的public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); // Token 为空或校验失败返回 401前端跳转登录页 if (StringUtils.isBlank(token) || !JwtUtil.verify(token)) { response.setStatus(401); return false; } request.setAttribute(userId, JwtUtil.getUserId(token)); return true; } }注意密码绝对不能明文存储。用 BCrypt 加密Spring Security 的BCryptPasswordEncoder单独拎出来用就行不必为了加密把整个 Spring Security 引进来。注册时加密登录时比对这一条在答辩时也是加分项。项目分层建议保持简单实用Controller → Service → Mapper。Controller 只负责参数接收和结果返回Service 写业务逻辑Mapper 负责数据库操作。我再强调一遍不要在 Controller 里写 SQL也不要用MyBatis-Plus的QueryWrapper包一层花哨查询掩盖业务不清晰的问题。老师翻代码的时候看到的应该是干净的分层和清晰的命名。再配一个全局异常处理器RestControllerAdvice统一捕获业务异常和系统异常返回统一的Result对象Data public class ResultT { private Integer code; private String message; private T data; }统一返回体这件事前后端联调的时候会省心非常多。前端拦截器拿到code ! 0的返回值直接弹出错误提示不需要每个页面单独处理异常分支。3.2 余票扣减与下单创建的并发处理这是整套系统最核心的代码也是答辩时老师最有可能追问的部分。很多学生第一次写下单接口是这样的逻辑先查余票判断余票大于买的张数再执行更新。这个写法在单用户测试的时候一切正常但两个用户同时抢最后一个座位时两个请求可能同时读到remain_seats 1都认为可以下单最后两个订单都创建成功座位却只有一个。这就是超卖。正确的做法是把“判断”和“扣减”合并成一条带条件的原子 UPDATETransactional public OrderVO createOrder(CreateOrderDTO dto) { // 1. 原子扣减余票这条 SQL 自带条件判断 int rows scheduleMapper.deductSeat(dto.getScheduleId(), dto.getTicketNum()); // 影响行数为 0说明此时余票已经不足 if (rows 0) { throw new BizException(余票不足请重新选择班次); } // 2. 生成订单号 String orderNo T LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) RandomUtil.randomNumbers(6); // 3. 创建待支付订单设置过期时间 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(dto.getUserId()); order.setScheduleId(dto.getScheduleId()); order.setPassengerName(dto.getPassengerName()); order.setPassengerIdCard(dto.getIdCard()); order.setTicketNum(dto.getTicketNum()); order.setOrderStatus(0); order.setExpireTime(LocalDateTime.now().plusMinutes(15)); orderMapper.insert(order); return OrderVO.from(order); }对应的 Mapper SQLUPDATE schedule SET remain_seats remain_seats - #{num} WHERE id #{scheduleId} AND remain_seats #{num}这条 SQL 的巧妙之处在于数据库的行锁会保证同一时刻只有一个事务能修改这一行remain_seats #{num}是更新条件的一部分不满足时更新影响行数为 0。完全不需要额外的锁也不会有超卖。这就是常说的“乐观锁思想落到 SQL 条件里”。整个方法用Transactional包裹扣减余票和创建订单必须是同一个事务。中间任何一个环节抛异常事务回滚余票不会平白减少。注意业务异常要继承RuntimeExceptionSpring 的默认事务回滚策略才会生效如果抛的是受检异常就会导致“订单没创建成功余票却已经扣了”的尴尬局面。订单号生成不用搞太复杂时间戳 6 位随机数就够用。别用 UUID 直接作为订单号太长了而且没有可读性。如果后续想扩展分布式场景可以替换成雪花算法这个扩展点在论文里提一句就行。3.3 未支付订单超时取消与座位释放用户下单后不支付座位一直被占着这是很现实的业务问题。真实生产环境会用延迟队列或定时任务平台但毕设阶段用 Spring 自带的定时任务完全说得通。Scheduled(fixedDelay 60_000) Transactional public void cancelExpiredOrders() { ListOrder expiredOrders orderMapper.selectExpiredOrders(LocalDateTime.now()); for (Order order : expiredOrders) { // 只有待支付订单才能被取消条件更新避免重复释放余票 int rows orderMapper.cancelByOrderNo(order.getOrderNo()); if (rows 0) { scheduleMapper.increaseRemainSeats(order.getScheduleId(), order.getTicketNum()); } } }对应的两条 SQL-- 查询超时未支付订单 SELECT * FROM orders WHERE order_status 0 AND expire_time #{now} -- 条件取消只有状态还是待支付时才会执行成功 UPDATE orders SET order_status 4 WHERE order_no #{orderNo} AND order_status 0 -- 释放余票 UPDATE schedule SET remain_seats remain_seats #{num} WHERE id #{scheduleId}注意Scheduled(fixedDelay 60_000)和fixedRate的区别fixedDelay是上次任务执行完再隔 1 分钟执行fixedRate是 1 分钟执行一次不考虑上次是否执行完。对于这种批量扫描任务fixedDelay更安全不会因为某次执行时间过长导致任务重叠。如果以后项目部署了多个实例来分担压力定时任务会每个实例各跑一遍。这时候上面的“先更新订单状态再决定是否释放余票”就派上用场了——两个实例同时扫到同一笔订单只有一个实例的 UPDATE 影响行数为 1另一个是 0自然就避免了重复释放。这个点如果能在论文里写到会非常加分。4. 前端Vue页面查询链路、购票流程与状态反馈4.1 路由规划与页面划分前端部分用的 Vue有 Vue2 Element UI 和 Vue3 Element Plus 两条路线。如果是跟着现有源码走大概率是 Vue2 Element UI因为这套组合的资料数量和市场存量明显更大。如果你打算自己从头搭我仍然建议 Vue2 起步等基本流程跑通了再考虑升级 Vue3 的组合式 API。页面和路由可以这样规划路由页面说明/login登录/注册两者共用一个页面tab 切换/首页展示热门线路引导搜索/search班次查询输入起终点、日期展示班次列表/order/create确认订单填写乘客信息、确认票价/order/pay待支付展示剩余支付时间模拟支付/order/list我的订单查看状态、申请退票/admin/schedule班次管理管理员维护线路班次/admin/order订单管理管理员查看所有订单路由守卫不能少不然未登录用户直接输个 URL 就能进下单页业务逻辑就白做了router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { return next(); } if (!token) { return next(/login); } if (to.path.startsWith(/admin) localStorage.getItem(role) ! 1) { return next(/); } next(); });这段逻辑很简单没 token 一律去登录页访问管理端还要校验角色。实现上不复杂但能体现出你对“前端安全控制”有概念。4.2 购票全流程的状态反馈前端最重要的不是写出一堆漂亮的页面而是把购票流程的状态反馈做清楚让用户每一步都清楚现在发生了什么。搜索页的交互看起来简单细节却多。绑定一个searchForm输入起点、终点、日期调GET /api/schedule/query返回的班次列表用卡片或表格展示。余票状态要区分处理余票大于 10 显示“余票充足”小于 10 显示“仅剩 N 张”等于 0 时购票按钮直接置灰。这些看似琐碎的判断恰好是真实产品体验和课程作业的分水岭。下单页填写乘客信息时一次买多张票就要重复渲染乘客表单。提交订单前前端不要自己拼价格要以服务端返回的totalAmount为准。下单成功后跳转待支付页这时要显示订单号和剩余支付时间。倒计时用setInterval每秒递减到 0 时提示“订单已超时取消”按钮变为不可用。这里的定时器记得在组件销毁时用clearInterval清理不然切换页面后计时器还在后台跑控制台会报错。支付页是模拟的但流程要走完点击“确认支付”调POST /api/order/pay/{orderNo}后端把订单状态从 0 改成 1写入pay_time前端收到成功后跳到购票成功页。购票成功页展示班次信息、发车时间、座位号和乘车提示最后一步的仪式感做到位演示效果会很好。订单列表建议用 tab 分栏全部、待支付、已出票、已取消/已退票。因为是管理后台和用户端共用一套设计风格可以直接在 Element UI 的el-tabs和el-table基础上快速搭起来。每个订单的状态除了文字再用el-tag配不同颜色演示的时候一眼能看出状态变化。5. 部署与踩坑从本地联调到上线发布5.1 环境准备与版本搭配很多人拿到源码后第一反应是直接跑然后被各种版本问题折磨到想放弃。根据我的经验下面这套版本组合是目前兼容性最稳的环境推荐版本备注JDK8 或 11兼容性最好不建议 JDK 17 或 21Maven3.6.3 或 3.8.xIDEA 自带的也行Spring Boot2.7.x不要用 3.xAPI 和包名变化不小MySQL5.7 或 8.0字符集统一 utf8mb4Node.js16.xVue2/ 18.xVue3版本太高可能编译报错IDEA2021 之后记得装 Lombok 插件application.yml里有几个关键配置尤其是数据库连接和 Jackson 时间格式化spring: datasource: url: jdbc:mysql://localhost:3306/bus_ticket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: trueallowPublicKeyRetrievaltrue这一项务必保留MySQL 8 环境经常遇到“Public Key Retrieval is not allowed”的报错加了这个参数就解决了。serverTimezoneAsia/Shanghai不配置的话后端查出来的时间会跟本地时间差 8 个小时。5.2 跨域、时间、金额、分页等常见坑这一节是我最想写的因为这些问题几乎每个跑毕设的人都遇到过。跨域开发环境下Vue 跑在 5173 端口后端跑在 8080 端口浏览器会因为端口不同拦截请求。最简单的办法是用 Vite 的 devServer 代理把/api开头的请求转发到后端server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }如果项目没有走代理也可以在后端加一个 CORS 配置类允许指定来源跨域。生产环境用 Nginx 反向代理后后端接口和前端页面同源跨域问题自然消失。时间前端展示时间有偏差别急着改前端先检查三处——MySQL 的serverTimezone、Spring Boot 中 Jackson 的time-zone、数据库字段是否用了DATETIME。这三处统一成Asia/Shanghai后九成的时间问题都能解决。金额所有涉及钱的地方Java 用BigDecimal数据库用DECIMAL前端展示时用toFixed(2)保证两位小数。绝对不要用double去计算票价和总金额浮点数精度问题会导致 0.1 0.2 变成 0.30000000000000004这种低级错误在演示时被老师点到会非常尴尬。分页如果用了 PageHelper注意版本兼容。PageHelper 1.x 和 Spring Boot 2.7 配合时偶尔会出现分页信息包装异常换最新的pagehelper-spring-boot-starter基本能解决。还要记住 PageHelper 只对紧跟它之后的第一条查询 SQL 生效别在中间插入无关查询再去分页。5.3 打包部署毕设演示最省事的部署方式是前后端打成一个 Jar。先在前端目录执行构建npm run build然后把dist目录下所有内容复制到后端项目的src/main/resources/static目录下。再打包后端mvn package -DskipTests java -jar target/bus-ticket-0.0.1-SNAPSHOT.jar打开浏览器访问http://localhost:8080整个系统就起来了。这种方式演示时只需要起一个 Java 进程和一个 MySQL省去了 Nginx 和 Node 的麻烦。如果以后想做得更正规一点可以前后端分离部署后端 Jar 跑在 8080前端dist扔给 Nginx。Nginx 配置里最关键的是 Vue 的 history 路由刷新不能 404server { listen 80; location /api/ { proxy_pass http://127.0.0.1:8080/api/; } location / { root /opt/bus-front/dist; try_files $uri $uri/ /index.html; } }没有try_files $uri $uri/ /index.html这一行你访问/order/list后一刷新Nginx 会去找磁盘上不存在的order/list文件返回 404。这是前端路由部署最常见的坑。6. 毕设答辩与学习扩展让项目从“能跑”到“能讲”6.1 演示路径设计代码跑通只是第一步能顺利演示完才是重点。提前把演示脚本写好到时候按部就班操作比临场发挥强一百倍。步骤操作预期效果1启动 MySQL导入bus_ticket.sql四张核心表创建成功2启动后端服务启动前端访问首页正常3注册一个新用户数据库中密码是加密串4登录搜索“滨江 到 临安 今天”展示班次列表、票价、余票5选择班次下单 2 张生成待支付订单显示倒计时6点击支付状态变为已出票余票减 27订单列表操作退票状态变为已退票余票加回 28手动改写expire_time为过去时间定时任务取消订单余票恢复演示前一定要把测试数据固定好。比如专门建一条“滨江—临安”的线路票价设成 35 元把余票设成 2 张。到时候直接搜索不要现场输入大段城市名。手机号、身份证号这些测试数据提前准备几组别到演示时现编容易错。6.2 论文/答辩常见提问答辩环节老师最喜欢盯着这几个问题反复问提前准备得好气氛会很愉快SpringBoot 相比传统 Spring 有哪些优势为什么用 Vue 做前后端分离JSP 不行吗订单表为什么要冗余票价字段如何防止余票被超卖除了你现在的写法还有什么方案可以解决超卖数据库索引怎么设计的查询慢怎么办订单状态为什么要用整数枚举JWT 登录和 Session 登录有什么区别如果用户量变大这个系统哪里会成为瓶颈前面的设计细节能答上来这些问题就是送分题。特别是超卖问题你如果能主动讲出“先用条件 UPDATE 原子扣减再考虑扩展 Redis 预扣减方案”老师基本不会再刁难。6.3 可扩展方向毕设做完如果还有余力这几个方向随便挑一个加上去论文和答辩的质量都能再上一个台阶用 Redis 缓存热门的班次查询结果减少数据库压力用 Redis 的 Lua 脚本实现库存预扣减作为高并发场景的进阶方案增加 ECharts 统计报表按月按线路展示售票数据对接微信支付的沙箱环境把模拟支付换成真支付流程把定时轮询取消订单改成 RabbitMQ 延迟队列更贴近生产架构增加座位图选座需要引入 seat 表并处理座位状态。每一个方向都不是随随便便说说的完全可以写进论文的“未来展望”章节让老师看到你确实考虑过系统的演进方向。最后说一句实操上的体会网上能下载到的源码很多但直接复制粘贴跑不起来的大有人在。真正稳妥的做法是拿一套能跑的源码当底子然后把下单、支付、退票这三条链路自己动手再敲一遍每一步都搞清楚为什么这么写。等你能用自己的话把这三条链路讲顺这套系统就真正成为“你的项目”了。
返回列表