尧图精选

SpringBoot轨道交通自动售票系统:从订单到二维码检票的毕设实战

🕒 发布时间:2026/10/2 17:45:12 📁 来源:尧图网络
1. 项目整体定位为什么这套系统是毕设和练手的“性价比之王”1.1 从标题拆解系统边界先别急着写代码把标题拆开看一遍轨道交通自动售票系统、SpringBoot、JavaWeb、电子售检票、退订、订单管理。这里面藏着两件事——它既是一套“自动售票机背后的业务系统”也是一套“完整的电商订单体系”的变体。地铁售票和淘宝下单在核心逻辑上高度一致选商品选乘区间、锁定库存余票、生成订单支付、发货出票/生成二维码、售后退票退款只是换了个业务外壳。这也是为什么这类题目在毕设里经久不衰它足够真实有明确的业务闭环技术栈又是最主流的JavaWeb组合做完了能讲的东西非常多。从系统角色上分这套系统天然包含两类用户普通乘客购票、查票、退票、查历史订单和车站管理员审核退票、查看售检票统计、维护线路站点。能把这两套权限和界面都做清楚项目在答辩时的完整度就直接拉开一档。1.2 技术选型背后的决策逻辑题目直接点名了SpringBoot这就是主心骨。SpringBoot的核心价值在于“约定优于配置”它把SpringMVC、内嵌Tomcat、自动配置全部打包你不需要像老SSH时代那样写一堆XML一个带main方法的jar包就能跑起来。对毕设来说这大大降低了交付成本你不需要在答辩现场花十分钟配置环境启动即用。持久层我建议优先用MyBatis-Plus而不是原生MyBatis。为什么因为票务系统的订单表、线路表天然需要大量条件查询和分页MyBatis-Plus的LambdaQueryWrapper和分页插件能省掉至少三分之一样板代码。前端方面题目写的是JavaWeb实现你可以选两条路线传统Thymeleaf服务端渲染或者Vue前后端分离。我的建议是如果你是奔着“毕设稳过工作量饱满”去的直接用Thymeleaf Bootstrap就足够如果你后续想把这套项目写进简历去面Java岗位我强烈建议做前后端分离哪怕只是把管理后台做成Vue Element UI面试时能多聊十分钟。数据库选MySQL没悬念。需要注意的一点是这个项目的数据量不会大但表之间的关联关系要设计清楚这部分我们下一章展开。1.3 功能模块梳理与系统角色我把整套系统拆成六个模块所有功能不外乎这六块用户端注册登录、线路站点查询、购票下单、订单支付、电子票查看、退票申请票务核心余票管理、座位/票额分配、车票生成、二维码生成与校验检票模块闸机模拟、二维码扫码核验、进出站状态更新订单中心订单查询、支付状态、退票状态、订单超时取消管理后台售检票统计、线路站点管理、票价管理、退票审核系统支撑定时任务、异常处理、日志记录、权限拦截每个模块的职责边界要清晰不然写到后期你自己都会乱。比如“退票”这件事用户端只负责提交申请后台负责审核票额释放和订单状态变更必须放在同一个事务里这就是业务边界。2. 数据库与核心流程设计先把地基打好2.1 核心表结构设计数据库设计是这种业务系统最容易翻车的地方翻车不在表的数量而在字段语义混乱和状态流转缺失。这里我给出一个经过实测的简化模型六张核心表足够支撑整套系统线路表lineline_id主键、line_name线路名、start_station、end_station、运营时间。地铁线就一两行但你有高铁/轻轨扩展需求时这张表就值钱了。站点表stationstation_id、line_id外键、station_name、station_order。站点在一条线路上是有顺序的这个order字段直接决定了可乘坐区间怎么算。很多新手会忽略这个顺序字段导致后面算不出区间票。车票/班次表traintrain_id、line_id、train_date、start_time、end_time。别小看这张表地铁虽然没有固定班次但我们要模拟“当日票”和“单程票”的票额池它就能承载“某一天某个时段放多少票”的逻辑也让整个系统更接近真实的地铁调度模型。订单表ordersorder_id、order_no业务单号、user_id、train_id、start_station_id、end_station_id、ticket_count、total_amount、status0待支付、1已支付、2已检票、3已退票、4已取消、5已过期、create_time、pay_time。order_no一定要单独出来做业务单号不要用自增id暴露给用户这是老生常谈但要强调的事。车票表ticketticket_id、order_no、train_id、user_id、qrcode_url、status0未使用、1已检票、2已退票、3已过期。一张订单对应多张车票这就是订单和车票为什么是两个表的原因。用户表useruser_id、username、passwordBCrypt加密、phone、role0普通用户、1管理员。这套设计的核心思想是“一次购买动作拆成订单票两个层面”。订单管钱、票管实物电子票它们分开之后退票流程就非常顺退了订单就要处理所有关联车票可以单张退也可以整单退。2.2 从购票到检票再到退票的业务时间线拿一个完整场景走一遍流程用户在A站上车B站下车选择今天上午的“票额池”下单2张单程票。系统先判断当前车次的余票是否大于等于2满足则生成订单状态置为待支付同时用乐观锁扣减票额池的余票。用户模拟支付成功后订单状态变为已支付系统为这2张票生成电子凭证每张票附带一个二维码。乘客到达闸机口扫码系统校验二维码状态通过后把票状态改成已检票同时记录进站时间。如果乘客行程有变在未检票状态下可以发起退票系统校验是否在退票时限内通过后退款原路返回这里做模拟即可订单状态变更为已退票关联车票状态同步变更票额池的余票加回去。这条时间线就是系统的核心骨架所有代码都围着它转。答辩时你把这个流程讲清楚老师基本就能听懂你的设计接下来看你代码怎么落地。2.3 订单状态机设计状态机是这个项目的隐藏加分项。很多人的订单表只存一个status字段但代码里到处if判断改一个状态就要动用三四个地方的逻辑这样的代码一旦加需求就崩。正确做法是明确“谁能变成谁”待支付可取消用户主动、可过期超时未付、可支付已支付可退票未检票、可检票全部检完已检票不可退行程结束已退票终态已取消/已过期终态这个状态机确定之后你在写接口时就会发现逻辑特别“丝滑”——例如退票接口第一步先校验status是否等于已支付只要不是直接抛异常即可不用考虑一堆乱七八糟的边界。面试时能说出“我用状态机约束了订单流转”这句话比你说“我用了Redis缓存”更容易让面试官点头。因为前者体现的是业务建模能力后者只是技术点的堆砌。3. 关键功能实操与代码落地从零写出能跑通的模块3.1 购票与余票扣减的并发控制眼疾手快的正确姿势购票功能是整个系统的“心脏”。它看起来简单——查余票、减余票、存订单——但并发一上来就会出现超卖问题。什么是超卖就是票剩1张两个用户同时下单都通过了余票检查最后卖出去2张。这在毕设答辩时是高频拷问点。解决办法有三种方案你根据自己的项目进度选方案一最简单适合工期紧给车票表加一个余票字段扣减时用UPDATE语句的条件判断。关键SQL如下UPDATE train SET remain_tickets remain_tickets - 1 WHERE train_id ? AND remain_tickets 0这个SQL利用数据库的行锁天然保证同一时刻只有一个事务能成功扣减。受影响行数为0就说明余票不足下单失败。这是我在项目里推荐你用的方案没有引入额外组件又完整地解释了并发问题的解法。方案二进阶适合想展示技术深度引入Redis分布式锁。锁的key用train_id train_date获取锁成功后才能执行查余票和扣减逻辑。这个方案在答题时可以讲但实际写代码时要注意锁的释放放在finally块里防止异常死锁。方案三说明你懂高并发把库存预热到Redis用Redis的原子操作DECR扣减然后再异步同步回MySQL。这个方案工作量最大对于毕设来说属于“锦上添花”但如果你项目中的卖点正好是大促场景这个就必须得有。我个人建议选择方案一作为主实现然后面试或答辩时口头补充方案二和方案三的适用场景这样既保证项目能跑通又显得你思考过深度问题。3.2 电子票生成与二维码检票让系统“看得见摸得着”电子票这个功能是视觉上的亮点也是系统区别于普通CRUD的关键。每次购票成功后系统要生成一张电子票票面上有订单号、乘车区间、日期、座位码等信息并且附带一个二维码。这里我用ZXing库来生成二维码它的依赖引入很简单dependency groupIdcom.google.zxing/groupId artifactIdcore/artifactId version3.5.1/version /dependency生成二维码的核心逻辑是把票的唯一标识可以用订单号车票id拼接加密后塞进二维码里。这里要提醒一个细节不要把原始信息直接放入码中万一被同行扫出来他可以反推你的系统结构。最简单的处理方式是做一次签名例如把内容做哈希后加盐存入二维码。检票时先重新计算签名一致才允许放行。检票接口的伪代码如下public Result verifyTicket(String qrcodeContent) { // 1. 解析二维码内容得到 ticketId sign // 2. 校验 sign 是否合法哈希比对 // 3. 查询车票信息判断状态是否为未使用 // 4. 校验车票日期是否为当天 // 5. 将状态改为已检票记录检票时间 }这个流程每一步都可以在答辩时展开讲。尤其是“状态必须为未使用”这一步直接呼应了前面说的状态机设计一条线把所有逻辑串起来了。另外为了方便演示你可以在后台做一个模拟闸机页面用一个输入框粘贴二维码内容点击“检票”按钮就能看到校验结果比拿手机扫码好演示一百倍。3.3 退票业务与票额释放事务一致性是唯一的底线退票比购票更容易出错因为它的影响范围更大订单状态要变、车票状态要变、票额池要加回去、还要做退款流水记录。任何一个环节遗漏系统数据都会对不上。实现退票接口时我必须强调一个高频踩坑点事务一定要加在public方法上并且不能是同类中方法调方法。举个例子你在OrderService里定义了退票方法refund然后在同一个类里的另一个方法调用了refund这时Spring的Transactional会失效因为它走的是this调用而不是代理对象。正确做法是把退票逻辑放到独立的Service类或者用AopContext.currentProxy()获取代理对象再调用。这个坑在毕设答辩时几乎必踩提前避开了你就比大多数人稳。退票业务的伪代码大致如下Transactional(rollbackFor Exception.class) public Result refund(String orderNo) { Order order orderMapper.selectByOrderNo(orderNo); // 1. 校验订单状态必须是已支付否则直接拒绝 if (order.getStatus() ! OrderStatus.PAID) return Result.fail(当前状态不可退票); // 2. 修改订单状态为已退票 order.setStatus(OrderStatus.REFUNDED); // 3. 修改所有关联车票状态为已退票 ticketMapper.updateStatusByOrderNo(orderNo, TicketStatus.REFUNDED); // 4. 回补票额UPDATE train SET remain_tickets remain_tickets #{count} trainMapper.increaseRemainTickets(order.getTrainId(), order.getTicketCount()); // 5. 记录退款流水模拟 refundRecordMapper.insert(...); return Result.ok(); }这里特别说明一下回补票额为什么不用先查再改因为“先SELECT再UPDATE”在并发场景下会把另一个事务的扣减覆盖掉必须直接用UPDATE语句做原子自增。你可以在代码注释里写明这一点答辩老师看到这个注释就知道你理解并发印象分会差很多。3.4 管理后台与统计报表给答辩准备一份好看的成绩单管理后台是整个系统展示功能最直观的部分也是工作量最容易量化的地方。除了常规的线路站点维护、车次管理之外一定要做两个统计页面每日售票统计和线路客流分析。前者用柱状图展示最近7天每天的售票数、退票数、营收后者按线路维度展示哪条线卖得最多。数据统计的SQL并不复杂核心是掌握GROUP BY和DATE_FORMAT的配合SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(*) AS order_count, SUM(total_amount) AS revenue FROM orders WHERE status IN (1, 3) -- 已支付和已退票已退票算负营收的话要分开算 AND create_time DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY day ORDER BY day DESC统计图表我建议用EChartsCDN引入即可不需要额外安装什么。一个折线图上放三条数据售票数、检票数、退票数老师一看就能明白你的数据是通的。这个页面的技术含量不高但它能直观呈现你前面所有设计的成果所以千万别敷衍了事。答辩时间有限老师很多情况下就是看这个页面来判断你的系统“做成了没有”。另外管理后台需要加一个简单的登录拦截器。用SpringBoot的HandlerInterceptor在preHandle方法里判断session里有没有管理员标识没有就重定向到登录页。这里有个小细节放行路径要包含登录接口和静态资源否则会出现前端页面加载不出CSS的尴尬场景。我见过太多人在这上面卡半小时排查之后发现只是拦截器把静态资源拦了。4. 毕设现场高频踩坑与排查实录4.1 启动失败与依赖版本地狱SpringBoot项目最常见的启动失败场景一个是版本不兼容一个是端口被占用还有一个是MyBatis的Mapper扫描不到。版本问题现在尤其多发。很多教程用的还是SpringBoot 2.x你本地如果装了3.x会发现很多旧代码跑不起来例如javax.servlet要换成jakarta.servletRedis客户端从Jedis换成了Lettuce。我的建议是如果你对版本没有把握直接锁定SpringBoot 2.7.x JDK8的组合这个组合经过千锤百炼网上99%的问题都有解决方案。如果你坚持用SpringBoot 3.x那就要接受很多老教程不再适用的现实。这里没有高下之分能跑通交付才是第一原则。Mapper扫描不到的问题典型症状是启动时报Invalid bound statement (not found)。处理方式有且只有一个在启动类或配置类上加MapperScan(com.xxx.mapper)并且确保Mapper接口的类名与XML的namespace完全一致。顺便提醒如果你的实体类字段用了驼峰命名而数据库字段是下划线记得在application.yml里配置map-underscore-to-camel-case: true否则查出来全是null误以为SQL写错了排查一个小时才发现是这个配置的问题。4.2 定时任务重复执行和JSON循环引用订单超时未支付自动取消你大概率会想到用Scheduled。但注意本地调试时如果项目重启过SpringBoot默认是单机单实例不会出现重复执行问题。可如果你在演示时开了两个实例比如模拟集群那就要考虑分布式任务锁了。毕设阶段不需要上分布式组件但你要能说出“单机定时任务有重复执行的隐患正常生产会引入XXL-JOB或Redis分布式锁”这种话这就是加分项。JSON循环引用是控制器返回数据时最隐蔽的坑。典型场景订单对象里含User对象User对象里又含List 序列化成JSON时直接栈溢出。解决办法很简单在实体类的关联字段上加JsonIgnore或者在字段上标记JsonProperty(access Access.WRITE_ONLY)。我的习惯是统一以VO视图对象的形式返回数据把实体和前端展示彻底解耦。虽然多写几个类但长远看省心太多——你的订单表操作日志、支付回调这些字段根本不应该返给前端。4.3 日期时间处理与支付模拟日期处理是这个项目另一个绕不开的坑。Java 8之前的Date、Calendar API设计得反人类SpringBoot 2.x默认又引入了Jackson对LocalDateTime的支持但序列化格式默认是数组格式前端根本不好解析。解决方案是全局配置Jackson的日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai同时实体类里的日期字段直接用LocalDateTime别再用java.util.Date。数据库字段用datetime类型。这样从数据库到JSON全链路都是干净的字符串格式前端不用做额外转换。支付模拟就简单了不需要真接支付宝微信。做一个“模拟支付页面”用户点击支付时直接调用你自己的回调接口让订单状态变成已支付即可。但你要在代码里预留真实的回调入口哪怕只是注释掉。答辩时如果有老师问“支付怎么接入真实渠道”你能说出“真实场景是支付成功后会异步回调商户平台的通知地址我这里是模拟了回调过程”这一句话就把问题的档次拉开了。4.4 一个拯救过无数毕设的“保底演示方案”最后分享一个我见过太多人翻车的场景现场演示时数据库连不上、服务起不来、浏览器打不开页面。这个问题很多时候不是代码质量问题而是演示环境的意外。我的建议是答辩前准备一套完整的“保底演示方案”本地MySQL用固定端口3306账号root密码保持最简单项目启动时把数据库初始化脚本写在SpringBoot的sql.init配置里让它自动建表建数据省得手动导入SQL前台和后台各准备一个测试账号用户密码写死在说明文档里服务启动方式用mvn spring-boot:run或打包成jar双击运行不要依赖IDEA的绿色小三角。更极致一点你可以把整个项目制作成Docker Compose一键启动数据库、Redis、后端三件套全部容器化。虽然工作量多了点但你想象一下答辩现场老师问你项目怎么启动你说“docker compose up -d”一行命令所有服务就绪这种震撼力比任何PPT都有效。而且这个技能在你入职后做本地环境搭建时绝对能用上。说回项目本身。如果你时间充裕还可以追加几个锦上添花的扩展功能比如多线路换乘方案查询、地铁客流热力图、基于用户画像的购票推荐。这些功能既有业务价值又有展示价值难度却都在可控范围内。真正做下来你会发现这个题目最值钱的地方不在于技术有多深而是它让你完整经历了一遍“从需求分析到数据库设计再到接口实现和前端展示”的软件工程全流程这套能力是刷一百道面试题都换不来的。最后再给大家交个底这个系统我第一次做的时候前前后后花了三周其中有四天是在改数据库表结构和处理各种启动异常真正写业务代码的时间其实只有十天左右。所以心态放平遇到bug很正常你踩过的每一个坑最终都会变成答辩时的谈资。这套系统做扎实了你不仅能顺利毕业还能在面试时把它讲成一段有血有肉的项目经历。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →