SpringBoot+Vue 汽车票网上预订系统管理平台源码解析:从课设到实战的全流程复盘
如果你正在为毕业设计或课程设计找项目选题,大概率会看到“汽车票网上预订系统”这类方案。原因很直接:它是典型的管理信息系统课题,业务流程完整、前后端交互清晰、数据库表结构有设计空间,难度曲线对本科生和初级学习者来说刚好卡在“跳一跳够得着”的位置。
这套系统的核心玩法是:用户在线查车次、选座位、下单订票,管理员在后台上架线路、排班、设置票价、管理订单。技术上就是主流的 SpringBoot + Vue + MySQL 组合,前端管交互,后端管业务,数据库管数据。对于要交毕设的人来说,它最大的价值在于每一个模块都能讲清楚业务逻辑,答辩时不会被问到哑口无言。
这篇博文不打算做流水账式的功能罗列,而是从选型思路、功能拆解、核心代码实现、联调打包到踩坑实录,把整个项目从头到尾捋一遍。你既可以把它当成毕设开发的参考文档,也可以从中提取一些通用的前后端分离开发经验,换到别的业务场景同样适用。
1. 项目定位与技术选型:为什么这套组合是课设的“标准答案”
1.1 汽车票预订系统的业务场景与课题价值
先说说为什么汽车票预订系统在毕设选题里长盛不衰。一个课题适不适合做毕设,看三个维度:业务逻辑是否完整可讲、功能边界是否清晰可控、技术点是否覆盖常见知识点。汽车票系统在这三个方面都很占便宜。
业务上,它天然分成用户端和管理端两条线。用户端有注册登录、车次查询、在线预订、订单支付(课设阶段可模拟)、我的订单、退票改签;管理端有线路管理、班次管理、车辆管理、票价管理、订单管理、数据统计。这样一个系统基本覆盖了增删改查、权限控制、状态流转、数据关联查询这些最常用的后端能力。
功能边界上,相比商城系统动辄涉及商品库存、优惠券、物流、支付回调,汽车票系统的核心领域就是“班次—座位—订单”这条主线,不会把项目拖到做不完的境地。而且它的状态流转很清晰:下单时锁座,支付后保留,未支付自动释放,发车后不可退,这些业务规则既简单又有技术实现空间。
从评审角度讲,答辩老师最关心的是“哪些功能是你自己实现的,遇到了什么问题,怎么解决的”。汽车票系统的余票并发控制是一个绝好的提问切入点,后面我会专门讲这部分怎么实现、怎么自圆其说。
1.2 技术栈选型考量:SpringBoot、Vue、MySQL各自的定位
既然是前后端分离架构,三端各选什么技术就是最先要决策的事。
后端选择 SpringBoot 而不是传统 SSM,核心原因在于它能极大压缩配置成本。SSM 时代要写 Spring 配置文件、MyBatis 配置、web.xml,光是环境搭建就能劝退一大批人。SpringBoot 的自动配置加上内嵌 Tomcat,一个 main 方法就能起服务。对课设项目来说,时间应该花在业务实现上,而不是在踩配置的坑上。
前端选择 Vue 的理由同样简单。Vue 的渐进式开发模式对新手特别友好:模板语法接近 HTML,数据绑定和组件化让页面复用变得简单,配合 Element UI/Element Plus 这类组件库,半天时间就能拉出后台管理界面。相比 React 的学习曲线,Vue 的上手成本低得多,而且在国内中小公司和绝大部分学校的课程里,Vue 的普及度也明显更高。
MySQL 就不用多说了,关系型数据库里它是最没门槛的。在 Windows 上有目前很完整的可视化和命令行方式可选,也提供了足够丰富的索引优化、事务隔离机制,应付课设级别的数据量绰绰有余。
这套组合还有一个隐性优势:它是国内招聘市场上出现频率最高的技术栈。哪怕你以后不做这个方向,做过一个完整的 SpringBoot + Vue 项目,写简历、面试聊项目经验,都能直接拿得出手。
2. 核心细节解析与实操要点:系统功能模块与数据库设计思路
2.1 前后端功能架构拆解
在动手写代码之前,先画清楚功能地图。后端按角色和服务类型划分,前端按使用人群划分,两层结构对上,开发时就不会乱。
用户端的功能主线是“搜索车次 → 查看班次详情 → 选座下单 → 管理订单”。核心页面包括:首页(含线路搜索)、车次列表页(支持按出发地、目的地、日期筛选)、车次详情页(展示座位余量、票价、出发到达时间)、订单确认页、个人中心(我的订单、退票入口)。
管理端的功能主线是“维护基础数据 → 排班调度 → 处理订单 → 统计分析”。核心页面包括:登录页、仪表盘(统计今日订单、总营收等)、线路管理页、班次管理页、车辆管理页、订单管理页、用户管理页。
这里面有两个容易被忽视但很重要的模块设计。第一个是“座位与余票”的抽象,它不能只是一个 int 类型的余票字段,因为当订单取消或者退票时,余票需要回滚,而且不同车型的座位数不同,所以更合适的做法是每个班次关联车辆,车辆有座位总数,余票 = 座位总数 - 已售票数。第二个是订单状态的枚举设计,推荐定义 待支付(0)、已支付(1)、已出票(2)、已退票(3)、已取消(4)这几种状态,所有业务操作都围绕状态流转展开,逻辑会非常干净。
2.2 数据库表设计与核心关联关系
数据库设计是整个项目的基石,表结构建得合理,后面写 SQL 和业务代码都能省一半力气。我根据个人踩坑经验,把核心表罗列一下,并说明怎么设计才能减少后期返工。
用户表基本字段就是账号、密码(BCrypt 加密后存储)、真实姓名、手机号、角色标识。角色用 role 字段区分 普通用户/管理员 即可,不需要引入复杂的权限框架。
线路表存储 出发城市、到达城市、里程、预计时长。这里要做一个决定:线路和班次是否拆分。如果图省事把发车时间直接写在线路表里,就会遇到两条线路同样从 A 到 B 但发车时间不同的情况,到时候改起来很痛苦。正确做法是线路是“静态基础数据”,班次是“某条线路某天某时刻的一趟车”,表之间用外键关联。
班次表是业务核心,字段包括线路 ID、车辆 ID、发车时间、到达时间、票价、剩余票数、状态。发车日期应该单独用 date 字段,因为同一个班次在7月1日和7月2日是可重复生成的,很多课设项目在这里设计成“每天新建一个班次记录”,虽然逻辑上也通,但会产生大量冗余数据。比较规范的思路是班次模板 + 每日发车实例,或者直接用 日期+班次号 做唯一约束。
订单表字段包括订单号、用户 ID、班次 ID、座位号、购买数量、总金额、状态、创建时间、支付时间。订单号建议用时间戳加随机数生成,避免自增 ID 暴露业务量。
这里要特别强调一下表之间的关联关系:用户表一对多订单表,班次表一对多订单表。下单时查询班次的剩余票数,扣票和创建订单必须在同一个事务里完成,这样才能保证数据一致性。为了避免超卖,可以在扣减票数时使用带条件的 UPDATE,这在后面的业务实现里还会详细展开。
2.3 从零开始的表结构落地参考
为了帮你快速起步,我给一套可以直接用的建表 SQL 参考。字段命名用的是下划线风格,Java 侧再用驼峰映射,这一点得益于 MyBatis 的 mapUnderscoreToCamelCase 配置,非常方便。
CREATE TABLE `user` ( `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID', `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)', `real_name` VARCHAR(30) DEFAULT NULL COMMENT '真实姓名', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `role` TINYINT DEFAULT 0 COMMENT '角色 0-普通用户 1-管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `line` ( `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '线路ID', `depart_city` VARCHAR(50) NOT NULL COMMENT '出发城市', `arrive_city` VARCHAR(50) NOT NULL COMMENT '到达城市', `distance` DECIMAL(8,2) DEFAULT NULL COMMENT '里程(公里)', `duration` INT DEFAULT NULL COMMENT '预计时长(分钟)', UNIQUE KEY `uk_city_pair` (`depart_city`, `arrive_city`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='线路表'; CREATE TABLE `vehicle` ( `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '车辆ID', `plate_no` VARCHAR(20) NOT NULL COMMENT '车牌号', `seat_count` INT NOT NULL COMMENT '座位数', `vehicle_type` VARCHAR(30) DEFAULT NULL COMMENT '车型' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆表'; CREATE TABLE `schedule` ( `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '班次ID', `line_id` INT NOT NULL COMMENT '线路ID', `vehicle_id` INT NOT NULL COMMENT '车辆ID', `depart_date` DATE NOT NULL COMMENT '发车日期', `depart_time` TIME NOT NULL COMMENT '发车时刻', `arrive_time` TIME NOT NULL COMMENT '到达时刻', `price` DECIMAL(10,2) NOT NULL COMMENT '票价', `remaining_tickets` INT NOT NULL COMMENT '余票数', `status` TINYINT DEFAULT 1 COMMENT '状态 1-正常 0-停运', KEY `idx_depart_date` (`depart_date`), KEY `idx_line_date` (`line_id`, `depart_date`), CONSTRAINT `fk_schedule_line` FOREIGN KEY (`line_id`) REFERENCES `line`(`id`), CONSTRAINT `fk_schedule_vehicle` FOREIGN KEY (`vehicle_id`) REFERENCES `vehicle`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班次表'; CREATE TABLE `booking` ( `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '订单ID', `order_no` VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号', `user_id` INT NOT NULL COMMENT '用户ID', `schedule_id` INT NOT NULL COMMENT '班次ID', `seat_numbers` VARCHAR(50) NOT NULL COMMENT '座位号集合,逗号分隔', `ticket_count` INT NOT NULL COMMENT '购票张数', `total_amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额', `status` TINYINT DEFAULT 0 COMMENT '状态 0-待支付 1-已支付 2-已出票 3-已退票 4-已取消', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间', `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间', KEY `idx_user` (`user_id`), KEY `idx_schedule` (`schedule_id`), CONSTRAINT `fk_booking_user` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), CONSTRAINT `fk_booking_schedule` FOREIGN KEY (`schedule_id`) REFERENCES `schedule`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';这里有一个非常值得记住的细节:所有表的字符集都设置为 utf8mb4,而不是 utf8。原因是 utf8 在 MySQL 中最多只能存 3 字节的字符,遇到表情符号(比如用户昵称里带个 emoji)插入时直接报错,utf8mb4 才是真正完整版的 UTF-8。我见过太多人在这一步踩坑,表已经建了一堆数据才发现问题,返工成本很高。
主键统一用自增 INT,对课设项目来说性能完全够用,又不引入分布式 ID 的复杂度。外键加上是给数据一致性加一道保险,但在实际联调阶段你会发现外键有时会拖慢调试速度,如果出现删除被阻塞的情况,先检查是不是有外键没级联,再决定要不要保留。对于课设项目,按上面的设计保留外键更稳妥,从学习角度看也更规范。
2.4 关键业务设计:余票控制与事务边界
余票超卖是这个系统最经典的业务难点。场景是这样的:当两个用户同时买了一趟只剩 1 张票的班次,如果代码写成先查余票大于 0,再扣减,再下单,那么极有可能两个请求都查到 1 而同时进入扣票逻辑,数据就乱了。
解决思路很简单:把检查与扣减合成一条原子 SQL。
// 伪代码示意,位于 ScheduleMapper 中 @Update("UPDATE schedule SET remaining_tickets = remaining_tickets - 1 " + "WHERE id = #{scheduleId} AND remaining_tickets > 0") int deductTicket(@Param("scheduleId") Long scheduleId);这条 SQL 能保证只有一个请求把余票从 1 减到 0,另一个请求因为 remaining_tickets > 0 条件不满足而返回 0,代码拿到返回值去判断是否插入订单。如果更新影响行数为 0,直接抛出“余票不足”异常即可,不需要再做额外的并发控制。
事务边界也必须清晰。核心原则是:创建订单和扣减余票放在同一个 @Transactional 方法中,成功则一起提交,失败则一起回滚。支付成功后更新订单状态则是在另一个事务操作里,因为支付动作和购票动作从时间上可能是分开的。
退票的逻辑容易忽略一件事:退票时不仅要改订单状态为已退票,还要把对应班次的余票加回来。这里必须用乐观锁或者带条件更新的方式防止多次退票,比如更新订单时加上 WHERE status = 已支付 的条件,如果更新的行数为 0,说明这张票的状态已经变了,不能再退。
3. 实操过程与核心环节实现:从搭建到打包上线的完整流程
3.1 环境准备:版本选型和安装避坑
动手前先把环境搞定。后端 JDK 我建议用 JDK 8,不是因为它新,而是因为 SpringBoot 2.x 对 JDK 8 的支持最成熟,网上资料也最多,遇到问题能搜到方案的概率会高很多。如果你用 JDK 17 甚至更新版本,某些老库可能会出现兼容性问题。如果你决定用 JDK 17,那 SpringBoot 直接上 3.x 版本,否则会有一堆 javax 包名不兼容的坑。
Maven 装好了之后,仓库地址建议换成国内镜像,不然后端第一次构建要下载大量依赖,原地址速度能让你怀疑人生。在 Maven 安装目录的 conf/settings.xml 里配置 mirrors,加入阿里云镜像即可,这一步能省半小时以上。
MySQL 安装时有两个点容易踩坑:一是安装完成后 root 密码的设置,一定要记好;二是 MySQL 8.x 默认的认证插件是 caching_sha2_password,而部分旧版驱动或可视化工具可能不支持,如果连接时提示认证方式错误,改回 mysql_native_password 就能解决。数据导入时用 source 命令或 Navicat 运行 SQL 即可,注意先建库再导表。
前端环境主要是 Node.js 和 npm。建议安装 Node 16 或 18,如果你用的 Vue CLI 创建项目,太新的 Node 会有某些依赖编译报错的问题,太旧的又跑不起新构建工具,卡在中间比较省心。
3.2 后端工程结构与核心代码实现
推荐的后端包结构是我自己一直在用的 Controller — Service — Mapper 三层架构,这也是当前企业里主流的开发模式。直接照这个结构建包:
com.example.ticket ├── controller // 控制层,接收请求、返回响应 ├── service // 业务层,写核心业务逻辑 │ └── impl ├── mapper // 数据访问层,MyBatis 映射接口 ├── entity // 实体类 ├── dto // 数据传输对象 ├── common // 通用返回结果、异常处理 └── config // 配置类(跨域、拦截器等)实体类字段配合 Lombok 大大简化代码,但 Lombok 在旧版本 JDK 或某些 IDE 环境中会有编译兼容问题,如果报找不到 getter 或 setter,检查一下 Lombok 的版本,或者用 IDEA 自带的“生成”功能手动补齐。
核心的业务逻辑放在 Service 层,Controller 层只做参数接收和结果返回,不要堆业务。以一个下单购票的业务为例,核心实现逻辑如下:先校验班次是否存在且状态正常,然后调 Mapper 做余票原子扣减,如果扣减失败就抛出余票不足异常,成功则创建订单并把订单号和总金额返回给前端。整体上保持这个方法 @Transactional,任何异常自动回滚,防止订单建了但票没扣或者票扣了订单没建的情况。
跨域配置在这里单独说一下。前后端分离开发时,前端在 8080 端口跑,后端在 8081 端口跑,如果不做跨域处理,浏览器会直接拦截请求。后端的做法是写一个 WebMvcConfigurer 配置类,重写 addCorsMappings 方法,允许 http://localhost:8080 访问,允许所有请求方法,放开所有请求头。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .allowedHeaders("*") .maxAge(3600); } }注意 allowCredentials(true) 和 allowedOriginPatterns("*") 要配套使用,否则浏览器可能报“请求头不允许携带凭证”之类的错误。开发环境直接放开所有源没问题,部署到线上时就得把 allowedOriginPatterns 改成具体的域名或 IP。
登录鉴权方面,课设项目不建议引入 Spring Security + JWT 的大套件,学习成本和配置复杂度都比较高。更务实的方式是定义一个拦截器,对需要登录的接口校验请求头中的 token(用 UUID 或直接存用户 ID 都行),用户登录成功后把 token 保存在后端内存 Map 或 Redis 里,Redis 没有也不影响使用,内存 Map 足够支撑课设场景。这个实现思路简单,但该讲的技术点一个不少,答辩时也能自圆其说。
3.3 前端工程与核心功能实现
前端建议直接用 Vue CLI 创建,项目创建过程中如果选择“手动选择功能”,我想提个醒:如果你用 Vue CLI 创建,预设里勾上 Router 和 Babel 就行了,其他功能(ESLint、测试等)可以不加,避免配置问题。
组件库选择上,如果你用 Vue 2,就配 Element UI;如果用 Vue 3,就配 Element Plus。我的建议是直接用 Vue 3 + Element Plus,毕竟 Vue 2 已经进入维护冻结状态,新项目没必要守着旧技术。整体看 Vue 3 的语法与组合式 API 更接近未来的项目实践。
前后端交互这一层,我用 axios 做一个简单的封装,统一处理请求路径前缀、token 注入和错误提示。前后端同时开发时,可以把 baseURL 配为 http://localhost:8081/api,联调完再改成线上接口。
// request.js import axios from 'axios' import { ElMessage } from 'element-plus' 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 => { ElMessage.error(error.response?.data?.message || '请求失败') return Promise.reject(error) } ) export default request路由守卫是前端权限控制的关键。用户未登录时想访问个人中心或订单页,应该被重定向到登录页。在 router 里配置 meta.requiresAuth 字段,在全局前置守卫里判断 localStorage 是否有 token 即可。这个方案虽然简单,但确确实实能拦住没有登录的用户。
车次查询页和订单确认页这两个页面,是前端能体现“业务理解”的地方。查询页要用日期选择器限定查询日期,把选择的出发地、到达地、日期拼成查询参数;订单确认页展示班次信息和座位选择逻辑,每选一个座位就重新计算总价,座位号在页面里用文本形式展示即可。这里的核心价值点在于让用户看出你理解了“票务系统”的业务流程,而不只是做了个增删改查。
3.4 前后端联调与打包部署
联调阶段经常出问题的点,集中在字段映射不一致和接口返回格式不统一。后端返回结果建议统一封装成一个 Result 对象,包含 code、message、data 三个字段。前端 axios 响应拦截器里拿到 response.data 之后再做业务判断,这样前后端约定的结构越简单,联调越顺畅。
打包环节分成两步。后端用 Maven 的 package 命令打出可运行的 jar 包,我已经习惯把端口、数据库连接信息外置到 application.yml 并通过环境变量覆盖实现配置多环境切换。如果你把前端打包后的 dist 目录直接放到 SpringBoot 的 static 目录下,实现单 jar 包部署,注意前端资源的路径,确保 Vue 里用的是相对路径或者和服务端 context-path 对应的路径。
前端打包用 npm run build 执行。打包完成后检查 dist 目录下生成的 index.html,如果引用路径是 /static 开头的绝对路径,而项目又部署在非根路径下,页面就会白屏。最稳妥的办法是在 vue.config.js 里配置 publicPath: './',让它使用相对路径。
后端 jar 包在启动时可能报“端口被占用”,原因是本机某个服务占了 8080 或 8081。打开命令行,执行 netstat -ano | findstr 8080,查出来占用进程号,再在任务管理器里结束对应进程就行了。
4. 常见问题与排查技巧实录:这套系统最容易踩的坑
4.1 MySQL 连接与配置类问题
这个项目的所有问题里,MySQL 相关的占了一半以上。我先把高频问题整理成一张速查表,方便你按图索骥。
| 问题现象 | 常见原因 | 解决方案 |
|---|---|---|
启动报Access denied for user 'root'@'localhost' | 密码错误或密码未设置 | 确认安装时的 root 密码,重置密码后用 ALTER USER 变更认证信息 |
报Unable to load authentication plugin 'caching_sha2_password' | MySQL 8.x 默认认证插件与连接驱动不兼容 | 在 MySQL 执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '新密码'; |
报Server returns invalid timezone. Need to set 'serverTimezone' property | 数据库时区未设置 | 连接 URL 加上?serverTimezone=Asia/Shanghai&useSSL=false |
| 中文数据乱码 | 库/表字符集不是 utf8mb4 | 建库语句指定DEFAULT CHARSET=utf8mb4,已有表用 ALTER 转换 |
| 项目能启动但第一次请求超时 | 数据库连接池初始化慢或密码错误被重试 | 检查 URL、用户名、密码,后续尝试在启动参数里调大连接超时时间 |
4.2 后端启动与依赖问题
SpringBoot 项目启动报错,大多数是环境问题而不是代码问题。最常见的两个是端口占用和依赖冲突。端口占用刚才说过,用命令找到占用进程结束即可。依赖冲突则建议用 IDEA 的 Maven 面板跑一下 dependency:tree,看有没有相同 jar 的不同版本混进来了,锁定版本后就能解决。
还有一类问题出现在 JDK 版本不匹配的情况:如果你安装了 JDK 17,又在 pom.xml 里用了 SpringBoot 2.6.x,大概率会报 java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException。这不是代码问题,是包名从 javax 改成 jakarta 导致的。要么换 SpringBoot 3.x,要么换 JDK 8,别硬扛。这个我试过,硬扛会消耗大量时间。
如果启动正常但接口访问就报 404,先检查 Controller 上的类注解是不是 @RestController(不是 @Controller),再检查 mapper 层接口上是否加了 @Mapper 注解或启动类上有 @MapperScan。这些细节报错不明显,但排查起来很费时间。
4.3 前端联调与部署问题
页面白屏是最常见的状态,先打开浏览器控制台看具体报错,再定位原因。通常有两大类:第一类,npm run dev 阶段就起不来,多为 Node 版本和依赖编译问题,优先升级或降级 Node 版本;第二类,打包部署后白屏,大概率是资源路径问题,检查 publicPath 和部署环境子路径。
接口 404 或跨域报错的问题,建议后端把请求日志打印出来,每次请求来了都会看到请求 URL 和方法。如果后端没有收到请求,问题就在前端或网络层;如果后端收到了但响应报错,就去调试后端代码。这个方法能快速缩短问题定位范围,不用无线猜。
4.4 业务逻辑与数据一致性问题的排查实录
在余票并发这个问题上,最容易出现的现象是:下单前明明有余票,下单时却提示余票不足。这其实说明前面的原子扣减 SQL 生效了,是正常的防超卖表现。但如果测试时发现余票是负数,那几乎可以肯定扣减逻辑没有加 remaining_tickets > 0 的条件。
另一个数据一致性问题是订单状态异常。比如支付页面刷新两次,订单被创建了两次,但扣票只发生了一次,出现了一张订单没票的情况。解决方案是在创建订单之前先检查当前用户是否对该班次有一个未完成(待支付或已支付)的订单,有就直接返回已有订单,不允许重复下单。
退票加回余票时要防止“重复退票”导致余票虚增。除了前面说的在 UPDATE 语句里加 status 条件,后端 Service 层最好也判断一下当前订单状态,如果已经是已退票就直接拒绝。双保险写起来不复杂,但能挡住很多边界操作。
5. 结合毕设/课设场景的开发节奏建议
5.1 如何在有限时间内高效完成
如果是毕设,时间相对充足但也别拖。我的建议是两周打通主线:第一周做完环境准备、数据库设计和后端接口,第二周全力做前端页面和前后端联调。语言层面不用过于纠结细节,关键就是先把主流程跑通,再回头优化。
如果是课设且时间只剩一周左右,优先级应该是这样:先做好数据库和两个核心页面(查询页 + 下单页),再把管理端的线路和班次维护做出来,最后有时间才去补统计图表和用户管理。不要先把用户头像、密码找回这些边缘功能做完,再回来做核心业务。
5.2 答辩时的高频提问与回答思路
答辩老师最常问的几个问题,我提前帮你演练一下。
“订单超卖怎么解决的?”回答思路:先讲清楚问题 —— 并发下单时同时读到相同的余票数量,然后讲方案 —— 用带条件的 UPDATE 语句,把余票检查和扣减合并成一条原子 SQL,通过影响行数判断是否成功,数据库的行锁保证同一时间只有一个事务能改这条记录。
“为什么前后端要分离?”回答思路:职责分离,后端只管接口和数据,前端只管展示和交互,两边独立开发互不阻塞,也方便以后多端复用同一套接口。
“登录鉴权怎么做的?”回答思路:用户登录成功后后端生成 token 写入响应,前端存到 localStorage,每次请求在 axios 拦截器里带上,后端过滤器拦截除白名单外的所有请求校验 token 有效性。把业务对象和鉴权区分开梳理,这一块基本不会卡壳。
5.3 如何扩展这个项目做出差异化
如果你想在答辩中多拿几分亮点,建议在这个基础上做一两个扩展。低成本高性价比的有三个方向:一是管理端增加按日期统计营收的小图表,用 ECharts 画柱状图,这是一道很好的加分题;二是增加班次筛选条件,比如按车型、按时间段过滤,前端加个下拉框,后端加两个查询参数就行;三是把订单超时自动取消做成 Quartz 定时任务,定期扫描待支付订单,超过 30 分钟自动释放余票,这是一个很能体现工程能力的点。
6. 个人实操心得与最后的小建议
这个项目我前后带过不少同学从头到尾完成,最大的感受是:它的难点不在技术本身,而在“你是否能把业务逻辑梳理清楚”。很多人的代码写不下去,本质上是没想明白用户下的单和班次之间的状态关系。所以开工第一件事,别急着敲代码,拿张纸把“用户下单 → 扣余票 → 支付 → 出票 → 退票 → 加余票”这条流程画一遍,再开始写表结构和接口设计,你会发现后面的路顺得多。
再说一个具体操作的坑:我在初版开发时曾经把订单的金额在前端页面计算后直接传给后端保存,结果用户在浏览器里改一下请求参数,票价就变了。正确做法是后端根据班次 ID 查出票价,乘以购买数量得到总金额,前端传过来的金额一律不信任。这种安全问题在课设里可能不会被攻击,但你把它做对了,写在简历上就是一个真实的安全实践经历。
最后给一个小技巧:开发过程中一定养成写接口文档的习惯,哪怕是手动在笔记里维护一份接口列表,包含 URL、请求参数、返回结构。别嫌麻烦,调试时查文档比翻代码快不知道多少倍,而且这份文档在写毕业论文时还能直接复用。
做课设不是堆功能,是在有限的一个真实场景里,把学过的技术合理组合起来并讲清楚为什么这么选。只要主线逻辑完整、关键实现拿得出手、踩过的坑能讲明白,这个项目就交出了一份体面的答卷。