简介:基于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 管 SQL,MySQL 做存储,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.version>5.2.9.RELEASE</spring.version> <mybatis.version>3.5.6</mybatis.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;order 是 SQL 保留字,所以表名写成 orders。订单表通过 user_id、trip_id 关联用户和班次,省去中间关联表,逻辑最直接,适合课程设计的数据量。init.sql 里我会预置几条班次数据,比如 A 市到 B 市的高铁 G101、B 市到 C 市的航班 MU5102,让系统启动后立刻有数据可查,不需要手动造数据。
3. 核心模块逐段拆解:登录、查询、下单的完整链路
3.1 登录认证:一个请求怎么穿过三层
登录是理解这个工程的最佳入口。浏览器把 username 和 password 提交到 UserController:
@Controller @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,接下来的数据访问交给 Service:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public User login(String username, String password) { return userMapper.selectByUsernameAndPassword(username, password); } }Service 类上标了 @Service,Spring 容器启动时会自动扫描并创建实例。登录业务足够简单,所以这里只做参数透传;如果后续要加“连续输错三次锁定账号”这种规则,就应该把判断逻辑放在 Service 而不是 Controller,那才是分层该管的事。
真正执行 SQL 的是 MyBatis 的 XML 映射文件:
<select id="selectByUsernameAndPassword" resultType="com.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 class="com.course.ticket.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>拦截器配置完成后,未登录用户访问任何业务页面都会跳回登录页,后台的 admin 目录也因此被保护。这个细节经常是老师检查的重点。
3.2 车次查询与余票扣减:一个带条件的 UPDATE
订票系统的核心集中在两个动作:查车次、扣余票。查询接口在 TripController:
@RequestMapping("/trip/search") public String search(TripQuery query, Model model) { List<Trip> trips = tripService.search( query.getStartCity(), query.getEndCity(), query.getDate() ); model.addAttribute("trips", trips); return "search_result"; }对应的 XML 查询:
<select id="search" resultType="com.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,这在并发下会出大问题:两个用户同时读到 stock=1,同时通过判断,最后库存变成 -1。所以我直接把判断和扣减合并成一条 SQL:
<update id="deductStock"> 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 id="selectOrderWithDetail" resultMap="orderDetailMap"> 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 的步骤如下:
- 打开 IDEA,File → Open,选择 pom.xml,以 Maven 项目导入,等依赖下载完成。
- Project Structure → Project,把 SDK 设置为 1.8,Language level 也设为 8。
- 修改 src/main/resources/jdbc.properties,把数据库账号密码换成自己本地的。
- 执行 init.sql 建库建表,并插入预置班次数据。
- Run → Edit Configurations,新增 Tomcat Server → Local,Deployment 选择 exploded war 方式部署。
- 启动 Tomcat,访问 http://localhost:8080/。
启动成功的判断标准是 IDEA 控制台出现 SpringContext 初始化完成的日志,并且页面能打开登录页。如果直接访问就 404,优先检查 web.xml 里的 DispatcherServlet 映射和 springmvc.xml 是否被加载。
4.2 数据库初始化与 Spring 配置核对
jdbc.properties 是整个项目唯一必须改的配置文件:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ticket_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456MySQL 5.7 时代常写 com.mysql.jdbc.Driver,MySQL 8.0 驱动必须改成 com.mysql.cj.jdbc.Driver。连接串里的 serverTimezone=Asia/Shanghai 也一定要加,否则插入 datetime 字段时会报时区错误。characterEncoding=utf8 解决中文写入乱码问题。
applicationContext.xml 里的关键配置:
<context:component-scan base-package="com.course.ticket"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath*:mapper/*.xml"/> </bean> <mybatis:scan base-package="com.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 class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/jsp/"/> <property name="suffix" value=".jsp"/> </bean>Controller 返回 search_result,实际查找的是 /jsp/search_result.jsp。文件不存在就会 404。所以排错顺序应该是:先看控制台有没有 handler 映射成功,再看 JSP 是否存在。
第三个坑:中文乱码。现象是页面上“订单”“车次”全部变成问号。乱码通常有三个产生点:数据库连接串缺少 characterEncoding=utf8、数据库表字符集不是 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 项目,都会先画一遍请求链路再动手改代码。希望帮到你。
本文还有配套的精品资源,点击获取