简介:这个汽车票务系统用Java开发,基于SSM框架(Spring+SpringMVC+MyBatis),后端搭配MySQL 8.0数据库,前端界面简洁易用。用户能完成注册登录、实时查询班次、可视化选座、生成订单并查看订单状态;管理员可通过后台管理车辆信息、线路设置、班次排期和全部订单。项目已适配IntelliJ IDEA,内置Tomcat 7.x/8.x部署方案,开箱即用——导入项目后只需配置数据库连接地址和账号密码,就能直接启动运行。压缩包里包含完整源代码、建库脚本db.sql、Navicat可导入的数据库样例(xlsx格式)、详细配置说明txt、开题报告、答辩PPT、Word版毕业论文,以及Linux下一键启动脚本(start.sh/run.sh)。所有功能模块均经真实环境测试,适合本科毕业设计选题、Java Web课程实训或期末综合大作业直接复用。
我做过不下二十个Java Web毕设项目,从早期SSH到SSM再到Spring Boot,每年帮学生改代码、调环境、过答辩。这个汽车客运订票系统,是我去年带三个本科生做毕业设计时打磨出来的“教学友好型”模板——不是那种堆砌功能但跑不起来的Demo,而是真正能在IDEA里点开就跑、在Tomcat上部署就用、答辩老师现场扫码就能查订单的实打实系统。它把本科阶段该练的全链路能力都串起来了:MVC分层怎么划才不混乱、MyBatis动态SQL怎么写才安全、事务边界在哪设才不丢数据、前端选座交互怎么和后端状态同步、管理员权限怎么隔离才不越权……关键词里写的“汽车订票系统、SSM框架、Java毕设、MySQL数据库”,每一个都不是虚词,而是对应着学生最容易卡壳的真实环节。比如“SSM框架”不只是名字好听——Spring负责IoC容器管理Bean生命周期,SpringMVC用@RestController+@RequestBody处理前后端分离式JSON请求,MyBatis用
批量插入座位状态、用
动态拼接班次查询条件;而“MySQL数据库”也不是随便建几张表——我们特意用tinyint(1)存座位状态(0空闲/1已售/2锁定),用datetime存发车时间而非varchar,用联合唯一索引(route_id, departure_time, bus_id)防重复排班。这套东西,你拿去当毕设,不用重写核心逻辑;拿去当课程设计,能照着源码反向推导出Spring配置原理;拿去当面试素材,能把“事务传播行为”“MyBatis一级二级缓存”“SpringMVC拦截器鉴权”讲得有血有肉。下面我就按一个真实开发者从零搭环境到上线调试的顺序,把这套系统掰开揉碎讲清楚——不讲概念,只讲你敲命令时会遇到什么、改哪行配置会生效、为什么这里要用@Transactional而那里不能加。
1. 系统整体架构与设计思路拆解
1.1 为什么选SSM而不是Spring Boot?
很多同学看到新项目第一反应就是“上Spring Boot”,但这个系统坚持用原生SSM,是有明确教学意图的。Spring Boot确实省事,自动装配一堆starter,可一旦出问题,你根本不知道底层哪个Bean没注入、哪个Filter没生效、DataSource是怎么被HikariCP接管的。而SSM是“显式可控”的——Spring的applicationContext.xml里清清楚楚写着哪些Service被扫描、哪些包走AOP代理;SpringMVC的spring-mvc.xml里明明白白列着HandlerMapping怎么匹配URL、ViewResolver怎么解析JSP路径;MyBatis的mybatis-config.xml里规定着typeAliases怎么映射实体类、plugins怎么加分页插件。我在指导学生时发现,用SSM调试时,只要在web.xml里把ContextLoaderListener和DispatcherServlet的init-param日志级别调成DEBUG,就能看到Spring容器加载了几个Bean、MVC注册了多少Handler、MyBatis加载了几张Mapper XML——这种“看得见摸得着”的过程,对理解IoC、DI、MVC生命周期至关重要。
举个具体例子:系统里的“在线选座”功能,前端点击座位格子触发AJAX请求,后端Controller接收参数后要校验座位是否空闲、是否超时、是否已被他人锁定。如果用Spring Boot,你可能直接在Controller方法上加@Transactional,但学生根本不清楚这个注解背后是Spring AOP生成代理对象、TransactionInterceptor拦截方法调用、PlatformTransactionManager开启数据库连接。而在SSM里,我们把事务配置单独放在applicationContext-service.xml里:
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="select*" read-only="true" propagation="SUPPORTS"/>
<tx:method name="*" propagation="SUPPORTS"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="serviceOperation" expression="execution(* com.yourpackage.service..*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/>
</aop:config>
学生看这段配置,立刻明白:所有service包下的方法,只要以add/update/delete开头就强制开启事务,以select开头就只读事务,其他方法支持当前事务(如果有的话)。这种显式声明,比Spring Boot的@EnableTransactionManagement+@Transactional组合更利于建立底层认知。
1.2 数据库设计背后的业务约束逻辑
MySQL 8.0不是随便选的版本。MySQL 5.7对JSON字段支持弱,而我们班次表(bus_schedule)里存的是“座位布局JSON”,比如{"rows":3,"cols":5,"layout":[[0,0,0,0,0],[0,0,1,0,0],[0,0,0,0,0]},表示3行5列座位,中间那个是已售。MySQL 8.0原生支持JSON_EXTRACT、JSON_CONTAINS等函数,我们在查询某班次剩余座位数时直接用:
SELECT COUNT(*) FROM bus_seat
WHERE schedule_id = ? AND JSON_EXTRACT(seat_layout, '$.layout[*][*]') = 0;
这比在Java层解析JSON再计数快得多,也避免了内存溢出风险。再比如“线路-班次-车辆”三者关系,很多学生会设计成三个独立表外键关联,但实际业务中存在“同一线路不同日期用不同车型”“同一车辆不同时间段跑不同线路”的复杂场景。所以我们采用“班次主表+关联中间表”设计:
bus_route(线路表):route_id, route_name, start_city, end_city, distance_kmbus_bus(车辆表):bus_id, license_plate, seat_count, bus_typebus_schedule(班次表):schedule_id, route_id, bus_id, departure_time, arrival_time, price, statusbus_schedule_bus(班次-车辆关联表):id, schedule_id, bus_id, assign_date
注意bus_schedule_bus里没有route_id——因为route_id已在bus_schedule里,冗余会导致数据不一致。assign_date字段用来记录“某班次在某天分配给某辆车”,这样就能支持“周一用大巴、周二用中巴”的灵活调度。这种设计不是拍脑袋定的,而是我陪学生去客运站实地调研后确定的:站长说他们系统里最常改的就是车辆分配,因为维修、事故、临时调车太频繁,必须保证修改只影响当天班次,不影响历史数据。
1.3 前端交互与后端状态协同的关键设计
系统前端用的是纯HTML+jQuery+Bootstrap,没上Vue或React,这也是刻意为之。毕设答辩时,老师更关注你能不能说清楚“点击座位后发生了什么”,而不是“Vue响应式原理”。我们把选座逻辑拆成三个原子操作:
- 查座位图:前端发GET
/seat/layout?scheduleId=123,后端返回JSON格式座位布局和已售状态数组; - 锁座位:用户点击后,前端立即发POST
/seat/lock,携带scheduleId和座位坐标,后端用Redis分布式锁(key为seat_lock:${scheduleId}:${row}_${col})防止并发抢座,成功则更新MySQL seat_status字段并设置Redis过期时间15分钟; - 确认订单:用户点击“确认购票”,前端发POST
/order/create,后端校验Redis锁是否存在、MySQL座位状态是否仍为空闲,双重校验通过才生成订单并扣减库存。
这个流程里最关键的不是技术多炫,而是状态一致性保障。很多学生写的选座功能,只在数据库层面校验,结果高并发下出现“显示可选但提交失败”。我们用Redis锁+DB双校验,成本低、效果稳。Redis锁的value存的是客户端IP+时间戳,释放锁时先GET再DEL,避免误删别人锁——这些细节都在SeatLockService.java里写了注释,学生抄作业时至少得看懂注释在说什么。
2. 核心模块细节解析与实操要点
2.1 用户认证与权限隔离实现
系统没用Shiro或Spring Security,而是手写了一套轻量级权限控制,原因很实在:毕设答辩时,老师问“你的登录怎么防暴力破解”,你说“Shiro自带的”,他接着问“Shiro底层怎么实现的”,你就哑火了。我们用最朴素的方式:
- 登录接口
/user/login接收用户名密码,先查user表验证账号密码; - 验证通过后,生成token(UUID+时间戳MD5),存入Redis(key为
token:${uuid},value为userId:123|role:user|expire:1800),设置过期时间30分钟; - 后续所有接口在Interceptor里校验token:从Header取X-Auth-Token,Redis GET,解析出userId和role,塞进ThreadLocal供Controller使用;
- 管理员后台入口
/admin/**在web.xml里配了filter,只放行role=admin的请求。
重点来了:密码存储不用BCrypt,而用SHA-256加盐哈希。不是因为SHA不安全,而是教学需要——学生能自己写DigestUtils.sha256Hex(password + salt),能理解盐值随机生成、存进数据库salt字段,能手动验证哈希过程。BCrypt虽然更安全,但BCryptPasswordEncoder.encode()一行代码掩盖了所有细节,不利于建立密码学基础认知。
还有一个易错点:登录成功后跳转。很多学生写response.sendRedirect("/index.jsp"),结果页面404。因为我们项目结构是标准Maven WebApp,静态资源放在src/main/webapp/下,而/index.jsp路径在Tomcat里实际是http://localhost:8080/index.jsp,但前端Ajax请求的base URL是http://localhost:8080/,所以Controller返回"redirect:/index.jsp"时,SpringMVC会拼成http://localhost:8080/index.jsp,没问题;但如果写成response.sendRedirect("index.jsp"),就会变成相对路径http://localhost:8080/user/login/index.jsp,必然404。这个坑,我在debug时让学生用浏览器开发者工具Network面板抓包,亲眼看到302跳转的Location头,比讲十遍都管用。
2.2 在线选座模块的技术实现细节
选座界面不是一张静态图片,而是动态渲染的HTML表格。关键在于座位状态实时同步。我们没用WebSocket(毕设没必要搞这么重),而是用“轮询+本地缓存”策略:
- 页面加载时,发一次
/seat/layout?scheduleId=123获取初始座位图; - 同时启动定时器,每10秒发一次
/seat/status?scheduleId=123&lastUpdate=1712345678900(lastUpdate是上次获取的时间戳); - 后端对比MySQL seat表的update_time字段,只返回变化的座位坐标和新状态;
- 前端用jQuery
.attr('data-status', '1')更新对应td元素,并改变背景色。
这样既保证了状态新鲜度,又避免了高频全量请求。数据库层面,seat表设计为:
CREATE TABLE `bus_seat` (
`id` bigint NOT NULL AUTO_INCREMENT,
`schedule_id` bigint NOT NULL,
`row_num` tinyint NOT NULL COMMENT '行号,从1开始',
`col_num` tinyint NOT NULL COMMENT '列号,从1开始',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '0空闲,1已售,2锁定',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_schedule_row_col` (`schedule_id`,`row_num`,`col_num`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
注意update_time用ON UPDATE CURRENT_TIMESTAMP,这样每次UPDATE seat_status,时间戳自动更新,轮询时只需查WHERE update_time > ?,不用额外维护版本号字段。这个设计比加version字段更轻量,且MySQL 8.0的timestamp精度到微秒,完全满足10秒轮询需求。
2.3 订单生成与支付模拟逻辑
系统没接入真实支付渠道,但支付流程模拟得很真。订单状态机设计为:
created(创建)→paid(已支付)→confirmed(已确认)→completed(已完成)created→cancelled(已取消)paid→refunded(已退款)
状态变更不是简单update status字段,而是用状态迁移校验:
public boolean changeOrderStatus(Long orderId, String fromStatus, String toStatus) {
// 先查当前状态
Order order = orderMapper.selectById(orderId);
if (!order.getStatus().equals(fromStatus)) {
throw new BusinessException("状态非法:期望" + fromStatus + ",实际" + order.getStatus());
}
// 再校验迁移合法性
if ("created".equals(fromStatus) && !"paid".equals(toStatus) && !"cancelled".equals(toStatus)) {
throw new BusinessException("created只能迁移到paid或cancelled");
}
// 最后更新
order.setStatus(toStatus);
order.setUpdateTime(new Date());
orderMapper.updateById(order);
return true;
}
这样,即使有人绕过前端直接调API,传fromStatus=created&toStatus=completed,也会被拦截。支付模拟接口 /pay/simulate 接收订单ID,随机返回“支付成功”或“余额不足”,成功后调用changeOrderStatus(orderId, "created", "paid")。学生调试时,可以故意把fromStatus写错,看控制台抛出的BusinessException,从而理解状态机的价值。
3. 实操部署与运行全流程详解
3.1 开发环境配置(IDEA + Tomcat 8.5)
这不是“下载安装包点下一步”的傻瓜教程,而是告诉你为什么这么配、不这么配会怎样。
第一步:导入项目到IDEA
- 解压资源包,打开y6uTVYhsS9BigRGembKM-master-d59ca096d2f2950ca52baafcf081bcb0d01c88a5文件夹(这是Git克隆的原始目录);
- IDEA选择File → Open → 选中该文件夹,勾选“Auto-import Maven project”;
- 等待Maven下载依赖(pom.xml里指定了jdk.version=1.8,所以IDEA会自动识别为Java 8项目)。
第二步:配置Tomcat Server
- File → Project Structure → Project → SDK选1.8,Language level选8;
- Run → Edit Configurations → + → Tomcat Server → Local;
- Application server选你本地的Tomcat 8.5(必须8.5,不是9.0——因为项目web.xml里<web-app>声明的是3.1版本,Tomcat 9默认用4.0,会报schema错误);
- Deployment → + → Artifact → 选qichekeyundingpiao:war exploded;
- VM options填 -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=512m(UTF-8防中文乱码,Metaspace防PermGen OOM)。
第三步:数据库配置
- 打开src/main/resources/jdbc.properties,修改四行:
properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/bus_ticket?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=your_password
- 注意serverTimezone=Asia/Shanghai必须加,否则MySQL 8.0会报The server time zone value 'XXX' is unrecognized;
- allowPublicKeyRetrieval=true是MySQL 8.0新加的安全限制,不加连不上。
第四步:初始化数据库
- 用Navicat或命令行执行db.sql(不要用MySQL Workbench的GUI导入,它会忽略DELIMITER);
- db.sql里包含CREATE DATABASE IF NOT EXISTS bus_ticket CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,所以直接运行就行;
- 执行完检查bus_user表里是否有admin/admin测试账号(密码是SHA-256哈希后的字符串,明文是admin)。
第五步:启动验证
- 点击IDEA右上角绿色三角形启动Tomcat;
- 控制台输出INFO : org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'springmvc': initialization completed in 1234 ms即成功;
- 浏览器访问http://localhost:8080/,看到首页即OK;
- 用admin/admin登录,进后台添加一条线路、一辆车、一个班次;
- 用普通用户注册,查班次,选座,下单——整个链路跑通。
提示:如果启动报
ClassNotFoundException: com.mysql.cj.jdbc.Driver,说明MySQL Connector/J没下载成功。去Maven仓库.m2/repository/mysql/mysql-connector-java/8.0.28/看jar包是否完整(约2.3MB),不完整就删掉整个目录让Maven重下。
3.2 Linux服务器一键部署(start.sh/run.sh原理)
start.sh不是简单的sh catalina.sh start,而是做了三件事:
-
环境预检:
bash # 检查Java if ! command -v java &> /dev/null; then echo "Error: Java not installed" exit 1 fi # 检查MySQL if ! nc -z localhost 3306; then echo "Error: MySQL not running on port 3306" exit 1 fi -
配置替换:
脚本会读取conf/db.conf(用户自定义的数据库地址/账号),用sed命令替换war包里WEB-INF/classes/jdbc.properties的占位符:
bash sed -i "s/{DB_URL}/$(cat conf/db.conf | grep url | cut -d'=' -f2)/g" target/qichekeyundingpiao.war -
静默启动:
用nohup ./bin/startup.sh > /dev/null 2>&1 &启动,避免终端关闭导致Tomcat退出。
run.sh则是开发机调试用的,它会:
- 自动编译mvn clean compile package -Dmaven.test.skip=true;
- 清空Tomcat webapps下的旧war包;
- 复制新war包过去;
- 调用curl http://localhost:8080/qichekeyundingpiao/health检测应用是否启动成功(/health接口返回{"status":"UP"});
- 成功则打开浏览器xdg-open http://localhost:8080/qichekeyundingpiao。
这两个脚本的存在,让学生第一次部署时不用记startup.sh在哪、webapps目录在哪、怎么查端口占用,降低心理门槛。
3.3 关键配置文件逐行解读
pom.xml里藏着很多教学线索:
<properties>里定义了spring.version=5.3.32,这是Spring 5最后一个稳定版,兼容Java 8且无重大breaking change;<dependency>里mysql-connector-java版本是8.0.28,对应MySQL 8.0,驱动类名从com.mysql.jdbc.Driver升级为com.mysql.cj.jdbc.Driver;<plugin>里maven-compiler-plugin指定source=8target=8,确保编译出的class文件能在Java 8 JVM运行;<build>里resources节点包含<includes><include>**/*.xml</include></includes>,这是为了把MyBatis的Mapper XML文件打包进war,否则运行时报Invalid bound statement (not found)。
web.xml是SSM的灵魂:
<!-- Spring容器启动 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- SpringMVC前端控制器 -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
这里的关键是<load-on-startup>1</load-on-startup>:数字越小优先级越高,确保DispatcherServlet在Tomcat启动时就初始化,而不是第一次请求才加载。如果删掉这行,首次访问会慢2-3秒,学生会误以为系统卡顿。
4. 常见问题与排查技巧实录
4.1 “404错误”的七种可能及定位方法
404不是“页面找不到”那么简单,得像侦探一样找线索:
| 现象 | 可能原因 | 定位方法 | 解决方案 |
|---|---|---|---|
访问http://localhost:8080/ 404 | Tomcat没启动或端口被占 | netstat -ano \| findstr :8080(Windows)或lsof -i :8080(Linux) | 杀掉占用进程或改Tomcat端口 |
访问http://localhost:8080/login 404 | Controller没被SpringMVC扫描到 | 查IDEA控制台是否有Mapped "{[/login],methods=[POST]}"日志 | 检查spring-mvc.xml里<context:component-scan base-package="com.xxx.controller"/>包路径是否正确 |
访问http://localhost:8080/static/css/style.css 404 | 静态资源没配置放行 | 查spring-mvc.xml是否有<mvc:default-servlet-handler/> | 加上该标签,或配置<mvc:resources mapping="/static/**" location="/static/" /> |
Ajax请求/seat/layout返回404 | URL路径大小写错误 | 浏览器F12 Network面板看Request URL | Controller方法上@RequestMapping("/seat/layout")必须和前端AJAX的url完全一致(区分大小写) |
JSP页面里<%@ include file="/header.jsp"%> 404 | header.jsp不在WebContent目录下 | 在IDEA项目结构里确认src/main/webapp/header.jsp路径 | 把jsp文件放到src/main/webapp/下,不是src/main/java/ |
访问http://localhost:8080/qichekeyundingpiao/ 404 | 应用没部署成功 | 进Tomcat webapps目录看是否有qichekeyundingpiao文件夹 | 检查IDEA Deployment配置是否选对Artifact,重启Tomcat |
| 所有URL都404 | web.xml配置错误 | 查Tomcat logs/catalina.out是否有Parse error in application web.xml | 用XML Validator校验web.xml语法,特别注意<web-app>的namespace是否匹配 |
我让学生养成习惯:遇到404,第一件事不是改代码,而是打开浏览器开发者工具,看Network面板里Request URL是什么、Response Headers里有没有X-Application-Context(Spring Boot才有)、Status Code是不是真的404(有时是302跳转到404页面)。有一次一个学生折腾半天,最后发现是他把<url-pattern>/</url-pattern>写成了<url-pattern>/*</url-pattern>,导致静态资源也被DispatcherServlet拦截,而Controller里没写处理CSS的映射。
4.2 “数据库连接失败”的根因分析
报错Cannot create JDBC driver of class 'com.mysql.cj.jdbc.Driver',表面是驱动问题,实际可能是五层嵌套:
- Jar包缺失:Maven没下载成功 → 删
.m2/repository/mysql/重下; - 驱动类名错误:MySQL 5.7用
com.mysql.jdbc.Driver,8.0必须用com.mysql.cj.jdbc.Driver→ 改jdbc.properties; - JDBC URL参数缺失:少了
serverTimezone=Asia/Shanghai→ 加上; - MySQL服务未启动:
systemctl status mysqld(CentOS)或brew services list \| grep mysql(Mac)→ 启动服务; - 用户权限不足:root用户被禁用远程访问 →
mysql -u root -p后执行GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'your_password'; FLUSH PRIVILEGES;。
最隐蔽的是第五种:学生用Navicat连得上,但Java连不上。因为Navicat连的是localhost,而Java程序连的是127.0.0.1,MySQL里root@localhost和root@127.0.0.1是两个用户。解决方案是统一用root@'%',或者在jdbc.url里把localhost改成127.0.0.1。
4.3 “选座后状态不更新”的并发调试实战
现象:两个用户同时选同一个座位,前端都显示“已选中”,但数据库里只有一个被更新。这不是Bug,是预期行为,但学生常以为是程序错了。
调试步骤:
- 在
SeatService.lockSeat()方法第一行加System.out.println("Locking seat for scheduleId=" + scheduleId + ", row=" + row + ", col=" + col);; - 启动两个浏览器窗口,登录两个不同账号;
- 同时点击同一个座位,观察控制台输出:
- 如果只打印一次,说明Redis锁生效,第二个请求被阻塞;
- 如果打印两次,说明锁没起作用,检查Redis key是否一致(seat_lock:${scheduleId}:${row}_${col}); - 查MySQL seat表,看那条记录的
status和update_time; - 查Redis,用
redis-cli执行GET seat_lock:123:2_3,看value是否为当前客户端IP。
有一次学生发现锁失效,最后定位到是JedisPool配置的maxTotal=1,导致多个线程抢同一个Jedis实例,setex命令串行执行。改成maxTotal=10就解决了。这个案例告诉我们:并发问题不能只看业务代码,还得看中间件配置。
4.4 毕设答辩高频问题应答清单
我把近三年答辩老师问的问题整理成速查表,附答案要点(不是背稿,是理解逻辑):
| 问题 | 回答要点 | 为什么这么答 |
|---|---|---|
| 为什么用SSM不用Spring Boot? | “SSM让我清晰看到IoC容器如何加载Bean、MVC如何分发请求、MyBatis如何执行SQL。比如事务配置在applicationContext-service.xml里显式声明,比Spring Boot的@Transactional更利于理解AOP原理。” | 展示技术选型的思考过程,不是跟风 |
| 怎么防止黄牛刷票? | “前端加按钮置灰+倒计时,后端用Redis分布式锁控制单座位并发,数据库加唯一索引(schedule_id+row_num+col_num),三重防护。” | 体现分层防御思维,不说“用了XX算法”这种虚话 |
| 订单超时未支付怎么处理? | “用Quartz定时任务每5分钟扫描status=created且create_time早于当前时间30分钟的订单,调用cancelOrder()方法回滚座位状态。” | 说出具体技术(Quartz)、频率(5分钟)、条件(30分钟)、动作(回滚),证明真做过 |
| MySQL怎么优化查询性能? | “班次查询加了复合索引(route_id, departure_time),座位状态查用tinyint(1)减少存储,轮询用update_time范围查询避免全表扫描。” | 用具体字段、具体类型、具体SQL说明,不谈“加索引”这种空话 |
| 如果用户投诉买不到票,怎么排查? | “先看Redis锁是否堆积(redis-cli keys ‘seat_lock*’),再查MySQL seat表status分布,最后看Tomcat线程池是否满(jstack pid | grep http-nio)。” | 给出可操作的排查路径,体现运维意识 |
最后分享个小技巧:答辩前,把系统里所有System.out.println()删掉,换成SLF4J的log.info(),并在logback.xml里配置<root level="WARN">——这样答辩时控制台不会刷屏,显得更专业。这个细节,很多学生都不知道。
我在实际使用中发现,这套系统最大的价值不是功能多全,而是每个模块都留了可扩展的钩子:比如OrderService.createOrder()方法里,// TODO: 发送短信通知的注释,就是留给学生加阿里云短信SDK的地方;AdminUserController.java里@PreAuthorize("hasRole('ADMIN')")注释掉的Spring Security配置,就是引导学生去研究权限框架的入口。它不是一个终点,而是一个精心设计的起点——你站在上面,能看到更远的地方该怎么走。
简介:这个汽车票务系统用Java开发,基于SSM框架(Spring+SpringMVC+MyBatis),后端搭配MySQL 8.0数据库,前端界面简洁易用。用户能完成注册登录、实时查询班次、可视化选座、生成订单并查看订单状态;管理员可通过后台管理车辆信息、线路设置、班次排期和全部订单。项目已适配IntelliJ IDEA,内置Tomcat 7.x/8.x部署方案,开箱即用——导入项目后只需配置数据库连接地址和账号密码,就能直接启动运行。压缩包里包含完整源代码、建库脚本db.sql、Navicat可导入的数据库样例(xlsx格式)、详细配置说明txt、开题报告、答辩PPT、Word版毕业论文,以及Linux下一键启动脚本(start.sh/run.sh)。所有功能模块均经真实环境测试,适合本科毕业设计选题、Java Web课程实训或期末综合大作业直接复用。
&spm=1001.2101.3001.5002&articleId=162856278&d=1&t=3&u=3eb3e4ab12a949d29715b02bba0c34b4)
980

被折叠的 条评论
为什么被折叠?



