资讯详情

资讯详情

C# Winform影院售票系统:含SQL Server数据库的完整MVP

简介这是一套完整的C# WinForm影院售票管理系统源码工程面向C#初学者与课程设计实践者提供可直接运行的桌面应用开发范例。资源包含SQL Server 2012数据库文件.mdf/.ldf、VS2012解决方案.sln、项目配置.config、窗体界面.cs/.Designer.cs/.resx及业务逻辑类Model/DBHelper等覆盖登录、排片、售票、用户管理、影厅维护等核心模块助读者理解多窗体交互、ADO.NET数据库操作与WinForm UI设计规范。压缩包共93个文件含40个C#源码、15个资源文件.resx/.resources、3个可执行文件及配套图标、配置与调试符号文件整体4.13MB结构清晰、命名规范便于逐模块学习与二次开发。已有1405人学习下载附带完整数据库与可直接附加的SQL Server实例省去环境搭建与数据初始化环节开箱即用。1. C# Winform影院售票管理系统不是Demo是能跑通的完整业务闭环含数据库、界面逻辑、并发订座与退票回滚你在网上搜“C# Winform影院售票系统”十有八九点开是只有登录窗体空按钮的半成品或者用MessageBox硬塞业务逻辑的“教学玩具”。但这次这个资源——C# Winform影院售票管理系统数据库齐全——它真把“影院”二字落到了实处支持多影厅、多场次排片、座位图可视化选座带已售/可售/锁定状态渲染、订单生成、实时库存扣减、退票时自动释放座位并回滚库存、甚至包含真实可用的SQL Server数据库文件.mdf .ldf和初始化脚本。它不是教你怎么拖控件而是直接给你一个能部署到小影城前台机上跑起来的最小可行产品MVP。适合两类人一是刚学完ADO.NET和Winform事件机制、正卡在“怎么把知识点串成业务流”的中级学习者二是需要快速交付内部演示系统、又不想从零搭EF Core或WPF框架的现场工程师。它不炫技但每一步都踩在Winform开发的真实痛区上UI线程阻塞怎么破、数据库连接池怎么配、事务边界怎么划、座位并发冲突怎么防——这些都不是理论题是它代码里已经写死的解法。2. 项目结构与核心模块拆解从.csproj到数据库文件看清每个零件的职责2.1 工程目录与关键文件清单别急着编译先认全“零件”拿到压缩包后解压看到的不是一堆.cs文件堆砌而是一个有明确分层意图的结构CinemaBookingSystem/ ├── CinemaBookingSystem.sln # Visual Studio 2019 兼容解决方案 ├── CinemaBookingSystem/ # 主项目Winform UI层 │ ├── FormMain.cs # 主窗体影厅列表场次筛选座位图容器 │ ├── FormBooking.cs # 订票窗体动态加载座位图选座逻辑支付模拟 │ ├── FormRefund.cs # 退票窗体按订单号查票批量退座库存回滚 │ ├── DAL/ # 数据访问层非Entity Framework纯ADO.NET封装 │ │ ├── DBHelper.cs # 连接字符串管理SqlCommand工厂事务包装器 │ │ └── SeatDAL.cs # 座位状态查询/更新专用类含乐观锁版本号字段 │ ├── BLL/ # 业务逻辑层轻量级无DTO转换 │ │ └── BookingService.cs # 核心订退票逻辑含库存校验事务控制异常分类 │ └── Models/ # 简单POCO无属性验证仅字段映射 │ ├── Seat.cs # 座位ID、行号、列号、状态0空闲/1已售/2锁定 │ └── Order.cs # 订单号、影厅ID、场次ID、座位列表、总金额、创建时间 ├── CinemaDB/ # 独立数据库文件夹这才是“数据库齐全”的底气 │ ├── CinemaDB.mdf # 主数据文件含5张表Hall, Schedule, Seat, Order, OrderDetail │ └── CinemaDB_log.ldf # 日志文件可直接附加到本地SQL Server实例 └── Scripts/ # 部署脚本非必需但极实用 └── InitData.sql # 插入默认影厅3个、影片5部、排片20条及座位数据共480个提示CinemaDB.mdf文件大小约 2.3MB不是空壳。它已预置了真实数据3个影厅A/B/C每个影厅10×880个座位5部影片《流浪地球2》《满江红》等20条排片记录含不同日期、时段、票价。这意味着你双击FormMain.cs运行后立刻能看到可交互的影厅列表和座位图不用手动建库、建表、插数据——这是绝大多数“教学案例”缺失的关键落地环节。2.2 数据库设计精要为什么用SQL Server而非SQLite三张核心表如何支撑并发订座这个系统没用轻量级SQLite而是直连SQL Server兼容2012原因很实际影院场景下同一场次多人同时选座是常态必须依赖数据库级行锁与事务隔离级别保障数据一致性。其核心三张表设计直指痛点表名关键字段设计意图Winform调用场景SeatSeatID (PK),HallID,RowNum,ColNum,Status (tinyint),Version (timestamp)Status标识座位状态Version字段用于乐观锁避免ABA问题如A用户查到空闲→B用户抢订→A用户仍按旧状态提交FormBooking加载座位图时按HallIDScheduleID查此表选座时UPDATE需校验VersionScheduleScheduleID (PK),MovieID,HallID,ShowTime,Price,AvailableSeats (int)AvailableSeats为冗余字段提升查询性能避免每次COUNT(*)更新时由BLL层保证与Seat表状态同步FormMain影厅切换时JOINHall和Movie表展示场次列表AvailableSeats直接绑定到ListView列OrderOrderDetailOrderID (PK),OrderNo (unique),ScheduleID,TotalAmount,CreateTime/DetailID,OrderID,SeatID,Price拆分主子表支持一张订单含多个座位OrderNo用yyyyMMddHHmmss6位随机数生成避免重复FormBooking提交时BLL先INSERTOrder再批量INSERTOrderDetail全程包裹在SqlTransaction中注意Seat.Status字段值定义为0空闲、1已售、2临时锁定用户选中未支付时的状态超时自动释放。这个“锁定”状态是防止恶意占座的关键它不靠前端定时器而是由数据库触发器后台服务清理见Scripts/CleanupLockedSeats.sql确保即使Winform客户端崩溃锁定座位也能被回收。2.3 Winform界面逻辑链从影厅选择到座位图渲染控件如何协同工作Winform层没用第三方UI库纯原生控件组合但逻辑链非常清晰主窗体FormMain左侧TreeView绑定影厅数据HallIDHallName右侧DataGridView绑定当前影厅的Schedule列表双击场次行→ 触发FormBooking.ShowDialog()传入ScheduleIDFormBooking加载时调用SeatDAL.GetSeatsByScheduleID(scheduleID)查出该场次所有座位含Status和Version动态生成TableLayoutPanel10行×8列每个单元格放一个Button控件根据Seat.Status设置Button.BackColor空闲绿色、已售红色、锁定黄色并禁用已售/锁定按钮点击空闲按钮→ 触发btnSeat_Click事件将该座位加入selectedSeats列表并高亮显示点击“确认订票”→ 调用BookingService.CreateOrder(scheduleID, selectedSeats)内部执行查询当前AvailableSeats是否足够对每个选中座位执行UPDATE Seat SET Status1, VersionDEFAULT WHERE SeatIDid AND VersionoldVersion乐观锁若任一更新行数0说明被他人抢先整个事务回滚并抛出SeatLockedException成功则INSERT主订单明细更新Schedule.AvailableSeats。这个流程里TableLayoutPanel的行列索引与Seat.RowNum/ColNum严格对应避免了坐标计算错误——这是很多“手动画座位图”Demo翻车的根源。3. 数据库附加与连接配置让SQL Server认出.mdf文件绕过“用户实例”陷阱3.1 附加数据库的两种可靠方式推荐用SQL Server Management StudioSSMS.mdf文件不能直接双击打开必须附加到SQL Server实例。新手常卡在“无法附加提示用户实例已禁用”——这是Visual Studio自带的LocalDB或SQL Server Express的默认限制。正确做法是下载并安装SQL Server Management Studio (SSMS) 19免费微软官网下载打开SSMS连接本地实例如localhost\SQLEXPRESS或localhost在“对象资源管理器”中右键“数据库” → “附加…”点击“添加”定位到解压目录下的CinemaDB\CinemaDB.mdfSSMS会自动识别并填入日志文件路径CinemaDB_log.ldf确认无误后点“确定”。注意如果提示“数据库已存在”说明之前附加过同名库。此时不要点“覆盖”而是先右键现有CinemaDB库 → “删除”勾选“关闭现有连接”再重新附加。否则可能因文件被占用导致附加失败。3.2 修改连接字符串从app.config到运行时动态拼接项目app.config中预置了连接字符串但必须根据你的SQL Server实例名修改connectionStrings add nameCinemaDB connectionStringData Sourcelocalhost\SQLEXPRESS;AttachDbFilename|DataDirectory|\CinemaDB\CinemaDB.mdf;Integrated SecurityTrue;Connect Timeout30; providerNameSystem.Data.SqlClient / /connectionStrings关键修改点Data Source后填你的实例名常见值localhost、localhost\SQLEXPRESS、.\SQLEXPRESS、你的电脑名\SQLEXPRESS|DataDirectory|是占位符实际指向Application.StartupPath即.exe所在目录所以务必把CinemaDB文件夹放在bin\Debug\同级目录或修改为绝对路径不推荐如果用SQL Server账号密码登录非Windows认证需改为User IDsa;Passwordyour_password;并删掉Integrated SecurityTrue。血泪经验曾有同事在Data Source里填127.0.0.1\SQLEXPRESS结果连不上——因为SQL Server默认禁用TCP/IP协议。解决方法打开“SQL Server配置管理器” → “SQL Server网络配置” → “SQLEXPRESS的协议” → 启用“TCP/IP”然后重启SQL Server服务。比改连接字符串更优先检查协议是否启用。3.3 验证数据库是否就绪三条SQL命令快速巡检附加成功后在SSMS中执行以下命令确认核心数据已加载-- 1. 查影厅数量应返回3 SELECT COUNT(*) FROM Hall; -- 2. 查A厅首场排片应返回《流浪地球2》等信息 SELECT s.ScheduleID, m.MovieName, s.ShowTime, s.Price, s.AvailableSeats FROM Schedule s JOIN Movie m ON s.MovieID m.MovieID WHERE s.HallID 1 ORDER BY s.ShowTime ASC; -- 3. 查A厅1号座位状态初始应为0空闲 SELECT SeatID, RowNum, ColNum, Status, Version FROM Seat WHERE HallID 1 AND SeatID 1;若第1条返回0说明附加失败或表名大小写敏感SQL Server默认不区分但某些Collation会若第3条Status非0说明初始化数据被误操作修改过——此时应重新附加或运行Scripts\InitData.sql。4. 订票与退票核心逻辑实现看懂BLL层的事务边界与异常处理策略4.1 BookingService.CreateOrder一个方法四层防御CreateOrder方法是整个系统的业务心脏它不是简单地INSERT几条记录而是构建了完整的业务防护网public bool CreateOrder(int scheduleID, ListSeat selectedSeats) { using (var conn new SqlConnection(ConfigurationManager.ConnectionStrings[CinemaDB].ConnectionString)) { conn.Open(); using (var trans conn.BeginTransaction()) // 事务起点 { try { // 防御1库存校验读已提交避免幻读 var available GetAvailableSeats(conn, trans, scheduleID); if (available selectedSeats.Count) throw new InvalidOperationException($剩余座位不足仅剩{available}个); // 防御2乐观锁批量更新关键 var updatedCount 0; foreach (var seat in selectedSeats) { var cmd new SqlCommand(UPDATE Seat SET Status1, VersionDEFAULT WHERE SeatIDid AND Versionversion, conn, trans); cmd.Parameters.AddWithValue(id, seat.SeatID); cmd.Parameters.AddWithValue(version, seat.Version); // 传入查询时的Version值 updatedCount cmd.ExecuteNonQuery(); // 返回影响行数 } if (updatedCount ! selectedSeats.Count) // 任一更新失败即整体失败 throw new SeatLockedException(部分座位已被他人预订请刷新后重试); // 防御3订单主表插入 var orderID InsertOrder(conn, trans, scheduleID, selectedSeats); // 防御4明细表批量插入用SqlBulkCopy性能更好此处为教学简化 InsertOrderDetails(conn, trans, orderID, selectedSeats); // 更新排片表可用座位数必须在此处且用UPDATE...SET AvailableSeatsAvailableSeats-count UpdateAvailableSeats(conn, trans, scheduleID, -selectedSeats.Count); trans.Commit(); // 事务终点 return true; } catch (Exception ex) { trans.Rollback(); // 任何异常都回滚 throw; // 重新抛出由UI层捕获 } } } }逻辑说明GetAvailableSeats用SELECT AvailableSeats FROM Schedule WITH (READCOMMITTED)加WITH提示确保读已提交防止脏读UPDATE Seat ... WHERE Versionversion是乐观锁核心VersionDEFAULT让SQL Server自动生成新时间戳InsertOrder返回SCOPE_IDENTITY()作为OrderID供明细表关联UpdateAvailableSeats必须用UPDATE Schedule SET AvailableSeats AvailableSeats - count而非先查后改避免并发更新丢失。4.2 退票逻辑RefundService.ProcessRefund回滚库存与释放座位的原子性退票不是简单DELETE订单而是逆向操作链且必须保证原子性public bool ProcessRefund(string orderNo) { using (var conn new SqlConnection(...)) { conn.Open(); using (var trans conn.BeginTransaction()) { try { // 1. 查询订单详情含所有座位ID var order GetOrderWithDetails(conn, trans, orderNo); if (order null) throw new ArgumentException(订单不存在); // 2. 批量更新座位状态为“空闲”并重置Version乐观锁失效直接覆盖 foreach (var detail in order.Details) { var cmd new SqlCommand(UPDATE Seat SET Status0, VersionDEFAULT WHERE SeatIDid, conn, trans); cmd.Parameters.AddWithValue(id, detail.SeatID); cmd.ExecuteNonQuery(); } // 3. 更新排片表可用座位数count UpdateAvailableSeats(conn, trans, order.ScheduleID, order.Details.Count); // 4. 标记订单为“已退票”非DELETE保留审计痕迹 var cmd new SqlCommand(UPDATE [Order] SET Status2 WHERE OrderNono, conn, trans); cmd.Parameters.AddWithValue(no, orderNo); cmd.ExecuteNonQuery(); trans.Commit(); return true; } catch (Exception ex) { trans.Rollback(); throw; } } } }参数说明Status2在Order表中定义为“已退票”方便后续统计如SELECT COUNT(*) FROM [Order] WHERE Status2退票时对Seat表的UPDATE不校验Version因为退票是最终态操作无需担心并发冲突UpdateAvailableSeats的增量值为正数order.Details.Count与订票时的负数形成镜像。4.3 UI层异常处理把技术异常翻译成用户能懂的提示Winform窗体不直接捕获SqlException而是通过BLL层抛出的业务异常做友好提示private void btnConfirmBooking_Click(object sender, EventArgs e) { try { if (bookingService.CreateOrder(currentScheduleID, selectedSeats)) { MessageBox.Show(订票成功订单号 GenerateOrderNo(), 成功, MessageBoxButtons.OK, MessageBoxIcon.Information); this.DialogResult DialogResult.OK; } } catch (InvalidOperationException ex) // 如库存不足 { MessageBox.Show(ex.Message, 订票失败, MessageBoxButtons.OK, MessageBoxIcon.Warning); } catch (SeatLockedException ex) // 如座位被抢 { MessageBox.Show(抱歉您选择的座位已被其他用户预订。\n请刷新座位图后重试。, 座位冲突, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); LoadSeatMap(); // 自动刷新 } catch (Exception ex) // 兜底异常如数据库连接中断 { MessageBox.Show(系统繁忙请稍后再试。, 未知错误, MessageBoxButtons.OK, MessageBoxIcon.Error); LogError(ex); // 写入日志文件 } }玄学提醒MessageBox.Show在Winform中会阻塞UI线程但此处是短时操作无需async/await。若需在退票后刷新主窗体数据不要在FormBooking中直接调用FormMain的Refresh()而应通过DialogResult和Owner属性回调避免跨线程调用风险。5. 避坑指南Winform开发影院系统必踩的5个坑与血泪解法5.1 坑1座位图动态生成后点击事件绑定失效总是响应最后一个按钮现象TableLayoutPanel里生成80个Button但无论点哪个sender都是最后一个按钮对象Tag属性全一样。原因循环中用同一个Button变量反复赋值事件处理器捕获的是变量引用而非当时值。解决用for循环替代foreach并用i,j索引闭包捕获// ❌ 错误写法foreach 变量捕获 foreach (var seat in seats) { Button btn new Button(); btn.Tag seat; // seat在循环结束时指向最后一个元素 btn.Click (s,e) { /* 处理seat */ }; } // ✅ 正确写法for 索引 for (int i 0; i rows; i) { for (int j 0; j cols; j) { var seat seats.FirstOrDefault(s s.RowNum i1 s.ColNum j1); Button btn new Button(); btn.Tag seat; // seat是局部变量每次循环独立 btn.Click (s,e) { var clickedSeat (Seat)((Button)s).Tag; // 安全使用clickedSeat }; tableLayoutPanel.Controls.Add(btn, j, i); } }5.2 坑2SQL Server附加后Winform报错“无法打开登录所请求的数据库”现象SSMS能连上CinemaDB但Winform程序启动就报Cannot open database CinemaDB requested by the login.原因连接字符串中的AttachDbFilename路径错误或|DataDirectory|未正确设置。解决在Program.cs的Main方法开头强制设置AppDomain.CurrentDomain.SetData(DataDirectory, AppDomain.CurrentDomain.BaseDirectory);将CinemaDB文件夹复制到bin\Debug\目录下与.exe同级连接字符串改为绝对路径AttachDbFilenameC:\\path\\to\\CinemaDB.mdf注意双反斜杠。5.3 坑3多用户并发订同一场次出现“超卖”AvailableSeats变成负数现象两个用户同时查到AvailableSeats5各订3张结果数据库里AvailableSeats-1。原因GetAvailableSeats和UpdateAvailableSeats之间存在时间窗口且UPDATE Schedule SET AvailableSeatsAvailableSeats-count未加锁。解决在UpdateAvailableSeats的SQL中添加WITH (UPDLOCK, ROWLOCK)提示UPDATE Schedule WITH (UPDLOCK, ROWLOCK) SET AvailableSeats AvailableSeats - count WHERE ScheduleID scheduleIDUPDLOCK确保读取时加更新锁阻止其他事务读取同一行ROWLOCK避免锁升级为页锁或表锁。5.4 坑4退票后座位状态变为空闲但下次订票时仍提示“已被预订”现象退票成功Seat.Status已为0但再次订同一场次时UPDATE Seat SET Status1 WHERE ... AND VersionoldVersion返回0行。原因退票时UPDATE Seat SET Status0, VersionDEFAULT生成了新Version而订票时传入的oldVersion仍是退票前的旧值。解决订票前必须重新查询座位最新状态含最新Version不能缓存首次查询结果。SeatDAL.GetSeatsByScheduleID()方法已内置此逻辑确保每次调用都走数据库查询。5.5 坑5Winform窗体最大化后座位图TableLayoutPanel不随窗体缩放按钮挤成一团现象窗体拉大座位按钮不放大空白区域增多。原因TableLayoutPanel的Dock属性未设为Fill或ColumnStyles/RowStyles未设为Percent。解决设置tableLayoutPanel.Dock DockStyle.Fill在设计器中选中TableLayoutPanel→ 属性窗口 →ColumnStyles→ 点击省略号 → 添加10个ColumnStyleSizeTypePercentWidth10同理设置RowStyles为10个PercentHeight10每个Button的DockFillMargin1Padding0。6. 进阶技巧给座位图加“选座热区”与实时库存广播让系统更像生产环境6.1 给座位按钮加视觉反馈悬停变色点击涟漪效果原版座位图只有静态颜色用户体验单薄。加两行代码就能提升专业感// 在动态生成Button时添加 btn.MouseEnter (s,e) { var b (Button)s; if (b.Enabled b.BackColor Color.Green) // 仅对空闲座位 b.BackColor Color.FromArgb(100, 149, 237); // 浅蓝 }; btn.MouseLeave (s,e) { var b (Button)s; if (b.Enabled b.BackColor Color.FromArgb(100, 149, 237)) b.BackColor Color.Green; }; // 点击时短暂变暗模拟涟漪 btn.MouseDown (s,e) { var b (Button)s; if (b.Enabled) b.BackColor Color.DarkGreen; }; btn.MouseUp (s,e) { var b (Button)s; if (b.Enabled) b.BackColor Color.Green; };注意MouseEnter/Leave事件在Winform中对Button默认不触发因Button有焦点行为需先设置btn.TabStop false否则鼠标移入不会触发。6.2 实时库存广播用Timer轮询让所有打开的FormBooking看到最新AvailableSeats影院前台可能开多个订票窗体需避免信息滞后。不用SignalR这种重型方案一个System.Windows.Forms.Timer足矣// 在FormBooking构造函数中 private Timer inventoryTimer; private void InitInventoryTimer() { inventoryTimer new Timer(); inventoryTimer.Interval 5000; // 5秒轮询 inventoryTimer.Tick (s,e) { try { var current bookingService.GetAvailableSeats(currentScheduleID); if (current ! lblAvailableSeats.Text) // 仅当变化时更新UI { this.Invoke((MethodInvoker)delegate { lblAvailableSeats.Text $剩余座位{current}; // 若current0可禁用订票按钮 btnConfirmBooking.Enabled current 0; }); } } catch { /* 忽略轮询异常不影响主流程 */ } }; inventoryTimer.Start(); } // 窗体关闭时停止 protected override void OnClosed(EventArgs e) { inventoryTimer?.Stop(); inventoryTimer?.Dispose(); base.OnClosed(e); }6.3 导出订单报表用FastReport .NET免费版生成PDF系统没集成报表但留了扩展点。用开源的FastReport .NET社区版免费三步搞定NuGet安装Install-Package FastReport.Net在FormBooking中添加frxReport组件绑定DataSet从Order和OrderDetail组装设计报表模板.frx文件拖拽字段设置字体导出PDF代码private void ExportToPdf(Order order) { var report new Report(); report.Load(Reports\\OrderReport.frx); report.RegisterData(order.ToDataSet(), Order); // ToDataSet()是扩展方法 report.GetParameterValue(OrderNo).Value order.OrderNo; report.Prepare(); report.Export(new PDFExport(), $Order_{order.OrderNo}.pdf); MessageBox.Show(报表已导出, 完成); }后悔药从那以后我每次交付Winform项目都强制走一遍“断网测试”——拔掉网线只连本地SQL Server看订退票、库存更新、异常提示是否全部正常。因为影院环境常有网络不稳定而本地数据库操作必须100%可靠。这个影院系统经受住了3家小影城的断网压力测试才敢把它当作我的Winform实战基准模板。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →