我在帮人改课设代码的时候,见过太多次这种场景:选题是“基于Java的电影购票系统”,需求文档写得很丰满,结果很多同学交上来的东西就一个控制台黑窗口,输入数字选电影、选座位、打印一张“票据”,然后就没有然后了。这显然和“功能完善、符合实际”这几个字差了十万八千里。
电影购票这个场景,放在Java Web里其实是非常经典的一类业务系统。它不像电商那样SKU复杂到让人头皮发麻,但又具备完整的核心交易链路:用户登录、影片浏览、场次选择、座位锁定、订单生成、支付状态流转、后台管理和数据统计。把它做扎实了,你对Java Web开发的理解绝对会上升一个台阶,而且这个东西放进简历里,比那些烂大街的“学生管理系统”有说服力得多。
这篇文章我准备从一个完整的实战角度来拆解这套系统。不是粘贴一堆零散的代码片段就完事,而是把设计思路、表结构、核心逻辑、权限控制、部署运行这些环节全部捋一遍。如果你正好在做课设、毕设,或者想找个能写进简历的真实项目,这篇内容可以帮你省下大量摸索的时间。文末我会把完整代码的获取方式和运行步骤一并交代清楚,保证拿下来能直接跑。
1. 系统整体设计与技术选型
1.1 为什么选这个题,它能练到什么
很多同学选课题有个误区,总觉得功能越多越显水平,于是什么积分商城、优惠券、会员等级全往里面塞。结果是代码写了一堆,核心链路反而做得稀烂。
电影购票系统这个题目的妙处在于,它的业务边界很清晰,但技术覆盖点一点都不少。从用户端看,需要处理注册登录、影片列表、影片详情、场次查询、选座、订单创建;从管理端看,需要处理影片管理、场次排片、订单管理、数据统计。这一套走下来,你等于把Java Web开发的常见模块全部过了一遍。
更重要的是,它天然带着两个高含金量的技术考点:一是座位锁定的并发问题,二是订单和库存的一致性事务问题。这两个点无论你以后是去面试还是继续做项目,都是躲不开的核心话题。能把这两个问题在项目里讲清楚,比背十道八股文都有用。
1.2 技术栈选型思路
技术栈的选择,我建议优先考虑“自己熟悉且部署成本低”的方案,而不是盲目追求新框架。经典组合是 JSP + Servlet + MySQL + Tomcat,这套组合至今仍是很多高校课程设计的主流环境。它最大的好处是能让你看清HTTP请求从发起、到Servlet处理、再到页面渲染的完整过程,不会被Spring Boot的自动化配置遮住底层逻辑。
当然,如果你已经有一定基础,用 Spring Boot + MyBatis Plus + Vue 来做前后端分离版本也完全可以。但我要提醒一点:如果你的课程设计要求是“提交可运行的Web项目”,那么 JSP 版本在演示和答辩时反而更占优势——一个Tomcat丢上去就能跑,评委不用单独启动前端服务,也不用配Nginx。
我个人建议的做法是:主体用 JSP + Servlet 完成核心功能,保证项目开箱即跑、逻辑清晰,同时适当地用一些工程化手法(比如统一响应类、工具类封装、三层架构分层)让代码结构看起来规范。这套组合既有传统课设的稳定,又有接近企业项目的质量感。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表结构
2.1 用户、影片、场次、订单四张核心表
数据库设计是整个系统的地基。很多课设项目死在第一步,就是因为表建得乱七八糟,字段一会叫 name 一会叫 username,类型一会儿是 int 一会儿是 varchar,写代码的时候全在跟这些不一致做斗争。
先给出最核心的四张表:
用户表(t_user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int 主键自增 | 用户ID |
| username | varchar(50) 唯一 | 登录名 |
| password | varchar(100) | 密码(MD5加密存储) |
| phone | varchar(20) | 手机号 |
| real_name | varchar(50) | 真实姓名 |
| create_time | datetime | 注册时间 |
影片表(t_movie)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int 主键自增 | 影片ID |
| title | varchar(100) | 片名 |
| director | varchar(50) | 导演 |
| actors | varchar(200) | 主演 |
| genre | varchar(50) | 类型(喜剧/动作/科幻) |
| duration | int | 时长(分钟) |
| release_date | date | 上映日期 |
| poster | varchar(255) | 海报图片路径 |
| description | text | 剧情简介 |
| status | int | 上架状态 0下架 1上架 |
场次表(t_schedule)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int 主键自增 | 场次ID |
| movie_id | int | 关联影片ID |
| hall_name | varchar(50) | 放映厅名称 |
| start_time | datetime | 放映时间 |
| price | decimal(10,2) | 票价 |
| remaining_seats | int | 余票数量 |
| seat_status | text | 座位状态,JSON字符串 |
这个 seat_status 字段我要特别说一下。很多同学第一次做会单独建一张座位表,每个座位一行记录,这样不是不行,但做起来会非常繁琐——一场100个座位就是100条记录,每次下单要更新一条记录,查询时还要组装二维数组。
更务实的做法是用一个文本字段存储整个影厅的座位状态。比如 10 排 10 座的影厅,可以用 ["0000000000","0000000000",...] 这样的字符串数组来存,0代表空座,1代表已售。查询时直接解析JSON,下单时通过事务更新对应位置的字符。这种方式在数据量不大的课设场景下,实现简单、运行效率高,而且完全够用。
订单表(t_order)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int 主键自增 | 订单ID |
| order_no | varchar(50) 唯一 | 订单编号 |
| user_id | int | 下单用户ID |
| schedule_id | int | 场次ID |
| seat_info | varchar(255) | 座位信息,如“3排5座,3排6座” |
| ticket_count | int | 购票数量 |
| total_price | decimal(10,2) | 订单总价 |
| status | int | 订单状态 0待支付 1已支付 2已取消 |
| create_time | datetime | 下单时间 |
2.2 表关联逻辑与索引设计要点
四张表之间的关联关系,理解起来很直观:一个用户对应多个订单,一个场次对应一个影片,一个订单对应一个场次的多张座位。
外键方面,我的建议是逻辑外键,而不是物理外键。也就是说,在订单表里存 user_id、schedule_id,但不真正创建 FOREIGN KEY 约束。原因很简单:物理外键会导致查询时MySQL强制做关联检查,在数据量大时会有性能损耗,最关键的是,课设演示过程中你经常会手动清数据、改数据,有外键约束容易卡住操作。
索引方面,t_order 表的 user_id 字段建议加一个普通索引,因为用户查询“我的订单列表”是高频操作。t_schedule 表的 movie_id 建议也加个索引,因为用户按影片查场次也是高频操作。order_no 订单号用唯一索引,保证不重复。
订单号的生成这里提一句,推荐用时间戳加随机数的组合:SimpleDateFormat 格式化当前时间到秒,再拼上4位随机数,最后加上用户ID后两位做散列。这样生成的订单号既不会重复,又带有业务含义。直接自增ID当订单号的话,既不好看,也容易暴露系统订单量。
3. 核心功能模块与业务逻辑实现
3.1 用户端:从注册到下单的完整链路
用户端的功能,本质上是“选片 → 选场次 → 选座位 → 下单 → 支付(模拟)→ 查看订单”这条链路,任何一个环节断掉,体验都会崩溃。
注册登录模块需要注意的点是密码存储。明文存密码是课设代码里的重灾区,哪怕是一个课程设计,也应该用 MD5 或加盐的方式处理密码。在 Java 里用 MessageDigest 类可以很轻松地完成 MD5 加密,代码也就十几行。虽然 MD5 现在已经不算安全加密方式了,但在课设场景里,它足以体现你的工程意识。如果你想让答辩老师眼前一亮,可以用 SecureRandom 生成一个盐值,然后做 MD5(密码 + 盐值) 的哈希存储。
影片展示模块要做一个合理的列表页和详情页。列表页支持按类型筛选、按名称模糊搜索,这些用 SQL 的 LIKE 语句就能实现。详情页要展示影片的海报、简介、导演演员信息,以及该影片的所有未来场次。这里有个细节容易被忽略——查场次时一定要加 start_time > NOW() 的条件,否则会把过去的场次也列出来,用户下单后才发现看不了,这就闹笑话了。
选座页是整个用户端最有交互感的部分。前端用 HTML 表格生成一个二维座位图,空位显示为绿色可选,已售座位显示为灰色不可点,用户点击座位后座位变红表示选中,再次点击取消。选好后点击“立即购买”,把座位坐标集合和场次ID提交到后端。
3.2 核心业务:座位锁定与防超卖的并发处理
这里就是整个系统最核心、最有含金量的地方了——座位的并发锁定问题。
我们先想想一个极端情况:一场电影还剩最后一个座位,用户A和用户B同时点击购买。如果代码是先查 remaining_seats,看到是1,然后都执行下单,最终就会卖出两张票,也就是所谓的“超卖”。
解决思路有几种层次。最基础的方式是在更新时加条件判断。更新场次余票的 SQL 写成这样:
sql复制UPDATE t_schedule SET remaining_seats = remaining_seats - 1 WHERE id = #{scheduleId} AND remaining_seats > 0
这条 SQL 本身是原子操作,数据库底层的行锁会保证同一时刻只有一个事务能修改这条记录。执行后通过 int affectedRows 判断是否更新成功,返回0说明余票不足,下单失败。这就是所谓的“乐观锁”思路。
在此基础上,为了确保订单记录和票数扣减的一致性,必须把这两个操作放进同一个数据库事务里:先插入订单记录,再更新余票数量,如果任何一步失败就整体回滚。在 Servlet 里可以通过 Connection.setAutoCommit(false) 手动管理事务,或者直接使用 Spring 的 @Transactional 注解来声明式管理。
如果你想把项目做成亮点,可以做一点“分布式锁”的替代方案——在 Java 层面用 synchronized 或 ReentrantLock 对同一个场次的购票操作加锁。但注意,单机锁在真正部署到多台服务器时就不起作用了,这个利弊要能在答辩时讲清楚。我在代码里默认采用的是数据库条件更新和事务控制,这是最简单可靠的方式。
3.3 管理端:影片维护与订单管理
管理端是体现“功能完善”的重要部分,很多课设项目在这里偷懒,只做了个简单的列表页,看起来就很单薄。
我的建议是管理端至少包含四个模块:
影片管理:支持影片的增删改查。新增影片时上传海报图片,注意要做文件类型的校验,只允许 jpg、png 格式,大小限制在2MB以内。编辑影片时支持修改上架/下架状态,下架的影片在前端列表里不可见。
场次管理:为影片安排放映场次,选择放映厅、设置时间、票价、初始余票数。新增场次后,系统需要为新场次初始化座位状态,比如10排10座就生成一个 ["0000000000","0000000000"...] 的字符串数组。
订单管理:管理员可以查看所有用户的订单,按订单号或用户名搜索,可以对异常订单做取消操作。取消订单时一定要记得恢复对应场次的余票和座位状态,否则数据会越走越偏。
数据统计:如果管理系统功能要求更高,可以加一个简单的统计页面,展示影片总票房排行TOP10、每日订单量趋势等。SQL 用 GROUP BY 和聚合函数就能做出来,但它极大地提升了项目的大局观和答辩说服力。
4. 项目结构规范与代码实现要点
4.1 三层架构如何组织分包
分包结构可以直接反映一个人的代码素养。我见过很多课设代码把几百行代码全写在一个 Servlet 类里,导出的时候全是红杠杠,这样即使功能能跑,答辩老师打开代码看两眼心里就打了个低分。
推荐的分包结构如下:
java复制com.cinema.controller // 控制层:Servlet
com.cinema.service // 业务逻辑层:接口 + 实现类
com.cinema.dao // 数据访问层:JDBC操作数据库
com.cinema.entity // 实体类:User、Movie、Schedule、Order
com.cinema.util // 工具类:DBUtil、MD5Util、JsonUtil
com.cinema.filter // 过滤器:登录校验、编码处理
控制层只做参数接收和页面跳转,不写SQL;业务层做具体逻辑判断和事务控制;DAO层只做数据库的增删改查。这个分层逻辑对标的就是企业里的Controller-Service-Mapper三层结构,你以后学了 Spring 会发现几乎一模一样。
DAO层的实现,我建议用 Apache Commons DbUtils 这个工具类来简化 JDBC 操作。它内置了 QueryRunner,支持把查询结果自动封装成实体类对象,能把原来二三十行的 JDBC 样板代码压缩到三五行。这个依赖在 Maven 里加一行就行:
xml复制<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.7</version>
</dependency>
4.2 登录拦截与MVC分层的关键代码
对于一个“功能完善”的系统来说,权限控制是必不可少的一环。用户未登录时,应该只能访问影片列表和详情页;点击购买时被拦截,跳到登录页。管理员的页面则必须要求登录账号是管理员角色。
这个功能最优雅的实现方式是写一个 LoginFilter,在请求到达 Servlet 之前做校验:
java复制public class LoginFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
HttpSession session = request.getSession();
Object user = session.getAttribute("loginUser");
if (user == null) {
response.sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
chain.doFilter(request, response);
}
}
然后在 web.xml (或注解) 里配置过滤器的 URL 映射,对 /user/*、/order/* 这些需要登录才能访问的路径进行过滤。这里有个坑要提醒:如果仅配置了过滤 Servlet 路径,静态资源(css、js、图片)可能也会被过滤器拦掉,导致页面样式丢失。解决方法是在过滤器里放行 .css、.js、.jpg、.png 后缀的请求。
Servlet 层拿参数、调服务、返回页面的套路也要统一。比如购票请求的 Servlet 核心逻辑大致是:
java复制protected void doPost(HttpServletRequest request, HttpServletResponse response) {
// 1. 获取参数
int scheduleId = Integer.parseInt(request.getParameter("scheduleId"));
String seatInfo = request.getParameter("seatInfo");
HttpSession session = request.getSession();
User user = (User) session.getAttribute("loginUser");
// 2. 调用业务层
Result result = orderService.createOrder(user.getId(), scheduleId, seatInfo);
// 3. 返回结果
response.setContentType("application/json");
response.getWriter().write(new Gson().toJson(result));
}
前端用 AJAX 提交座位数据,接收返回的 JSON 结果,动态展示“购票成功”或“座位已被占用”。这种前后端通过 JSON 交互的方式,跟传统的表单提交跳转相比,用户体验好很多,而且答辩的时候也更有亮点可讲。
5. 系统部署运行与完整代码获取
5.1 本地环境搭建与启动步骤
这套系统的运行环境要求不高,常规配置就能跑起来:
- JDK 8 或以上版本
- MySQL 5.7 或 8.0
- Tomcat 8.5/9.0
- IDEA 或 Eclipse
拿到完整代码包后,按下面的步骤操作:
- 在 MySQL 中创建数据库,建议字符集选择
utf8mb4,执行项目里的cinema.sql脚本,自动建库建表并插入测试数据。 - 用 IDEA 打开项目(基于 Maven 构建),等待依赖下载完成。
- 修改数据库配置文件中
db.properties的用户名和密码。这里特别提醒,MySQL 8.0 的驱动配置和旧版不一样,驱动类要写成com.mysql.cj.jdbc.Driver,连接串里要加上serverTimezone=Asia/Shanghai,否则会报时区错误。 - 配置 Tomcat,把项目部署到 Tomcat 中,启动 Tomcat,控制台会打印部署成功的日志。
- 浏览器访问
http://localhost:8080/cinema/,系统就起来了。
5.2 内置账号与演示数据说明
代码包里预置了一些测试数据,方便你立刻演示。管理员账号通常是 admin/admin123,普通用户一般预置了 user1、user2 等账号,密码统一写在 README 文档里。
测试数据中包含了5部左右不同类型的影片,每部影片配了2到3个场次,分布在不同的时间段,足够展示完整流程。数据脚本里还特意造了一个“即将售罄”的场次,用来演示座位锁定的效果,这个细节在答辩演示时可以直接拿来当素材。
完整代码下载的方式,在项目 README 里都会写清楚。如果你刚拿到代码包,建议先不要着急改代码,老老实实按 README 步骤跑起来一遍,把业务流程走通,再动手去改自己想要调整的部分。这个习惯会让你在以后做任何项目时都少走很多弯路。
5.3 部署中容易踩的几个坑
我整理了一下大家拿到代码后最常遇到的五个问题,提前打个预防针:
问题一:Tomcat 启动闪退。通常是 JAVA_HOME 环境变量没配置好,在命令行输入 java -version 确认能输出版本号再说。
问题二:页面中文乱码。检查浏览器编码、JSP 页面头部 pageEncoding="UTF-8"、数据库连接串里是否加了 characterEncoding=utf8。三个地方一个都不能少。
问题三:数据库连不上。先确认 MySQL 服务有没有启动,再确认密码对不对,最后检查驱动包有没有导入。
问题四:报 ClassNotFoundException: com.mysql.jdbc.Driver。这是驱动版本问题,MySQL 5.7 用 com.mysql.jdbc.Driver,MySQL 8.0 要换成 com.mysql.cj.jdbc.Driver。
问题五:端口被占用。Tomcat 默认 8080 端口被其他程序占用时,打开 server.xml 把端口改成 8081 或 9090 即可。
6. 常见问题排查与性能优化进阶
6.1 并发购票导致的“超卖”溯源排查
框架写好了、功能跑通了,这时不要急着收工。我强烈建议你把“超卖”这个场景重新拿出来,先想办法复现它,再去查看你的代码是怎么防住的。很多同学做项目,代码是“抄”的或“套”的,并不能真正讲出设计意图,答辩时老师一追问就露馅。
复现方法很简单:在代码里写一个测试接口,用多线程模拟两个用户同时购买同一个场次的最后一个座位。如果系统报错或两张订单都成功,说明事务和条件更新没有配合好;如果只有一个订单成功,另一个提示失败,说明防超卖机制生效了。把这条测试链路走一遍,你对事务和并发控制的理解会远超那些只会背八股文的人。
在此基础上做性能优化,可以考虑给 t_schedule 表的 id 加“悲观锁” SELECT ... FOR UPDATE,但这会把并发度降下来,不适合高吞吐场景。更好的做法是保留现有“乐观锁”方案,因为电影选座的业务本身并发量不大,而且用户选座时需要先看到实时座位图,悲观锁反而会拖慢响应。
6.2 查询性能慢与数据量增长应对
如果你的系统里测试数据比较多,比如往场次表里插了几万行数据,用户查询影片列表和场次时会明显变慢。这时可以从两个维度优化:
索引优化:确保 t_schedule.movie_id、t_order.user_id 都有索引。在 Navicat 里可以执行 EXPLAIN SELECT ... 看 SQL 是否走索引。
页面数据分页:影片列表如果一次性查出全部数据渲染到页面上,数据多了页面会卡。建议做分页查询,SQL 用 LIMIT 控制数据量,前端显示“上一页/下一页”按钮,这是一个很实用的工程能力体现。
有一个隐藏的性能杀手值得单独提出来:如果你的影片海报图片是 Base64 直接存到数据库里的,数据量会膨胀得非常快。合理做法是图片存文件路径,数据库只存字符串,图片文件放在 Tomcat 的某个静态资源目录下。这也是为什么我在表结构里用 poster varchar(255) 存路径而不是二进制字段。
6.3 代码定位思路与日志辅助排查
最后说一个排查问题的通用思路。系统报错的时候,很多人第一反应是到处看代码,其实最高效的做法是先看日志。Tomcat 的日志默认打印在控制台,IDEA 控制台里会显示完整的异常堆栈信息。找到 Caused by: xxx 这一行,基本就是问题根因。
比如最常见的 java.lang.NullPointerException,说明某个对象没有初始化。这时你先看报错的行号,找到对应的代码,再回过去查这个对象是从哪儿来的,是传参丢了,还是从数据库查出来就是空。按照这个路径排查,比瞎改代码效率高得多。
我在代码里特意加上了统一的日志输出工具类,每个关键操作(下单、支付、取消订单)都会在控制台打印业务日志。你运行时可以看到类似 下单成功:订单号2024061215301234,用户ID 1,场次ID 8,座位 3排5座 这样的记录,不仅是排错的依据,答辩时也能作为系统“容错能力”的展示素材。
写在最后
我实际带过的同学里,有人拿到这份完整代码后,第一件事就是把包名改成自己名字的缩写,却不理解里面任何一处设计;也有人花了一周时间把代码吃透,然后把 JSP 版本换成了 Spring Boot 版本,把座位状态管理从字符串改成单独的座位表,整个项目焕然一新。这两种人最后的答辩成绩和面试效果,差距是非常明显的。
不管你是直接用这套代码交作业,还是打算二次开发,我都建议你亲手从头到尾用 Postman 调一遍接口,用 Navicat 翻一遍数据库表。这个过程不需要太长,一个周末就够了,但它能让你对这个系统有真正的掌控感。
这套完整代码我已经整理好并打包在网盘里,内容包括:完整项目源码、数据库初始化脚本、部署文档、选题答辩PPT模板。你下载后按上面的步骤操作,十分钟内就能在浏览器里看到你的电影购票系统跑起来。如果在部署过程中遇到任何问题,也欢迎随时来交流。
