☰
JSP+SqlServer影院管理系统实战:选座、排片与并发处理
2026/10/8 11:55:50 网站建设 项目流程

简介:这份资源是一套基于JavaEE技术栈的JSP网络版影院管理系统完整项目源码,面向学习Java Web开发、需要企业级项目实战参考的开发者与在校学生。系统围绕影院日常运营展开,涵盖售票、排片、会员管理、订单处理等业务模块,采用Servlet、JSP、JavaBean与MVC模式组织代码,并以SqlServer存储影片、场次、用户等结构化数据,通过JDBC完成交互,是理解JavaEE开发流程与数据库设计的典型实例。压缩包共626个文件,约7.5MB,以398个gif、75个jsp页面、37个css样式、30个htm、26个js脚本及25个jpg图片为主,另含jar依赖、db与mdf/ldf数据库文件、xml配置等,前端界面与后端逻辑资源齐全。目前已有94人学习下载,适合作为课程设计、毕业设计或自学练手项目,帮助读者掌握从页面构建到数据持久化的完整实现思路。

1. 影院管理系统用 JSP + SqlServer 落地:从选座到排片,这套老架构为什么还在跑

影院管理系统这个需求,几乎每年都会出现在课程设计和中小型外包里。它要解决的核心问题很具体:一部电影在哪个厅、几点放、还剩多少座位、谁买了哪张票、会员卡里还有多少钱。用 JavaEE 的 JSP 做页面、SqlServer 存数据,是过去十几年里最稳的组合之一。放到今天,它依然适合两类人:一是要交一份能跑起来的完整网站设计,二是接手了老系统需要维护和二次开发。这套架构不新,但它的每一层都足够透明,出了问题你能顺着页面、Servlet、SQL 一路查到根上,不像某些框架报错只给你一个黑匣子。下面我按实际做项目的顺序,把选型、建库、页面、排错和进阶技巧拆开讲。

2. 技术选型与运行环境:为什么是 JSP + SqlServer 而不是别的

2.1 这套组合到底解决什么问题

影院管理系统的业务本质是「场次 + 座位 + 订单」三张核心表在互相牵制。用户打开页面要看到某场次的可选座位图,点选后生成订单,支付成功后座位状态要立刻变掉,同时后台要能排片、改价、查报表。JSP 的优势在于它本质就是带 Java 代码的 HTML,页面里直接嵌循环就能把座位图渲染出来,不需要前后端分离那一套接口约定。SqlServer 的优势在于事务和锁机制成熟,处理「两个人同时抢同一个座位」这种并发场景,用一条带条件的 UPDATE 就能解决,不用额外引入分布式锁。

常见做法是:JSP 负责展示和表单提交,Servlet 或简单的 JavaBean 处理业务逻辑,JDBC 直连 SqlServer。这套结构在中小型影院(3 到 8 个厅)的并发量下完全够用,日订单量几千笔以内不会有性能问题。我一般会建议把数据库连接池加上,哪怕只是简单的 DBCP,也比每次请求都新建连接强得多。

2.2 环境搭建的具体版本与配置

环境这块最容易翻车。JDK 建议用 8 或 11,Tomcat 用 8.5 或 9,SqlServer 用 2012 及以上都行。注意 SqlServer 2016/2017/2019 安装时如果报「无法找到数据库引擎启动句柄」,八成是之前装过残留了实例,需要先彻底卸载再重装,或者换个实例名。安装时记得把身份验证模式设成「混合模式」,否则 JDBC 用 sa 账号连不上。

# 检查 JDK 版本,JSP 项目建议 8 或 11 java -version # Tomcat 启动前确认端口没被占用,默认 8080 netstat -ano | findstr :8080 # SqlServer 服务确认在跑,服务名一般是 MSSQLSERVER net start | findstr MSSQL

上面三条命令是动手前的基础检查。第一条确认 JDK 装好且版本对,JSP 编译依赖它。第二条查 8080 端口,Tomcat 起不来最常见的原因就是端口被别的程序占了,找到 PID 后在任务管理器里结束掉。第三条确认 SqlServer 服务已启动,服务没起的话 JDBC 连接会直接抛异常,报「连接被拒绝」。

