简介基于SSM框架的机票/火车票预定系统课程设计完整项目包面向软件工程专业学生以及需要完成类似课设的开发者旨在解决预订系统从需求分析到编码实现的全流程参考问题帮助读者掌握Spring、SpringMVC、MyBatis三大框架的整合与实战。压缩包共1651个文件涵盖Java源码、JSP视图、XML配置、SQL脚本、CSS/JS前端样式以及依赖JAR包等整体约50.16MB目录组织规范便于按模块检索。已有273人学习下载。资源不仅包含可运行的项目代码还提供数据库建表脚本与关键配置文件支持快速搭建运行环境其中订单管理、票务查询、后台管理等功能完整展示了SSM分层架构、依赖注入以及事务管理的实际运用可借此理解MyBatis持久层映射与SpringMVC请求处理流程。这套资料既能支撑课程设计报告撰写也可用于答辩演示与框架进阶训练是一份兼顾完整性与实用性的高价值参考。1. 软件工程课程设计一个能直接跑起来的 SSM 机票/火车票预订系统如果你抽到“机票/火车票预订系统”这道软件工程课程设计题第一反应多半是去网上找一个现成的 SSM 项目但真正下载过的人都知道十个里面能跑起来的不到两三个剩下的不是缺依赖就是 JSP 里硬写 JDBC 的祖传代码。这个 SSM 项目是我实际改过、跑通过的那一类Spring 管对象、SpringMVC 管路由、MyBatis 管 SQLMySQL 做存储JSP 做展示层。用户端覆盖注册登录、车次查询、下单、模拟支付后台端覆盖班次维护和订单查看Controller-Service-DAO 三层边界清楚。课程设计答辩能应付想学 SSM 整合的人也能拿它当样本。下面按架构设计、核心代码、部署排错、二次改造的顺序拆。2. 技术栈与架构设计三层怎么拆数据库怎么建模2.1 SSM 技术选型与版本搭配为什么不用 Spring Boot因为题目写的是“基于 SSM 框架”。课程设计评阅通常会盯住 Spring、SpringMVC、MyBatis 三个框架是否真正整合而不是只看页面能不能点。Spring Boot 虽然开发快但技术栈对不上题目描述答辩时常被追问“Spring 的配置在哪”。所以这套工程采用 SSM 三件套Spring 负责 Service 层 bean 的创建和事务控制SpringMVC 处理请求分发与参数绑定MyBatis 负责 SQL 映射。pom.xml 里最关键的依赖是这些properties spring.version5.2.9.RELEASE/spring.version mybatis.version3.5.6/mybatis.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.21/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.1.24/version /dependency /dependencies这个搭配我踩过坑Spring 5.x 和很旧的 mybatis-spring 版本不兼容会出现 MapperFactoryBean 实例化报错。上面这组版本已经验证过可以配合直接照抄即可。数据库连接池用 Druid方便在监控页看到当前连接数答辩时也能作为“项目考虑过性能”的佐证。2.2 Maven 工程目录结构与分层职责包结构按三层来命名Controller、Service、DAO 一目了然src/main/java/com/course/ticket/ ├── controller/ │ ├── UserController.java │ ├── TripController.java │ ├── OrderController.java │ └── AdminController.java ├── service/ │ ├── UserService.java │ ├── TripService.java │ ├── OrderService.java │ └── impl/ │ ├── UserServiceImpl.java │ ├── TripServiceImpl.java │ └── OrderServiceImpl.java ├── dao/ │ ├── UserMapper.java │ ├── TripMapper.java │ └── OrderMapper.java ├── entity/ │ ├── User.java │ ├── Trip.java │ └── Order.java └── interceptor/ └── LoginInterceptor.java对应的 resources 与 webapp 目录src/main/resources/ ├── mapper/ # MyBatis 的 XML 映射文件 │ ├── UserMapper.xml │ ├── TripMapper.xml │ └── OrderMapper.xml ├── jdbc.properties ├── mybatis-config.xml └── spring/ ├── applicationContext.xml └── springmvc.xml src/main/webapp/ ├── WEB-INF/web.xml ├── static/ # css/js/images └── jsp/ ├── login.jsp ├── register.jsp ├── index.jsp ├── search.jsp └── admin/ # 后台管理页面各层职责很明确controller 只做参数接收、调用 service、决定返回哪个页面service 处理业务规则比如库存扣减和订单生成必须放在同一个事务dao 只负责数据读写SQL 全部写在 mapper XML 里。分层的实际价值在于以后想把 JSP 换成 Vue 前端只需要改 controller 层的返回值service 和 dao 可以原封不动。2.3 数据库表设计用户、车次、订单怎么关联项目使用 MySQL库名 ticket_db核心是 user、trip、orders 三张表。建表脚本如下CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(64) NOT NULL COMMENT MD5 密文, phone varchar(20) DEFAULT NULL, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 0 普通用户1 管理员, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE trip ( id int(11) NOT NULL AUTO_INCREMENT, code varchar(20) NOT NULL COMMENT 车次号或航班号, type tinyint(4) NOT NULL COMMENT 0 火车1 飞机, start_city varchar(50) NOT NULL, end_city varchar(50) NOT NULL, start_time datetime NOT NULL, end_time datetime NOT NULL, price decimal(10,2) NOT NULL, stock int(11) NOT NULL DEFAULT 0 COMMENT 余票数量, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id int(11) NOT NULL, trip_id int(11) NOT NULL, ticket_count int(11) NOT NULL DEFAULT 1, total_price decimal(10,2) NOT NULL, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0 待支付1 已支付2 已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;order 是 SQL 保留字所以表名写成 orders。订单表通过 user_id、trip_id 关联用户和班次省去中间关联表逻辑最直接适合课程设计的数据量。init.sql 里我会预置几条班次数据比如 A 市到 B 市的高铁 G101、B 市到 C 市的航班 MU5102让系统启动后立刻有数据可查不需要手动造数据。3. 核心模块逐段拆解登录、查询、下单的完整链路3.1 登录认证一个请求怎么穿过三层登录是理解这个工程的最佳入口。浏览器把 username 和 password 提交到 UserControllerController RequestMapping(/user) public class UserController { Autowired private UserService userService; RequestMapping(/login) public String login(String username, String password, HttpSession session, Model model) { User user userService.login(username, password); if (user null) { model.addAttribute(msg, 用户名或密码错误); return login; } session.setAttribute(loginUser, user); return redirect:/trip/list; } }这里三个参数很典型username、password 由 SpringMVC 自动从请求参数绑定session 和 model 按类型注入。service 返回 null 说明账号密码不匹配回到登录页并显示提示如果查到了用户就把用户对象放进 session然后重定向到车次列表。Controller 没有写任何 SQL接下来的数据访问交给 ServiceService public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override public User login(String username, String password) { return userMapper.selectByUsernameAndPassword(username, password); } }Service 类上标了 ServiceSpring 容器启动时会自动扫描并创建实例。登录业务足够简单所以这里只做参数透传如果后续要加“连续输错三次锁定账号”这种规则就应该把判断逻辑放在 Service 而不是 Controller那才是分层该管的事。真正执行 SQL 的是 MyBatis 的 XML 映射文件select idselectByUsernameAndPassword resultTypecom.course.ticket.entity.User SELECT id, username, password, phone, role, create_time FROM user WHERE username #{username} AND password #{password} /select#{username} 是预编译占位符对应 PreparedStatement 的 ?可以避免 SQL 注入。注意演示阶段密码是明文比对实际项目开发必须改成 MD5 加盐或 BCrypt 哈希这个可以在答辩时主动讲出来。另外项目里加了一个登录拦截器防止未登录用户直接访问后台地址public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); if (session.getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login); return false; } return true; } }在 springmvc.xml 注册mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ mvc:exclude-mapping path/static/**/ bean classcom.course.ticket.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors拦截器配置完成后未登录用户访问任何业务页面都会跳回登录页后台的 admin 目录也因此被保护。这个细节经常是老师检查的重点。3.2 车次查询与余票扣减一个带条件的 UPDATE订票系统的核心集中在两个动作查车次、扣余票。查询接口在 TripControllerRequestMapping(/trip/search) public String search(TripQuery query, Model model) { ListTrip trips tripService.search( query.getStartCity(), query.getEndCity(), query.getDate() ); model.addAttribute(trips, trips); return search_result; }对应的 XML 查询select idsearch resultTypecom.course.ticket.entity.Trip SELECT * FROM trip WHERE start_city #{startCity} AND end_city #{endCity} AND DATE_FORMAT(start_time, %Y-%m-%d) #{date} ORDER BY start_time /select这里最容易忽略的是日期比较。start_time 是 datetime 类型前端传过来的是日期字符串直接用等号很可能查不到结果所以用 DATE_FORMAT 把 datetime 转成日期再比对。想省事也可以把 start_time 拆成 start_date 和 start_time 两个字段但那样后台录入班次时就要分开编辑体验反而不好。余票扣减是最容易写错的地方。常见错误是先 SELECT 查 stock判断大于 0 再 UPDATE这在并发下会出大问题两个用户同时读到 stock1同时通过判断最后库存变成 -1。所以我直接把判断和扣减合并成一条 SQLupdate iddeductStock UPDATE trip SET stock stock - #{count} WHERE id #{id} AND stock #{count} /update关键在stock #{count}这个条件。数据库行锁保证同一时刻只有一个事务能修改当前这行其他并发请求要么等锁要么 affected rows 返回 0。看到返回 0 就知道库存不足直接提示用户即可。这种写法避免了一查一改的竞态窗口。3.3 订单生成与状态流转从下单到模拟支付下订单要同时做两件事扣库存、写订单表。两步必须一起成功或一起失败否则会出现“库存少了但订单没建出来”的数据不一致。事务边界放在 Service 层Service public class OrderServiceImpl implements OrderService { Autowired private TripMapper tripMapper; Autowired private OrderMapper orderMapper; Override Transactional(rollbackFor Exception.class) public boolean createOrder(Long userId, Long tripId, int count) { int affected tripMapper.deductStock(tripId, count); if (affected 0) { return false; } Trip trip tripMapper.selectById(tripId); BigDecimal totalPrice trip.getPrice() .multiply(new BigDecimal(count)); Order order new Order(); order.setOrderNo(UUID.randomUUID().toString().replace(-, )); order.setUserId(userId); order.setTripId(tripId); order.setTicketCount(count); order.setTotalPrice(totalPrice); order.setStatus(0); orderMapper.insert(order); return true; } }Transactional 把扣库存和插订单合并成一个事务rollbackFor Exception.class 表示只要抛出任何异常就整体回滚。affected 为 0 时直接 return false事务里没有产生写操作最终不会提交任何中间状态。order_no 用 UUID 去掉横线保证订单号唯一也便于在支付阶段按订单号查询。支付接口做的是模拟逻辑RequestMapping(/order/pay) ResponseBody public String pay(Long orderId, HttpSession session) { User user (User) session.getAttribute(loginUser); Order order orderService.getById(orderId); if (order null || !order.getUserId().equals(user.getId())) { return 订单不存在; } if (order.getStatus() 0) { orderService.updateStatus(orderId, 1); return 支付成功; } return 订单状态异常; }订单必须属于当前登录用户才能操作防止 A 用户拿 B 用户的订单号去支付。status 从 0 改成 1 就是支付完成。这里只做演示真正的第三方支付对接会在第 5 章讲怎么扩展。3.4 后台管理班次 CRUD 与订单查看后台管理放在 AdminController第一步判断当前用户角色Controller RequestMapping(/admin) public class AdminController { Autowired private TripService tripService; Autowired private OrderService orderService; RequestMapping(/tripList) public String tripList(HttpSession session, Model model) { User user (User) session.getAttribute(loginUser); if (user null || user.getRole() ! 1) { return redirect:/login; } model.addAttribute(trips, tripService.getAllTrips()); return admin/trip_list; } }订单列表为了同时展示用户名和车次信息使用 LEFT JOIN 查询select idselectOrderWithDetail resultMaporderDetailMap SELECT o.id, o.order_no, o.ticket_count, o.total_price, o.status, o.create_time, u.username, t.code, t.start_city, t.end_city FROM orders o LEFT JOIN user u ON o.user_id u.id LEFT JOIN trip t ON o.trip_id t.id ORDER BY o.create_time DESC /select后台不需要复杂业务把数据查出来展示再加“新增班次”“修改班次”“删除班次”三个操作就完整了。trip 表的增删改要经过 TripService不能在 Controller 里直接 new 一个 TripMapper。删除班次前还要检查 orders 表里是否有该车次的未完成订单避免删掉正在使用的数据。4. 本地跑通与避坑指南从 Maven 依赖到数据库配置4.1 运行环境准备与 IDEA 部署步骤推荐环境是 JDK 1.8、Maven 3.6、Tomcat 8.5、MySQL 5.7。MySQL 8.0 也能跑但驱动和连接参数要按下文配置。直接把项目导入 IDEA 的步骤如下打开 IDEAFile → Open选择 pom.xml以 Maven 项目导入等依赖下载完成。Project Structure → Project把 SDK 设置为 1.8Language level 也设为 8。修改 src/main/resources/jdbc.properties把数据库账号密码换成自己本地的。执行 init.sql 建库建表并插入预置班次数据。Run → Edit Configurations新增 Tomcat Server → LocalDeployment 选择 exploded war 方式部署。启动 Tomcat访问 http://localhost:8080/。启动成功的判断标准是 IDEA 控制台出现 SpringContext 初始化完成的日志并且页面能打开登录页。如果直接访问就 404优先检查 web.xml 里的 DispatcherServlet 映射和 springmvc.xml 是否被加载。4.2 数据库初始化与 Spring 配置核对jdbc.properties 是整个项目唯一必须改的配置文件jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/ticket_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456MySQL 5.7 时代常写 com.mysql.jdbc.DriverMySQL 8.0 驱动必须改成 com.mysql.cj.jdbc.Driver。连接串里的 serverTimezoneAsia/Shanghai 也一定要加否则插入 datetime 字段时会报时区错误。characterEncodingutf8 解决中文写入乱码问题。applicationContext.xml 里的关键配置context:component-scan base-packagecom.course.ticket/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath*:mapper/*.xml/ /bean mybatis:scan base-packagecom.course.ticket.dao/component-scan 的 base-package 必须和 dao 接口、service 实现的根包一致写偏了会出现 NoSuchBeanDefinitionException。mapperLocations 的值 classpath*:mapper/*.xml 决定了 MyBatis 去 classpath 下找 XML 映射文件这一步经常因为路径不匹配导致绑定异常。4.3 避坑清单5 个最常见的起步问题第一个坑Invalid bound statement (not found)。现象是容器启动正常一调用 dao 接口方法立刻报错。原因绝大多数是 mapper XML 没被打进 classpath或者 XML 的 namespace 与接口全限定名不匹配。解决方法是先打开 target/classes 看看有没有 mapper 目录及对应 XML再核对 namespace 是否为 com.course.ticket.dao.xxxMapper。第二个坑页面 404。原因可能是 DispatcherServlet 映射没配好或者 JSP 物理路径与视图解析器配置不一致。springmvc.xml 里的 InternalResourceViewResolver 配置如下它直接决定返回值对应哪个物理文件bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/jsp// property namesuffix value.jsp/ /beanController 返回 search_result实际查找的是 /jsp/search_result.jsp。文件不存在就会 404。所以排错顺序应该是先看控制台有没有 handler 映射成功再看 JSP 是否存在。第三个坑中文乱码。现象是页面上“订单”“车次”全部变成问号。乱码通常有三个产生点数据库连接串缺少 characterEncodingutf8、数据库表字符集不是 utf8mb4、web.xml 缺少 CharacterEncodingFilter。三处都要统一为 UTF-8只改一处没用。第四个坑Tomcat 8.5 启动报 NoClassDefFoundError 或 LinkageError。原因一般是 servlet-api 依赖打进了 war 包和 Tomcat 自带的冲突。解法是把项目中 javax.servlet-api 的作用域设为 provided不让它进入最终产物。第五个坑页面能打开但样式全丢js/css 加载 404。这是因为 DispatcherServlet 配置为 / 后会把静态资源请求也拦下来。在 springmvc.xml 加静态资源映射即可mvc:resources mapping/static/** location/static//后台列表点删除没反应时先按 F12 看 Network 面板请求是否发出再看请求返回 500 还是 404最后排查 Service 里的 SQL不要一开始就怀疑 Tomcat 配置。5. 二次开发视角三个值得动手的小改造5.1 把模拟支付改成策略接口现在支付是直接在 Controller 里 update status如果想对接真实支付渠道最好把支付逻辑抽象成接口public interface PaymentService { boolean pay(Long orderId, BigDecimal amount); }添加一个 MockPaymentService 作为现有逻辑的实现以后接入第三方支付时再写一个实现类用 Qualifier 指定 bean。改动不会影响下单主流程答辩时可以讲“这里预留了支付扩展点”这比单纯说“我接了支付宝”更可信。5.2 给车次查询加 Redis 缓存车次查询是读多写少的典型场景。常见做法是在 TripService 层加入 Redis 缓存查询 key 为 A 市到 B 市加日期的字符串命中就直接返回未命中再查 MySQL 并回写缓存。配置 spring-data-redis 后十几行代码就能完成。这是性能优化方向上最值得动的点项目里有了它答辩时谈“热点车次缓存”就有真实落点。5.3 给后台加一张统计页后台目前只有列表页可以加一个订单统计页面按车次分组计算月度订单量和总金额SQL 用 GROUP BY 和 SUM 就能完成。页面用 ECharts 画柱状图数据来自项目自身产生的订单不需要造数据。这个功能不仅让后台看起来完整也把数据库聚合查询、前端图表展示两个技能点都展示出来了。我从这个项目里最深的体会是SSM 项目的难点从来不在某个框架本身而在三个框架的配置如何互相衔接。组件扫描、拦截器、事务、SQL 映射任何一个环节不匹配整个链路就断掉。从那以后我每拆一个 SSM 项目都会先画一遍请求链路再动手改代码。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?