新闻详情

新闻详情

首页 / 资讯中心 / 详情

学生火车票订票系统:Spring Boot+MyBatis实战与并发防超卖

发布时间:2026/9/25 6:43:42来源:尧图网络
学生火车票订票系统:Spring Boot+MyBatis实战与并发防超卖
简介一份基于JAVAEE的学生火车票订票系统完整项目资源面向Java Web学习者、毕业设计选题学生及需要快速搭建订票类管理系统的开发者。系统覆盖后台车次维护、学生优惠、团购购票等典型功能有助于理解Servlet/JSP、MVC分层、数据库交互等企业级开发流程。压缩包共318个文件、约24.27MB包含72个jar依赖库、38个java源文件与对应class文件、33个jsp页面及css/js/gif等前端素材并附有sql数据库脚本便于部署调试。资源还整合了完整源码与页面素材目录结构清晰可直接作为课程设计或实训项目参考。已有3248人学习下载适合用来对照实现订票核心模块、掌握JavaWeb项目从设计到落地的整体思路。1. 学生火车票订票系统JAVA课程设计里最值得复现的实战项目一到期末不少同学从网上找JAVA课程设计案例源码题目不是图书管理就是学生信息做出来自己都不太愿意打开浏览器。学生火车票订票系统不一样它身上带着真实的业务约束余票不能卖超、退票要把座位还回去、同一个车次不能重复下单。这几条正好把JAVA基础里的面向对象、集合、JDBC串起来再往上是Spring Boot、MyBatis、事务管理一套下来课程设计和面试项目都有了着落。这篇文章从建表、登录、订票一路写到并发防超卖把整个系统的落地路径拆开讲同时标出那些容易翻车的参数和坑。适合正在做课程设计、期末作业以及想给简历加一个带业务深度的Web项目的读者。2. 技术选型与项目骨架为什么学生火车票订票系统用Spring Boot MyBatis如果你在网上搜学生火车票订票系统源码会看到一半是JSPServlet一半是SSM还有一小部分已经切到Spring Boot。并不是老方案不能跑而是给学生项目选技术栈要看三个指标学习成本低、资料多、演示时不容易翻车。Spring Boot在这三点上几乎都占优所以下面不是把所有方案罗列一遍而是直接给出我一般会推荐的组合Spring Boot MyBatis JSP并把每一步为什么这样选讲清楚。2.1 三种主流方案对比为什么不是JSPServlet也不是SSH方案环境配置事务支持资料数量对新手友好度JSPServlet手动配置Tomcat手动管理Connection多但乱一般SSM大量Spring XML注解XML多一般Spring Boot MyBatis自动配置内嵌TomcatTransactional很多高JSPServlet适合JAVA基础刚结课的时候所有逻辑都能用HttpServletRequest和HttpServletResponse完成但到了订票这一步你要手动管理数据库连接、手动处理字符编码、手动控制事务提交。不是不能做而是精力全耗在了非业务的地方。SSM是经典企业开发组合但Spring的XML配置和SpringMVC的组件扫描对只有JAVA基础的人来说太抽象往往项目还没跑起来人先被配置文件劝退了。Spring Boot把自动配置做到极致默认内嵌Tomcat一个Application类即可启动它帮你自动装配DataSource和MyBatis工厂你只需要关注Mapper接口和SQL语句。那为什么持久层选MyBatis而不选JPA因为火车票系统里有明确的SQL比如扣余票要写成UPDATE train SET remain_tickets remain_tickets - 1 WHERE id ? AND remain_tickets 1这种带条件的更新写在MyBatis的XML里一目了然。JPA也能做但新手在实体关系、级联操作上很容易触发意外更新。MyBatis的SQL作者可控排查直观答辩时老师问「你的SQL怎么写的」你直接打开XML讲就行。页面用JSP还是Thymeleaf我推荐JSP。网上大量JAVA课程设计案例源码本身就是JSP遇到问题容易对照。缺点是Spring Boot对JSP支持需要额外加tomcat-embed-jasper和jstl依赖并配置视图解析器。如果你完全不想碰JSPThymeleaf也行只是要额外学一套表达式语法这看个人时间。2.2 依赖配置pom.xml里一版不会出错的写法下面是一份常用的依赖配置版本尽量选你本地JDK能匹配的稳定版。我用Spring Boot 2.7.18举例它支持JDK8和JDK11学校机房和老电脑基本都能跑。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web启动器内嵌Tomcat、SpringMVC -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis官方Spring Boot适配器 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency !-- MySQL驱动Spring Boot管理版本不需要写版本号 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 如果使用JSP页面这两个依赖必加 -- dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-jasper/artifactId scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId /dependency /dependencies注意几个细节mybatis-spring-boot-starter不是Spring Boot官方starter它不随parent管理版本所以必须手动指定如果用的是Spring Boot 3.x则需要匹配JDK17和Jakarta包很多学生电脑还没升到那个环境不建议第一版就上。mysql-connector-j在2.7.x里由父依赖管理可以不写版本号如果你在更老的Spring Boot版本里驱动的类名要改成com.mysql.jdbc.Driver否则启动时会报找不到驱动。2.3 项目分包和配置文件一眼看出是用心做的项目拿到一个项目源码我会先看包结构。Controller、Service、Mapper、Entity四层分开比把代码全塞在Controller里要专业得多。下面是推荐的目录src/main/java/com/example/train/ ├── TrainApplication.java ├── config/ │ └── WebConfig.java ├── controller/ │ ├── UserController.java │ └── TrainController.java ├── service/ │ ├── UserService.java │ └── TrainService.java ├── mapper/ │ ├── UserMapper.java │ ├── TrainMapper.java │ └── OrderMapper.java └── entity/ ├── User.java ├── Train.java └── Order.java src/main/resources/ ├── application.yml └── mapper/ ├── UserMapper.xml ├── TrainMapper.xml └── OrderMapper.xml src/main/webapp/WEB-INF/jsp/ ├── login.jsp ├── trainList.jsp └── orderList.jsp包名不要用test这种含义不明的词。控制层只负责接收参数和返回视图业务层处理规则数据访问层写SQL答辩时你能准确说出这三层各自的职责这就是java基础里被反复提到的分层思想。application.yml里这几个参数值得逐项注意server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/train_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mvc: view: prefix: /WEB-INF/jsp/ suffix: .jsp mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.train.entity configuration: map-underscore-to-camel-case: trueserverTimezoneAsia/Shanghai解决数据库和本地时间差8小时的问题characterEncodingutf8解决中文乱码map-underscore-to-camel-case让数据库的real_name字段自动映射成Java实体里的realName属性。这三项是后续避坑章的主角现在配好了能省掉很多麻烦。启动类其实就三行但它是整个项目的入口SpringBootApplication MapperScan(com.example.train.mapper) public class TrainApplication { public static void main(String[] args) { SpringApplication.run(TrainApplication.class, args); } }SpringBootApplication是Configuration、EnableAutoConfiguration、ComponentScan三个注解的合成MapperScan自动扫描mapper包下的所有接口省得给每个Mapper单独加Mapper。这里要特别提醒MapperScan的包路径必须和Mapper接口实际位置一致否则启动后会告诉你找不到Mapper bean。技术栈选好后接下来的数据库设计决定了后面接口难不难写。火车票订票系统最核心的不是页面而是三张表之间的关系。3. 从建表到登录三张核心表与第一个用户接口火车票订票系统最怕的是表设计拍脑袋。很多JAVA基础阶段的小练习只建一张user表、一张ticket表就完事但学生火车票订票系统至少需要三张表用户、车次、订单。订单表是连接用户和车次的中间表也承载状态流转。下面直接给一套能本地跑起来的SQL和登录链路。3.1 建表SQL用户表、车次表、订单表怎么设计不返工先把建表语句完整写出来再逐段解释字段取舍。CREATE DATABASE IF NOT EXISTS train_db DEFAULT CHARACTER SET utf8mb4; USE train_db; -- 用户表存学生账号和基础信息 CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) NOT NULL, student_no VARCHAR(20) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生用户表; -- 车次表一列车的一种座位类型算一条记录 CREATE TABLE train ( id BIGINT NOT NULL AUTO_INCREMENT, train_no VARCHAR(20) NOT NULL, origin VARCHAR(50) NOT NULL, destination VARCHAR(50) NOT NULL, depart_time DATETIME NOT NULL, arrive_time DATETIME NOT NULL, seat_type VARCHAR(20) NOT NULL, price DECIMAL(8,2) NOT NULL, student_price DECIMAL(8,2) DEFAULT NULL, remain_tickets INT NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_train_seat (train_no, seat_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车次表; -- 订单表记录谁买了哪趟车、几张票、当前状态 CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, train_id BIGINT NOT NULL, ticket_count INT NOT NULL DEFAULT 1, total_price DECIMAL(8,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0已支付 1已退票, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_train_id (train_id), KEY idx_user_id (user_id), CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES user (id), CONSTRAINT fk_orders_train FOREIGN KEY (train_id) REFERENCES train (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;用户表的username加唯一约束因为登录时按用户名查找不允许重复。主键用BIGINT而不是INT课程设计里数据量不大但BIGINT能避免以后导出数据溢出。车次表设计成「车次座位类型」一行记录例如G1001次列车的二等座是一行一等座是另一行这种设计能直接复用同一套订票逻辑如果你把座位类型拆成单独关联表查询时要join对学生项目反而复杂。orders表不用order做表名因为ORDER是SQL关键字写原生SQL时容易引发语法问题。order_no加唯一索引对外展示用避免暴露自增主键。status字段用TINYINT表示状态0代表已支付1代表已退票Java端用常量替代魔法数字。保留外键约束是为了直观展示表关系真正的互联网级系统一般不建外键因为影响写入性能但课程设计保留它反而是加分项答辩时能说出这个取舍即可。3.2 第一批测试数据没有数据后面所有查询都是空跑。先插入一个学生用户和两个车次INSERT INTO user (username, password, real_name, student_no) VALUES (stu001, 123456, 王小明, 20230001); INSERT INTO train (train_no, origin, destination, depart_time, arrive_time, seat_type, price, student_price, remain_tickets) VALUES (G1001, 北京南, 上海虹桥, 2025-07-01 08:00:00, 2025-07-01 13:30:00, 二等座, 553.00, 442.40, 500), (G1001, 北京南, 上海虹桥, 2025-07-01 08:00:00, 2025-07-01 13:30:00, 一等座, 933.00, 746.40, 100);密码123456是明文仅限本地练习。外面下载的课程设计案例源码也大多这样但如果要传到公网或者写进简历必须换成BCrypt加密否则等于裸奔。这里插入两个座位类型是为了验证唯一索引uk_train_seat同一个train_no想再插入一次二等座会直接失败这正是我们想要的约束。3.3 登录链路实体类、Mapper接口、XML、Service、Controller登录是最经典的链路从数据库到页面要走五层。先写实体类字段对应数据库表public class User { private Long id; private String username; private String password; private String realName; private String studentNo; private LocalDateTime createTime; public Long getId() { return id; } // 其余getter/setter用IDE自动生成这里省略 }注意realName对应数据库的real_namestudentNo对应student_no依赖application.yml里配置的map-underscore-to-camel-case自动映射。接下来是Mapper接口只写一个按用户名查询的方法public interface UserMapper { User findByUsername(Param(username) String username); }对应的XMLselect idfindByUsername resultTypecom.example.train.entity.User SELECT id, username, password, real_name, student_no, create_time FROM user WHERE username #{username} /select这里必须用#{}而不是${}因为#{}会生成PreparedStatement参数占位符能有效防SQL注入${}是字符串拼接把用户输入直接拼进SQLa OR 11这种字符串能直接拖走整个表。这是j a v a基础里就该养成的习惯面试也常问。Service层处理业务逻辑这里不直接在Controller里查数据库Service public class UserService { Autowired private UserMapper userMapper; public User login(String username, String password) { User user userMapper.findByUsername(username); if (user ! null user.getPassword().equals(password)) { return user; } return null; } }密码比对用equals不要用。比较的是对象的引用地址String的equals才比较内容。这是JAVA基础里最容易被考到的点。Controller负责接收请求、调用Service、跳转页面Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public String login(String username, String password, HttpSession session, Model model) { User user userService.login(username, password); if (user null) { model.addAttribute(error, 用户名或密码错误); return login; } session.setAttribute(loginUser, user); return redirect:/train/list; } }为什么登录失败返回login成功要redirect而不是直接返回trainList因为redirect是重定向浏览器地址栏会变成/train/list刷新页面时不会重复提交表单如果直接return trainList刷新页面会再次触发POST登录请求这就是日常里所说的页面刷新翻车。Model.addAttribute会把error信息带到页面上JSP里用${error}就能显示。到这为止登录链路已经闭环。你会发现代码并不多但每一层都在做自己该做的事。接下来是订票和退票这才是真正考验系统设计的地方。4. 余票查询与订票/退票让系统扛得住并发如果学生火车票订票系统只做到能登录、能添加车次那它和图书管理系统没有本质区别。真正让它像样的是订票和退票这两个带状态变更的功能。这一章先把余票查询写通再把订票改成并发安全的版本最后处理退票。建议你实际把下面的代码跑一遍看看并发下余票会不会变成负数这比背一百道java面试题都管用。4.1 余票查询按出发地、目的地、日期过滤查询接口不复杂但要注意MyBatis多参数时必须用Param标记否则XML里拿不到参数。public interface TrainMapper { ListTrain findTrains(Param(origin) String origin, Param(destination) String destination, Param(departDate) String departDate); }XML写法select idfindTrains resultTypecom.example.train.entity.Train SELECT id, train_no, origin, destination, depart_time, arrive_time, seat_type, price, student_price, remain_tickets FROM train WHERE origin #{origin} AND destination #{destination} AND depart_time BETWEEN #{departDate} AND #{departDate} ORDER BY depart_time /select这里有一个小坑如果直接写AND DATE(depart_time) #{departDate}可读性好但DATE函数会让depart_time上的索引失效。我一般用BETWEEN把字符串日期转成当天开始和结束时间语义更清楚。课程设计数据量小用DATE()也能跑但面试时你能说出这个优化点是加分项。Service和Controller就简单了Service public class TrainService { Autowired private TrainMapper trainMapper; public ListTrain searchTrains(String origin, String destination, String departDate) { return trainMapper.findTrains(origin, destination, departDate); } }Controller public class TrainController { Autowired private TrainService trainService; GetMapping(/train/list) public String search(RequestParam(required false) String origin, RequestParam(required false) String destination, RequestParam(required false) String departDate, Model model) { ListTrain trains trainService.searchTrains(origin, destination, departDate); model.addAttribute(trains, trains); return trainList; } }三个参数全部可选页面不填就查全部。这里省略了参数判空实际项目里如果城市为空应该提前返回提示而不是让用户看到一张空列表。校验逻辑放Service层更好Controller层保持薄一点。4.2 订票接口为什么不能先查询余票再扣减先看一个看起来很有道理、实际上会超卖的写法// 错误示例并发下一定超卖 Train train trainMapper.findById(trainId); if (train.getRemainTickets() ticketCount) { train.setRemainTickets(train.getRemainTickets() - ticketCount); trainMapper.updateById(train); orderMapper.insert(buildOrder(train, ticketCount)); return true; } return false;两个请求同时读到remain_tickets1各自判断11成立各自扣一次最终数据库余票变成-1。顺序执行时没问题但Java Web是并发处理的这种先查后改在多个线程下就是典型的竞态条件。你把代码写成Thread.sleep模拟也一样复现。正确的做法是让扣库存变成一个原子操作由数据库来保证条件成立才更新Transactional(rollbackFor Exception.class) public boolean bookTicket(Integer userId, Integer trainId, Integer ticketCount) { int rows trainMapper.decreaseRemain(trainId, ticketCount); if (rows 0) { return false; } Train train trainMapper.findById(trainId); BigDecimal price train.getStudentPrice() ! null ? train.getStudentPrice() : train.getPrice(); Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTrainId(trainId); order.setTicketCount(ticketCount); order.setTotalPrice(price.multiply(BigDecimal.valueOf(ticketCount))); orderMapper.insert(order); return true; }对应的Mapper更新语句update iddecreaseRemain UPDATE train SET remain_tickets remain_tickets - #{count} WHERE id #{trainId} AND remain_tickets #{count} /update这段UPDATE在InnoDB下会对id这一行加行锁。两个事务同时执行时第一个事务持锁更新成功第二个事务等锁释放后再更新此时remain_tickets不再满足count受影响行数是0方法直接返回false不会插入订单。这比在Java代码里用synchronized合理以后如果系统部署成两台机器进程内的synchronized锁根本管不住另一个进程。事务必须加在Service的public方法上不能加在Controller也不能加到Mapper方法上。这里我用rollbackFor Exception.class让任何异常都触发回滚。Spring默认只回滚RuntimeException如果你在代码里显式抛出检查异常不加这个配置事务不会回滚余票扣了订单却没生成数据就错位了。订单号生成我用时间戳加随机数课程设计完全够用private String generateOrderNo() { return T System.currentTimeMillis() (int) (Math.random() * 1000); }真正上线要考虑订单号在极端并发下的重复问题可以用雪花算法但那是后话。4.3 退票接口状态流转比删除更安全退票如果直接delete订单就丢失了已退票的历史也没法做对账。所以订单表的status字段开始发挥作用0表示已支付1表示已退票。Transactional(rollbackFor Exception.class) public boolean refund(Integer orderId, Integer userId) { int rows orderMapper.updateStatusByUser(orderId, userId, 1); if (rows 0) { return false; // 订单不存在、不是这个人的、或已经退过票 } Order order orderMapper.findById(orderId); trainMapper.increaseRemain(order.getTrainId(), order.getTicketCount()); return true; }update idupdateStatusByUser UPDATE orders SET status #{status} WHERE id #{orderId} AND user_id #{userId} AND status 0 /updateupdate idincreaseRemain UPDATE train SET remain_tickets remain_tickets #{count} WHERE id #{trainId} /update注意updateStatusByUser里携带了user_id条件这是为了防止一个用户通过遍历orderId去退别人的票。同时带上status 0条件第二个退票请求要么等第一个提交后看到status1要么直接影响0行不会重复释放余票。前端Controller调用也很直观PostMapping(/order/refund) public String refund(Integer orderId, HttpSession session, Model model) { User user (User) session.getAttribute(loginUser); boolean ok orderService.refund(orderId, user.getId()); if (!ok) { model.addAttribute(error, 退票失败可能订单不存在或已退过); } return redirect:/order/list; }退票成功之后重定向到订单列表这样刷新页面不会重复退票。到这里核心业务闭环就已经有了登录、查车、订票、退票。但代码写完只是第一步真正让人头疼的是本地环境的各种报错。5. 学生火车票订票系统5个高频踩坑点从环境变量到余票超卖下面这五条不是零散报错而是课程设计里几乎每届都能遇到的翻车现场。每条按现象、原因、解决三步给可以直接对着排查。前两条和环境有关后三条和代码有关建议写进项目笔记里。5.1 一启动就报「java: 警告: 源发行版 17 需要目标发行版 17」现象在IDEA里打开别人的JAVA学生火车票订票系统课程设计案例源码一编译要么显示“java: 警告: 源发行版 17 需要目标发行版 17”要么直接“不支持发行版本 17”但自己的电脑只装了JDK8。原因项目pom.xml或IDEA的Project Structure里配置的编译器等级是17而Maven运行在JDK8上。配置不一致就会报这个错和代码本身关系不大。解决先确认本地JAVA环境变量配置。在命令行执行java -version mvn -v看两个命令显示的Java版本是否一致。如果mvn显示JDK8而项目要求17有两种处理一是装JDK17并把JAVA_HOME指过去二是把项目降级到JDK8。常见做法是检查pom.xml中的java.version以及IDEA的Project Structure里的Project SDK和Maven的JDK设置全部统一后再Refresh Mavenproperties java.version1.8/java.version /properties如果改了java.version但pom里某个依赖还是Java 11编译的也会继续报错。本质上就是JAVA_HOME、PATH和maven用了不同的JDK三条路走成一条就没事了。5.2 连不上MySQLPublic Key Retrieval is not allowed现象启动Spring Boot以后控制台爆出Public Key Retrieval is not allowed页面完全打不开。MySQL版本是8.0以上。原因MySQL 8.0默认认证插件是caching_sha2_password驱动在非SSL连接下要取服务器公钥才能完成密码验证。如果JDBC连接串没设置allowPublicKeyRetrievaltrue驱动直接拒绝。解决在application.yml的url上补参数url: jdbc:mysql://localhost:3306/train_db?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/ShanghaicharacterEncodingutf8顺带说明useSSLfalse是因为本地开发不需要证书serverTimezoneAsia/Shanghai解决日期差8小时characterEncodingutf8解决中文乱码。如果加上allowPublicKeyRetrieval之后还报Access denied就检查用户名密码或者用本地root账号别再用旧依赖里已经停用的com.mysql.jdbc.Driver。5.3 页面全是中文问号数据库插入也乱码现象登录页和车次列表里的中文全变成问号数据库里手工插入正常一程序操作就乱码。原因字符集不一致。常见有三处页面本身的编码不是UTF-8、JSP的contentType没设置、MySQL连接串没有characterEncodingutf8。如果用了Spring Boot的CharacterEncodingFilter还有一个容易忽略的坑forceEncoding没设true过滤器就只影响部分请求。解决第一JSP文件顶部统一写% page contentTypetext/html;charsetUTF-8 languagejava %第二确保application.yml里的数据库连接串带characterEncodingutf8。第三如果项目自己注册了过滤器可以用下面这段替换Bean public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter new CharacterEncodingFilter(); filter.setEncoding(UTF-8); filter.setForceEncoding(true); return filter; }有一个判断技巧在浏览器开发者工具里看响应头如果Content-Type显示charsetISO-8859-1说明页面声明没用如果数据库里直接查就是乱码一般是连接串问题。乱码问题外表看着像玄学绝大多数都能归结到这三处。5.4 MyBatis查询出来的字段全是null现象登录能成功但页面上的真实姓名、学号、车次、余票全是空白或null。后台打印日志能看到SQL查出了值。原因实体类字段是驼峰命名比如realName而数据库字段是下划线real_name。MyBatis没有开启mapUnderscoreToCamelCase时不会自动转换结果集映射时找不到对应属性于是全是null。老源码经常出现这个问题。解决在application.yml里加配置mybatis: configuration: map-underscore-to-camel-case: true如果不想改全局配置也可以在XML里写别名select idfindById resultTypecom.example.train.entity.Train SELECT id, train_no AS trainNo, remain_tickets AS remainTickets FROM train WHERE id #{id} /select但别名方案每条SQL都要维护很烦。遇到字段全null我会先查mapUnderscoreToCamelCase再回头检查实体类有没有用Lombok的Data两者都确认后再在XML里打印返回结果。排查这种看不到报错的空值问题最基本的方法就是先打开SQL日志看查询到底有没有返回内容。5.5 演示时一抢票余票就变成负数现象两个账号同时下单同一车次最后一张票结果两个订单都生成了数据库余票变成-1。现场演示翻车老师直接问数据一致性怎么保证原因Service里用了先查询余票、再判断够不够、最后UPDATE的顺序。两个并发线程都读到余票1都通过判断都执行更新最后写回-1。如果没有事务扣了票却在插入订单时失败数据会更乱。解决按第四章的做法把扣余票的SQL改成原子条件更新update iddecreaseRemain UPDATE train SET remain_tickets remain_tickets - #{count} WHERE id #{trainId} AND remain_tickets #{count} /update并在Service方法上加上Transactional。之后验证表引擎SHOW CREATE TABLE train;确认是InnoDB因为MyISAM不支持行锁条件更新也拦不住并发。如果之前是MyISAM用ALTER TABLE train ENGINE InnoDB;改完以后再用两个浏览器无痕窗口同时抢票观察余票不会小于0。这里纠正一个常见误用不要用synchronized锁Controller里的方法单机只能勉强能用换两台机器部署就失效锁范围太大还会把整个页面卡死。数据库行锁才是这个场景里最直接有效的方案。提示所有涉及状态变更的方法写完后都要自己问一句这个方法在并发调用两次会怎样能回答清楚才算是真的理解事务和锁。6. 把JAVA学生火车票订票系统讲成项目亮点验证方法与答辩技巧代码写完下一步是让别人相信它真的可靠。我的习惯是先写一个验证清单把正常订票、余票不足、重复退票、并发抢票四种场景各跑一遍并把每次数据库的余票数和订单状态记录下来。不管是课程设计答辩还是实习面试你都能直接拿数据去向对方证明而不是只会说「我做了个系统」。验证方法分两层。第一层是功能普通用户登录后搜北京南到上海虹桥的车次选二等座订2张票orders表出现一条status0的记录train表对应行的remain_tickets从500变成498退票后再查orders.status变成1remain_tickets回到500。第二层是并发开两个浏览器无痕窗口同时提交同一车次最后一张票的订单最终只有一个订单成功另一个返回余票不足。再进一步可以用JMeter开两个线程组对同一接口发请求看服务端日志里受影响行数为0的那次返回。这个实验本身就是很好的面试素材把普通的学生火车票订票系统描述成「用条件更新解决库存超卖」的并发项目含金量立刻不一样。答辩时不要背代码而是讲三个决策点。第一为什么车次表用「车次座位类型」作为唯一记录而不是一个车次一行第二为什么订单表加status字段而不是直接删除退票记录第三为什么扣余票用UPDATE...WHERE remain_ticketscount而不是先查再改。这三个问题能答清楚老师就知道你真正把项目写进去了。我自己当年做课程设计时把所有代码堆在Controller里数据库连接也是在一个Servlet里new Connection结果答辩时老师让我演示两台机器同时订票还没点两下页面就崩了。这件事给我的教训是业务代码的分层不是八股文事务和并发控制也不是面试前背两题就能糊弄过去的。后来再看JAVA课程设计案例源码我第一眼不是看页面美不美而是先看它有没有事务、有没有条件更新。最后给一个口袋技巧把上面说的测试用例和三个决策点写进项目根目录的README导师或面试官打开代码库第一印象就会好很多。这个火车站订票系统真正的价值不在于功能堆积而在于你在一个看似普通的课程设计里想清楚了并发和一致性。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Atlas 300V 24G推理卡实战:从环境搭建到YOLO部署全流程 2026/9/25 7:18:47

Atlas 300V 24G推理卡实战:从环境搭建到YOLO部署全流程

很多人上来就问:“Atlas 300V 24G 是运算加速卡吗?”答案是肯定的,而且它正是目前我在倒腾目标检测部署时,用得最顺手的一张推理卡。如果你最近在搜“Atlas 部署 YOLO”相关的资料,大概率是想把 PyTorch 训练好的权重&…

阅读更多 →
开源可审计的LLM代码审查工作流设计 2026/9/25 7:18:47

开源可审计的LLM代码审查工作流设计

1. 项目概述:这不是一个“工具”,而是一套可落地的开源代码审查工作流设计open-code-review 这个名字乍看像某个具体软件,但实际它代表的是一类正在快速成型的工程实践范式——用开源、透明、可审计的方式,把大语言模型&#xff0…

阅读更多 →
无刷电机FOC调试实战:PID整定与相位校准全流程 2026/9/25 7:18:34

无刷电机FOC调试实战:PID整定与相位校准全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
MT管理器全功能拆解:从文件管理到APK编辑,免费版够用吗? 2026/9/25 7:18:27

MT管理器全功能拆解:从文件管理到APK编辑,免费版够用吗?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Atlas 300V 24G实战:YOLO模型部署与多路视频流调优全攻略 2026/9/25 7:18:20

Atlas 300V 24G实战:YOLO模型部署与多路视频流调优全攻略

说实话,第一次听到“atlas部署yolo”这个搜索词组合的时候,我愣了一下。很多人对Atlas的印象还停留在“华为那个AI开发板”,或者干脆连它和“运算加速卡”之间是什么关系都没搞清。尤其是“atlas 300v 24g 是运算加速卡吗”这种问法&#xff…

阅读更多 →
Gomoon 桌面端大模型效率工具:从流式渲染到上下文采集的工程实践 2026/9/25 7:18:13

Gomoon 桌面端大模型效率工具:从流式渲染到上下文采集的工程实践

简介:Gomoon 是一款基于大模型的桌面端效率工具,面向希望借助 AI 提升工作与学习效率的开发者、学生及办公人群。它支持配置多种大模型引擎并实时切换,可创建专属助手,实现快速问答、连续对话、历史存取、答案编辑与重新生成&…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