数据库连接配置我一般写在db.properties里,不硬编码到 Java 代码中:

# db.properties 数据库连接配置 jdbc.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver jdbc.url=jdbc:sqlserver://localhost:1433;DatabaseName=CinemaDB;encrypt=false jdbc.username=sa jdbc.password=YourStrongPassword

驱动类名是 SqlServer 固定的,url 里的DatabaseName换成你自己的库名,encrypt=false在本地开发时加上,否则新版驱动会强制加密导致连接失败。用户名密码按实际填,生产环境别用 sa,建个专用账号只给必要的权限。

2.3 项目目录结构与关键文件

一个能跑的 JSP 影院系统,目录结构大致如下。这个结构不复杂,但每一层职责要分清,不然后面改需求会牵一发动全身。

目录/文件作用注意点
WebContent/存放 JSP 页面和静态资源座位图图片、CSS 放这里
WebContent/WEB-INF/web.xml部署描述符Servlet 映射和过滤器配置
src/com/cinema/dao/数据库访问层每个表一个 DAO 类
src/com/cinema/servlet/控制层处理登录、选座、下单
src/com/cinema/util/工具类数据库连接、日期处理
WebContent/seat.jsp选座页面核心页面,逻辑最重

DAO 层负责所有 SQL 操作,Servlet 只做参数接收和页面跳转,JSP 只做展示。这个分层看起来啰嗦,但当你需要把 SqlServer 换成别的库时,只改 DAO 层就行,页面和控制器不用动。

3. 数据库设计与核心表:座位、场次、订单怎么建才不打架

3.1 三张核心表的结构与关系

影院系统的数据库设计,核心就是让「一个场次有多少座位、哪些被卖了」这件事查得快、改得准。我一般建这几张表:Hall(影厅)、Movie(影片)、Schedule(场次)、Seat(座位)、Order(订单)、OrderDetail(订单明细)。其中Seat表是关键,它记录每个场次每个座位的状态。

-- 影厅表 CREATE TABLE Hall ( HallID INT PRIMARY KEY IDENTITY(1,1), HallName NVARCHAR(50) NOT NULL, RowCount INT NOT NULL, -- 排数 ColCount INT NOT NULL -- 每排座位数 ); -- 场次表 CREATE TABLE Schedule ( ScheduleID INT PRIMARY KEY IDENTITY(1,1), MovieID INT NOT NULL, HallID INT NOT NULL, StartTime DATETIME NOT NULL, Price DECIMAL(10,2) NOT NULL, FOREIGN KEY (HallID) REFERENCES Hall(HallID) ); -- 座位状态表,每个场次每个座位一行 CREATE TABLE Seat ( SeatID INT PRIMARY KEY IDENTITY(1,1), ScheduleID INT NOT NULL, RowNo INT NOT NULL, ColNo INT NOT NULL, Status INT DEFAULT 0, -- 0可售 1已售 2锁定 OrderID INT NULL, FOREIGN KEY (ScheduleID) REFERENCES Schedule(ScheduleID) );

Seat表用Status字段标记座位状态,0 是可售,1 是已售,2 是下单未支付的锁定状态。OrderID在售出后回填,方便反查。这里有个设计取舍:座位数据是排片时批量生成的,一个 10 排 15 列的厅,一个场次就插 150 行。场次多了数据量会涨,但 SqlServer 处理百万级数据毫无压力,不用提前优化。

3.2 选座并发问题的 SQL 解法

两个人同时点同一个座位,是影院系统最典型的并发场景。解法不是加锁,而是用带条件的 UPDATE,让数据库自己保证原子性。

-- 尝试锁定座位,只有当前状态是 0 才会更新成功 UPDATE Seat SET Status = 2, OrderID = @OrderID WHERE SeatID = @SeatID AND Status = 0; -- 检查影响行数,如果返回 0 说明座位已被抢 -- 在 Java 中用 executeUpdate 的返回值判断

