资讯动态

基于Spring Boot的车票预订系统设计与实现:从数据库到并发控制

发布时间:2026/10/14 23:53:23 来源:尧图企业网站定制
简介一份基于WEB的车票预订信息系统设计与实现的项目资源采用Java、Servlet与JSP技术栈搭配MySQL数据库适合Java Web学习者用于毕业设计、课程设计或项目实战。压缩包共1337个文件包含JSP页面、Java源码、前端CSS/JS与图片素材、jar依赖库及SQL数据库脚本整体大小55.82MB内附运行配置与使用说明便于快速搭建环境。项目基于MyEclipse开发通过实际案例完整展示了车票信息展示、用户预订、订单管理等核心业务逻辑。已有1105人学习下载。获取后可获得完整源码、数据库文件和页面素材对照学习可系统掌握Servlet与JSP协作、JDBC数据库操作及Web分层开发思路对提升实际项目开发能力有明显帮助。1. 基于WEB的车票预订信息系统课程设计里的高频题目为什么值得认真做“基于WEB的车票预订信息系统设计与实现”大概是很多人在毕业设计或课程设计里见过的最经典题目之一。表面上是做一个网上卖票的增删改查但真正动手会发现它比学生管理系统多了一道坎余票是共享资源多人同时下单时要保证不超卖订单要能超时失效车次、站点、日期这些维度还要能组合查询。把这些点做完基本就懂了一个Web业务系统从数据库到前端的完整链路。所以这个题目值得认真做而不是单纯交一份能跑的代码。它适合第一次独立做全栈系统的新手也适合想拿老项目重构、补并发和工程化细节的熟手。2. 先把业务理清角色、流程和数据库设计拿到这种题目我的习惯是先不开电脑写代码而是把业务流程在纸上过一遍。车票预订系统最常见的翻车点不是某个接口不会写而是角色和状态没有定义清楚一张订单表里塞了互相矛盾的信息。所以这一章先解决“系统里有哪些人、哪些状态、哪些表”的问题。2.1 角色与核心流程车票预订不是加减乘除先拆角色。用户端比较简单游客能看车次和余票注册登录后才能下单下单之后要能支付或取消还能查看自己订过的票。管理员这边负责维护车次、站点、票价并对当天的订单做简单统计。不要一上来就给管理员做一个复杂的权限菜单先把上面这几条路走通。核心流程有四条顺序也很重要。查询车次根据出发站、到达站、出发日期列出当天可预订的车次和余票。下单购票用户选择一个车次和座位类型提交乘车人数量和区间系统扣减余票并生成待支付订单。支付/取消模拟支付成功后订单变为已支付超过时限未支付订单自动取消并回补余票。管理端维护新增车次、录入站点、设置每个车次的票额。查询流程虽然在最前面但真正的难点在下单。同一个车次同一日期余票数量是全局共享的两个用户同时提交订单时必须保证扣减后的结果不超过总数否则最后谁都没法正常出行。这也是为什么这个题目比简单增删改查“高一级”理解这一点之后再开始设计表。给角色和状态做一张对照表后面写代码时就不会混淆。订单状态通常定义为0 待支付1 已支付2 已取消3 已退票。支付只做模拟不接真实渠道重点是让状态能流转。角色主要动作涉及数据游客查车次、查余票train, ticket_stock注册用户登录、下单、支付、取消user, order_info管理员维护车次、站点、票额train, train_station, ticket_stock2.2 技术选型Spring Boot MyBatis Thymeleaf 为什么合适这个题目网上能找到的老项目大多是 JSP Servlet 的写法。JSP 不是说不能做但今天再做新项目我的建议是换成 Spring Boot MyBatis Thymeleaf。原因很实际第一Spring Boot 内置 Tomcat本地用java -jar就能跑不用再配置外部服务器部署也方便第二MyBatis 的动态 SQL 很适合车次查询这种“条件不确定”的场景第三Thymeleaf 作为服务端渲染模板语法和 HTML 接近页面跳转、表单提交的写法和传统 Web 方案一致没有前后端分离的额外复杂度。技术组合可以做一张选型说明表方便中途接手的人理解每个组件负责什么。组件作用选择理由Spring Boot 2.7依赖管理、自动化配置上手快内置 Tomcat社区资料多MyBatis数据访问层动态 SQL 灵活SQL 可读性好Thymeleaf页面模板服务端渲染和表单/会话耦合自然MySQL 8.x存储免费事务支持可靠HikariCP连接池Spring Boot 默认集成性能好版本要提前定好。我用的是 Spring Boot 2.7.18 JDK 8MySQL 用 8.x。如果你的环境是 JDK 17可以直接换 Spring Boot 3.x但 MyBatis 的 starter 也要换成对应的版本否则启动时会报找不到类。这类问题属于“刚开始就翻车”的高频点所以建议新手直接照一个固定组合来。2.3 数据库设计五张表把票务系统的核心关系拆开车票系统的表不宜太少太少会出现万能表也不宜一开始就按真实票务系统搞区间库存那样会把课程设计做成算法题。我建议用五张核心表用户表、站点表、车次表、车次经停表、订单表再加上一张库存表。下面是一份可以落地的建表 SQL。CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT 建议存储加盐哈希, real_name VARCHAR(50), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_station ( id BIGINT PRIMARY KEY AUTO_INCREMENT, station_name VARCHAR(50) NOT NULL, city VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT站点表; CREATE TABLE t_train ( id BIGINT PRIMARY KEY AUTO_INCREMENT, train_no VARCHAR(20) NOT NULL UNIQUE, train_type VARCHAR(20) COMMENT 高铁/普快等, start_station_id BIGINT NOT NULL, end_station_id BIGINT NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车次表; CREATE TABLE t_train_station ( id BIGINT PRIMARY KEY AUTO_INCREMENT, train_id BIGINT NOT NULL, station_id BIGINT NOT NULL, stop_order INT NOT NULL, arrive_time VARCHAR(10), depart_time VARCHAR(10) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车次经停表; CREATE TABLE t_ticket_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, train_id BIGINT NOT NULL, run_date DATE NOT NULL, seat_type VARCHAR(20) NOT NULL COMMENT 二等座/一等座/硬座, total_tickets INT NOT NULL, left_tickets INT NOT NULL, version INT DEFAULT 0, UNIQUE KEY uk_train_date_seat (train_id, run_date, seat_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车次每日库存表; CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(40) NOT NULL UNIQUE, user_id BIGINT NOT NULL, train_id BIGINT NOT NULL, run_date DATE NOT NULL, start_station_id BIGINT NOT NULL, end_station_id BIGINT NOT NULL, seat_type VARCHAR(20) NOT NULL, ticket_count INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已取消 3已退票, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, expire_time DATETIME NOT NULL COMMENT 订单过期时间, pay_time DATETIME, cancel_time DATETIME, KEY idx_user_id (user_id), KEY idx_train_date (train_id, run_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;这套设计最核心的特点是把“车次基础信息”和“某天某车次的票额”分开。t_train只存车次和起终点t_train_station存经停顺序t_ticket_stock存某一天的具体票额。管理端每天开卖前只需要往库存表里插入几行记录不需要改车次表。需要注意几个细节。第一所有表都用 InnoDB 和 utf8mb4别图省事用默认的 MyISAM否则事务和中文都容易出问题。第二t_ticket_stock上加了uk_train_date_seat唯一键保证同一车次同一天同一种座位类型只有一条库存记录扣减时按这个条件更新就行。第三订单表里的start_station_id和end_station_id是从用户提交的查询条件带过来的方便展示车票区间库存扣减时会校验这个区间是否在车次经停表里存在但不会按区间拆库存。这里的简化方案是一趟车某天的所有余票共用一个池子不区分你从哪一站上、哪一站下。它能正确防止超卖代码也清晰。缺点是不符合真实票务的“区间限售”比如一趟车长途票原本有 100 张被大量短途订单买完后长途旅客反而没票。如果题目没有明确要求按区间控票这个简化足够如果老师要求做区间余票那就需要把库存表拆成站段我在第 5 章的避坑里再展开。库存表里的version字段目前没有参与扣减逻辑因为条件更新本身已经能防超卖。留这个字段是给你以后做乐观锁或数据对账时用的比如后台批量调价时扫描版本变化。3. 核心代码落地车次查询、余票扣减与订单生成业务表和状态定好之后就可以动手写代码了。这一章不讲完整的代码生成器只挑三个最值得写的点项目骨架、动态查询、事务扣减。把这三点吃透剩下的页面跳转和表单提交都是体力活。3.1 Maven 项目结构把包拆清楚后面少改目录我习惯按 controller、service、mapper、entity、common、config 分包。entity 放数据库实体mapper 是 MyBatis 的接口和 XMLservice 写业务逻辑controller 只做参数接收和页面转发。common 放统一返回结果和异常。config 放拦截器、日期转换等配置。这样每个类位置固定排查问题时不用到处找。先说 pom.xml 依赖无需把所有坐标背下来但核心的几个要明白用途。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies说明一下参数mybatis-spring-boot-starter的 2.3.2 是适配 Spring Boot 2.7 的稳定版本如果换成 3.x就改用 mybatis-spring-boot-starter 3.x 并配合 JDK 17。mysql-connector-j是 MySQL 8 的驱动坐标老资料里写的mysql-connector-java在 2.7 里也能用但建议跟随当前默认。Lombok 只是为了少写 getter/setter如果你不习惯注解可以不引入手写实体类也完全没问题。然后是 application.yml 里的关键配置。spring: datasource: url: jdbc:mysql://localhost:3306/train_ticket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 hikari: maximum-pool-size: 10 thymeleaf: cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.train.entity logging: level: com.example.train.mapper: debug这些配置里最常被坑的是 URL 参数。useUnicodetruecharacterEncodingutf8负责中文编码serverTimezoneAsia/Shanghai负责日期时间少了任何一个都可能遇到乱码或 SQL 时区异常。mapper-locations指定 XML 路径type-aliases-package让 XML 里可以直接写实体类名不用写全限定名。3.2 车次查询接口动态 SQL 是 MyBatis 最顺手的地方车次查询的搜索条件很典型用户可能只填出发地也可能三个条件都填。用 MyBatis 的where和if可以拼出合适的 SQL不需要在 Java 代码里做字符串拼接。来看 XML 里的查询写法。select idselectByCondition resultTypeTrain SELECT t.id, t.train_no, t.train_type, t.start_station_id, t.end_station_id FROM t_train t where if teststartStationId ! null AND t.start_station_id #{startStationId} /if if testendStationId ! null AND t.end_station_id #{endStationId} /if if testrunDate ! null AND EXISTS ( SELECT 1 FROM t_ticket_stock ts WHERE ts.train_id t.id AND ts.run_date #{runDate} AND ts.left_tickets 0 ) /if /where ORDER BY t.train_no /select这段 SQL 的参数是查询请求对象里的startStationId、endStationId和runDate。要注意的是runDate是日期不是字符串所以判断条件用了! null而不是! 。用EXISTS比先查出所有车次再去代码里过滤余票要高效而且在数据库层就把“当天没票的车次”滤掉了下面的代码不需要再做额外判断。对应 Mapper 接口只需要一行声明。ListTrain selectByCondition(Param(startStationId) Long startStationId, Param(endStationId) Long endStationId, Param(runDate) LocalDate runDate);查询结果里如果要展示“余票数量”可以单独查一次库存表也可以在查询里直接关联。我更倾向单独查因为库存表在同一天里同一车次有多条座位记录直接 join 会让车次记录重复。Controller 里用一个循环把每个车次的各座位余票填进去逻辑更直白余票数字也不会错。3.3 余票扣减与订单生成两步必须放进一个事务下单是整套系统的核心也是最容易写错的地方。错误写法是先查一次库存判断 left_tickets 足够再执行 update 减一。这种写法在并发下会超卖因为两个请求同时查到 left_tickets 1都认为自己可以买最后库存可能变成 -1。正确的做法是把扣减写成一条条件更新让数据库自己判断“当前余票够不够”。Service 里的下单方法我一般这样写。Transactional(rollbackFor Exception.class) public OrderInfo createOrder(CreateOrderReq req) { // 1. 通过当前登录用户信息补全 user_id Long userId UserContext.getUserId(); // 2. 扣减余票注意这里是条件更新 int rows ticketStockMapper.deductStock( req.getTrainId(), req.getRunDate(), req.getSeatType(), req.getTicketCount()); if (rows 0) { throw new BizException(余票不足请更换车次或日期); } // 3. 生成订单 OrderInfo order OrderInfo.builder() .orderNo(generateOrderNo()) .userId(userId) .trainId(req.getTrainId()) .runDate(req.getRunDate()) .startStationId(req.getStartStationId()) .endStationId(req.getEndStationId()) .seatType(req.getSeatType()) .ticketCount(req.getTicketCount()) .totalAmount(calcAmount(req)) .status(0) .expireTime(LocalDateTime.now().plusMinutes(30)) .build(); orderMapper.insert(order); return order; }对应的 Mapper 更新语句是这一条也是整个系统防超卖的关键。update iddeductStock UPDATE t_ticket_stock SET left_tickets left_tickets - #{count}, version version 1 WHERE train_id #{trainId} AND run_date #{runDate} AND seat_type #{seatType} AND left_tickets #{count} /update逻辑说明UPDATE的行锁和left_tickets #{count}条件一起生效。当两个并发请求同时执行这条 SQL 时数据库会串行化处理同一行数据前一个把 left_tickets 减掉后一个再次执行时条件不再满足影响行数为 0于是抛出业务异常订单也不会生成。这里没有使用version因为它已经在条件里控制了余票数version 字段只是辅助不是必须。参数说明#{count}是本次购票数量默认 1。seat_type是二等座、一等座或硬座要和查询时传入的座位类型保持一致。run_date是LocalDate类型MyBatis 会自动和 MySQL 的 DATE 映射不需要额外转换。需要注意扣减库存和生成订单必须在同一个事务里。这里Transactional(rollbackFor Exception.class)保证了扣减成功但插入订单失败时扣减会被自动回滚。要记住事务别加在 Controller 层否则拦截器、参数校验和事务锅会糊在一起。3.4 订单超时释放状态机与定时回补订单生成后是待支付状态如果用户一直不付票就应该重新放回去。最简单可靠的方案是给订单设置expire_time用一个定时任务扫描并取消超时订单。任务逻辑看起来简单但要注意“回补只能执行一次”。Scheduled(fixedDelay 120000) Transactional(rollbackFor Exception.class) public void cancelExpiredOrders() { ListOrderInfo expiredList orderMapper.selectExpiredOrders(); for (OrderInfo order : expiredList) { int rows orderMapper.compareAndUpdateStatus( order.getId(), 0, 2); if (rows 1) { ticketStockMapper.revertStock( order.getTrainId(), order.getRunDate(), order.getSeatType(), order.getTicketCount()); } } }这里的compareAndUpdateStatus是关键。update idcompareAndUpdateStatus UPDATE t_order SET status #{newStatus}, cancel_time NOW() WHERE id #{id} AND status #{expectStatus} /update逻辑说明定时任务每隔两分钟跑一次查出所有待支付且超过 expire_time 的订单。更新订单状态时强制要求当前状态还是 0影响行数为 1 才执行回补。这样即使两个定时任务节点同时启动也不会对同一张订单回补两次。这个“先更新后回补”的顺序很重要先回补再更新状态的话一旦更新失败就会造成库存被重复加回。定时任务启动开关是在启动类上加EnableScheduling。如果不想依赖固定频率也可以把订单号放进延迟队列但课程设计里定时任务已经足够清晰。4. 前端交互与接口联调从页面到数据库的完整链路后端接口调通后系统还不能给用户用因为多数用户不会直接看 JSON。这一章把页面和接口串起来重点是 Thymeleaf 模板怎么组织、表单提交和 Ajax 的区别、以及登录拦截器怎么守住下单接口。这一章做完整个链路才真正闭合。4.1 页面骨架Thymeleaf 模板和静态资源目录Spring Boot 的 Thymeleaf 默认从src/main/resources/templates读模板静态资源放在src/main/resources/static。页面布局可以用一个公共 header也可以像下面这样直接在单个页面里写完整表单。对于课程设计不建议引入太重的前端工程化框架Bootstrap 加原生 Thymeleaf 足够。车次查询页的核心是那个查询表单。form th:action{/train/query} methodget div classform-group label出发站/label select namestartStationId classform-control option value请选择/option option th:eachs : ${stationList} th:value${s.id} th:text${s.stationName}/option /select /div div classform-group label到达站/label select nameendStationId classform-control option value请选择/option option th:eachs : ${stationList} th:value${s.id} th:text${s.stationName}/option /select /div div classform-group label出发日期/label input typedate namerunDate classform-control required / /div button typesubmit classbtn btn-primary查询车次/button /form这段模板的逻辑很简单th:each遍历站点列表生成option提交后 Controller 用RequestParam接收参数调用查询接口把结果放进 Model。注意表单用的是 GET这样查询结果可以分享链接也符合查询操作的语义。日期输入框用typedate浏览器自带日期选择器后端接收类型是LocalDate。结果列表我用一个简单的th:each循环展示每行显示车次、出发站、到达站和各座位余票。余票数据在 Controller 里组装成一个TrainVO里面包含ListSeatStock模板直接取字段就好。不要在模板里写复杂循环判断可读性会迅速下降。4.2 异步下单Ajax 提交比表单提交更稳下单按钮有两种做法一种是普通表单 POST页面跳转到支付页另一种是 Ajax 提交返回 JSON成功后在浏览器端跳转。我更推荐 Ajax因为可以统一处理“余票不足”“未登录”这些错误提示不会因为服务器返回错误页面而打断操作流程。前端 JavaScript 片段如下。$(#submitOrder).click(function () { var params { trainId: $(#trainId).val(), runDate: $(#runDate).val(), seatType: $(#seatType).val(), ticketCount: $(#ticketCount).val(), startStationId: $(#startStationId).val(), endStationId: $(#endStationId).val() }; $.ajax({ url: /order/create, type: POST, contentType: application/json, data: JSON.stringify(params), success: function (res) { if (res.code 0) { location.href /order/pay/ res.data.orderNo; } else { alert(res.msg); } }, error: function () { alert(网络异常请稍后再试); } }); });对应的 Controller 方法用RequestBody接收 JSON返回统一结构ResultT。要注意runDate传到后端是字符串需要在 JS 里保证格式是yyyy-MM-dd否则后端的LocalDate会解析失败。如果出现日期解析错误通常不是 Java 的问题而是前端提交的格式带了时分秒。下单成功返回订单号前端直接跳到支付页。支付页也做简单处理点击“模拟支付”就会把订单状态从 0 改成 1同时记录支付时间。这里不接真实支付渠道只需要保证状态能正确更新。4.3 登录拦截器把下单和支付接口保护起来注册用户可以打开下单页但未登录用户一点“提交订单”就应该被拦下来。用 Spring MVC 的拦截器可以做到不用在每个 Controller 里重复判断 session。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getSession().getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login); return false; } return true; } }注册拦截器时需要指定拦截路径。通常把/train/query放行把/order/**、/user/center/**拦截掉。如果用 Java 配置写法如下。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/order/**, /user/center/**) .excludePathPatterns(/login, /register, /train/query); } }注意页面跳转时sendRedirect需要加上request.getContextPath()否则部署到带项目路径的环境时会跳错地址。登录后把用户对象放进 session下单接口从 session 里取userId这样可以避免前端伪造用户身份。到这一步一个用户可以登录、查车次、下单、支付管理员可以维护车次和票额核心链路已经完整。下一步要做的不是继续加功能而是回头检查最容易出问题的并发、编码和部署细节也就是下一章的避坑内容。5. 避坑指南并发超卖、乱码与区间库存模型的 5 个常见问题这一章记录我在做同类题目时真实踩过的坑每条都是“现象、原因、解决”三段可以直接照着排查。这些坑里有并发问题、编码问题也有设计简化带来的公平问题。说是用这篇笔记做系统的避坑清单也不为过。5.1 并发下单后余票变成负数现象用 JMeter 模拟 50 个用户同时提交同一车次订单数据库里left_tickets出现负数订单表里生成的订单数量超过了实际票数。原因服务层先用select查出余票判断left_tickets 0后执行update减一。这两个操作中间有间隙多个线程同时读到同一个余票数各自的判断都通过最后一起更新库存就超了。本质上是“先检查后更新”没有加锁。解决把扣减逻辑写成一条条件更新UPDATE t_ticket_stock SET left_tickets left_tickets - #{count} WHERE ... AND left_tickets #{count}。数据库的行锁会让同一条库存记录在并发下串行执行影响行数为 0 的请求直接判为余票不足。这条 SQL 是这个系统里最重要的防呆逻辑也可以尝试用SELECT ... FOR UPDATE悲观锁但会给课程设计引入显式锁代码更绕我建议优先用条件更新。5.2 页面和数据库中文全部乱码现象刚启动系统时填入中文用户名后页面显示乱码数据库里存储的也是乱码。原因三层编码不一致。第一Java 文件本身用了 GBK第二MySQL 连接 URL 没有指定characterEncodingutf8第三数据库表创建时用了默认的 latin1 字符集。解决统一为 UTF-8。工程文件在 IDE 里改成 UTF-8 保存连接 URL 加上useUnicodetruecharacterEncodingutf8建表时统一使用DEFAULT CHARSETutf8mb4。如果数据库已经建好可以执行ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4但要注意已有数据也可能需要重新导入。检查顺序建议是先看连接 URL再看表字符集最后才看 IDE 或编辑器的文件编码。执行SHOW CREATE TABLE t_user;能直接看到表的默认字符集定位很快。5.3 前端日期参数解析失败看起来是数据库问题其实卡在参数绑定现象查询车次时填了日期后提交后端报 400 错误日志里写着Failed to convert value of type java.lang.String to required type java.time.LocalDate。原因前端typedate提交的字符串是2024-06-01但 Spring MVC 默认无法把字符串直接转成LocalDate需要注册一个Formatter。很多同学把报错当成数据库问题其实在 Controller 参数绑定阶段就已经失败了。解决在配置类里加一个全局日期转换器。可以用DateTimeFormat(pattern yyyy-MM-dd)加在 Controller 参数上也可以注册一个ConverterString, LocalDate在全局生效。我建议用全局转换器这样实体和请求参数都不用每个字段写注解。常见做法是实现WebMvcConfigurer接口在addFormatters里注册转换器或者直接用DateTimeFormat应付临时测试。两种都行选一种保持全工程一致。如果你换到 Spring Boot 3.x注意转换器相关的包路径变了不要照搬旧代码。5.4 定时任务把订单重复取消、库存重复回补现象部署后跑了一晚上第二天发现部分订单状态是已取消但库存被加回了两次余票数量超过初始票额。原因定时任务扫描超时订单时先回补库存再更新订单状态。如果更新订单状态失败或者任务被重复触发同一张订单的库存会被回补两次。问题就出在“回补”和“更新状态”的顺序和条件上。解决先用条件更新把订单状态从 0 改成 2靠影响行数判断这次任务是否拿到了这张订单的处理权。只有rows 1才执行回补。这样即使任务重复执行第二轮更新时状态已经不是 0影响行数为 0自然不会再回补。另外定时任务方法上建议也加事务保证状态更新和库存回补要么都成功要么都回滚。排查重复回补时可以在日志里打印订单号和处理前状态能清楚看到第二次任务是否跳过。5.5 整趟车共用一个余票池短途订单挤占长途现象某车次从 A 站到 D 站总票只有 20 张。大量 A 到 B、B 到 C 的短途订单先买走了 19 张真正从 A 到 D 的旅客再查时只剩 1 张甚至无票。原因前面设计的t_ticket_stock是按“车次 日期 座位类型”存一个总余票池不管订单的出发站和到达站。它能防超卖但无法区分区间占用短途乘客会把长途车票也消耗掉。解决如果题目要求做真实的区间余票最简单的升级是把库存粒度拆成“相邻站段”。比如车次有 A-B-C-D 四站准备 A-B、B-C、C-D 三段库存下单从 A 到 D 时同时检查和扣减 A-B、B-C、C-D 三段每段余票都必须大于等于购票数扣减也要放在同一个事务里。这样短途订单只影响它经过的站段长途订单不会被直接挤掉。代价是初始化库存的代码量和维护成本明显增加先把基础版本跑通再决定要不要升级到区间模型。这些坑都属于“看着不起眼实际吞时间”的类型。调试这类问题不要靠猜先在代码里加日志看清楚 SQL 真正执行了什么很多时候答案就在日志里。6. 验证与收尾让课程设计具备工程感的三个动作功能跑通之后多数人的第一反应是准备写文档。我的建议是先花半小时做三个动作打开 SQL 日志、并发压测、检查事务边界。这三个动作不增加功能但能把系统从“能跑”变成“能说清楚为什么这么设计”也是答辩或面试时最容易被追问的部分。第一个动作把 MyBatis 的 SQL 日志打开确认扣减和回补语句真的带条件执行。logging: level: com.example.train.mapper: debug执行一次下单看日志里是否出现left_tickets ?这样的条件。如果没有说明 SQL 拼写或参数传递有问题尽早改不要等到验收时才发现库存对不上。第二个动作写一个并发验证脚本。不需要复杂的压测工具只要能模拟多个线程同时请求下单接口就行。我一般用 JMeter 开 50 个线程循环 5 次结束后对比订单总数和库存扣减总数。如果订单成功数加剩余票数不等于初始票数说明事务或扣减逻辑还有问题。这一步比写单元测试更贴近真实故障。第三个动作检查事务边界。打开所有Transactional标注的方法看事务里是否只有数据库操作。常见错误是在事务里调用远程接口、发送短信、或者做耗时的文件读写白白把事务时间拉长最后连接池被占满。遇到这种需求先把非数据库操作挪到事务外执行。我自己在这个题目上吃过大亏。第一次做的版本把“查询余票”和“扣减库存”放在两个没有事务关联的方法里线下测试怎么点都没问题上线后我用两个浏览器窗口同时下单余票立刻变成了负数。后来把扣减改成了单条条件更新又在定时任务里加了状态判断才算真正解决了问题。也是从那以后凡是涉及共享数字变更的地方我都会写一句条件更新并且用并发方式验证一遍而不是只在功能页面上点几次。这三个动作做完这个系统已经从“能跑”向“能演示、能说明白”迈了一步。以后再遇到类似的选座、秒杀、预约场景这套事务和条件更新的思路可以直接复用。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