这条 SQL 的逻辑是:只有当座位当前状态是 0(可售)时才更新为 2(锁定),并且带上订单号。如果另一个人已经先改了,Status不再是 0,这条 UPDATE 影响行数为 0,Java 里判断返回值就知道抢座失败,直接提示用户换座。不需要显式开事务锁表,SqlServer 的行级锁会处理。参数@OrderID和@SeatID从 Java 端传入,注意用 PreparedStatement 防注入。

3.3 排片与座位批量生成的存储过程

每次新增场次都要生成对应数量的座位记录,用存储过程一次搞定,比在 Java 里循环插入快得多。

CREATE PROCEDURE GenerateSeats @ScheduleID INT, @RowCount INT, @ColCount INT AS BEGIN DECLARE @r INT = 1, @c INT; WHILE @r <= @RowCount BEGIN SET @c = 1; WHILE @c <= @ColCount BEGIN INSERT INTO Seat (ScheduleID, RowNo, ColNo, Status) VALUES (@ScheduleID, @r, @c, 0); SET @c = @c + 1; END SET @r = @r + 1; END END

调用时传场次 ID 和厅的行列数,比如EXEC GenerateSeats @ScheduleID=5, @RowCount=10, @ColCount=15。这个存储过程用双层循环插入,150 行瞬间完成。注意@RowCount和@ColCount要和Hall表里对应厅的配置一致,否则座位图和实际不符。排片时先插Schedule拿到自增 ID,再调这个存储过程。

4. 页面实现与交互:选座图、订单流程、后台排片怎么写

4.1 用 JSP 渲染座位图的完整代码

座位图是影院系统的门面,用户点进来第一眼就看这个。实现方式是用 JSP 从数据库查出该场次所有座位,按行列渲染成表格或 div 网格。

<%@ page import="java.sql.*, com.cinema.util.DBUtil" %> <% int scheduleId = Integer.parseInt(request.getParameter("sid")); Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement( "SELECT RowNo, ColNo, Status FROM Seat WHERE ScheduleID=? ORDER BY RowNo, ColNo"); ps.setInt(1, scheduleId); ResultSet rs = ps.executeQuery(); %> <table class="seat-map"> <% int currentRow = 0; while (rs.next()) { int row = rs.getInt("RowNo"); if (row != currentRow) { if (currentRow != 0) out.print("</tr>"); out.print("<tr><td>" + row + "排</td>"); currentRow = row; } int status = rs.getInt("Status"); String cls = status == 0 ? "free" : (status == 1 ? "sold" : "locked"); out.print("<td class='" + cls + "'>protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int scheduleId = Integer.parseInt(req.getParameter("sid")); String[] seats = req.getParameterValues("seat"); // 选中的座位ID数组 Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 先生成订单主记录 PreparedStatement orderPs = conn.prepareStatement( "INSERT INTO [Order] (UserID, ScheduleID, TotalAmount, CreateTime) VALUES (?,?,?,GETDATE())", Statement.RETURN_GENERATED_KEYS); orderPs.setInt(1, userId); orderPs.setInt(2, scheduleId); orderPs.setBigDecimal(3, totalAmount); orderPs.executeUpdate(); ResultSet keys = orderPs.getGeneratedKeys(); keys.next(); int orderId = keys.getInt(1); // 逐个锁定座位 PreparedStatement seatPs = conn.prepareStatement( "UPDATE Seat SET Status=2, OrderID=? WHERE SeatID=? AND Status=0"); for (String seatId : seats) { seatPs.setInt(1, orderId); seatPs.setInt(2, Integer.parseInt(seatId)); if (seatPs.executeUpdate() == 0) { conn.rollback(); resp.getWriter().write("座位已被抢,请重选"); return; } } conn.commit(); resp.sendRedirect("pay.jsp?oid=" + orderId); } catch (Exception e) { if (conn != null) conn.rollback(); e.printStackTrace(); } finally { if (conn != null) try { conn.setAutoCommit(true); conn.close(); } catch (Exception ignored) {} } }

逻辑说明:先关掉自动提交开启事务,插入订单主记录拿到自增订单号,然后循环锁定每个座位。只要有一个座位executeUpdate返回 0,说明被抢,整个事务回滚,之前锁的座位全部释放。全部成功才提交。参数userId从 session 取,totalAmount由座位数和场次单价算出。这个流程保证了要么全成功要么全失败,不会出现订单生成了但座位没锁住的脏数据。

4.3 后台排片页面的关键实现

后台排片是管理员用的,核心是选影片、选厅、定时间和价格,提交后生成场次和座位。页面用 JSP 表单,提交到 Servlet 处理。

<form action="AddScheduleServlet" method="post"> <select name="movieId"> <%-- 从 Movie 表查出影片列表循环输出 option --%> </select> <select name="hallId"> <%-- 从 Hall 表查出影厅列表 --%> </select> <input type="datetime-local" name="startTime" required> <input type="number" name="price" step="0.01" required> <button type="submit">生成场次</button> </form>

Servlet 里先插Schedule表,拿到自增 ID 后调用前面写的GenerateSeats存储过程批量生成座位。注意datetime-local提交的格式是2024-06-01T14:30,SqlServer 的 DATETIME 能直接识别,但如果你用字符串拼接 SQL 要转一下格式,用 PreparedStatement 就没这个问题。价格字段用step="0.01"允许两位小数,和数据库的DECIMAL(10,2)对应。

5. 避坑与排查:JSP + SqlServer 影院系统最常见的 5 个翻车点

5.1 中文乱码:页面显示问号或方块

现象:JSP 页面提交的中文影片名存进数据库变成问号,或者页面显示乱码。原因通常是三处编码不一致:JSP 页面、请求体、数据库排序规则。解决:JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" %>,Servlet 里在取参数前设request.setCharacterEncoding("UTF-8"),数据库字段用NVARCHAR而不是VARCHAR,连接 url 里加;sendStringParametersAsUnicode=true。三处都对了才不会乱码。

5.2 座位图点击无反应:JavaScript 事件没绑上

现象:座位格子渲染出来了,但点击没反应,选不中。原因多半是 JSP 循环输出的td上没绑事件,或者绑了但>CREATE VIEW v_OrderDetail AS SELECT o.OrderID, o.CreateTime, m.MovieName, h.HallName, s.StartTime, o.TotalAmount, u.UserName FROM [Order] o JOIN Schedule s ON o.ScheduleID = s.ScheduleID JOIN Movie m ON s.MovieID = m.MovieID JOIN Hall h ON s.HallID = h.HallID JOIN [User] u ON o.UserID = u.UserID;

视图建好后,后台订单列表直接SELECT * FROM v_OrderDetail ORDER BY CreateTime DESC。但数据量大了不能一次全查出来,要分页。SqlServer 2012 及以上用OFFSET ... FETCH最方便:

-- 查第 3 页,每页 20 条 SELECT * FROM v_OrderDetail ORDER BY CreateTime DESC OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;

这里有个容易搞混的点:OFFSET 40 ROWS是跳过前 40 条,FETCH NEXT 20 ROWS ONLY是取接下来的 20 条,合起来就是第 3 页(每页 20 条)。注意OFFSET必须配合ORDER BY用,否则报错。页码从前端传,Java 里算(pageNum - 1) * pageSize作为 OFFSET 值。用视图加分页之后,订单查询从原来的秒级降到毫秒级,后台体验完全不一样。

验证方法很简单:在 SqlServer 图形化工具里先跑视图查询看结果对不对,再套上 OFFSET 看分页数据是否连续、有没有重复或漏掉。我一般会拿总条数除以每页条数算一下总页数,和前端分页控件对一下,对不上就是 OFFSET 算错了。这套东西不复杂,但每次做影院系统我都会先把视图和分页建好,后面省很多事。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询