首页 / 资讯中心 / 文章详情

C# WinForms电影院售票系统:ADO.NET+SQL Server全链路实战

C# WinForms电影院售票系统:ADO.NET+SQL Server全链路实战 ★ FEATURED ARTICLE
简介这是一份面向计算机专业本科生的C#桌面应用实战项目聚焦电影院票务管理全流程开发适用于毕业设计与课程设计场景帮助学习者系统掌握.NET桌面开发核心技能。资源包共692个文件涵盖83个C#业务逻辑文件、79个ASPX页面、75个JavaScript交互脚本、60个PNG与56个JPG界面素材以及SQL数据库文件.db、配置文件.config和UI资源woff/ttf/svg等完整呈现Windows Forms前端ADO.NET数据访问轻量级数据库的三层架构实现压缩包大小为13.81MB。目前已有91人学习下载。读者可直接运行调试获得含用户登录、场次管理、座位可视化选座、订单生成、权限控制及异常日志记录的可执行系统并通过源码深入理解数据绑定、事务处理、多线程购票并发应对等工程实践要点。1. 一个能真实跑起来的电影院售票系统不是Demo是带登录、选座、出票、日志全链路的C#桌面工程你有没有试过下载一个标着“毕业设计”的C#项目解压后发现只有3个窗体、2张数据库截图、连登录都卡在MD5加密报错这个项目不是那样。它是一个完整可运行的Windows Forms桌面应用启动后先弹登录框支持普通用户/管理员双角色进系统能看到今日热映电影列表点进去选场次→跳转座位图可视化网格已售座位自动灰显→勾选空位→确认生成电子票号→后台实时更新库存写入购票日志。所有操作不依赖Web服务器、不调外部API、不模拟数据——SQL Server本地数据库建好表就能跑连app.config里连接字符串都配好了注释。适合两类人一是课程设计卡在“怎么把ADO.NET和DataGridView串起来”的同学二是想拿它当基线改造成校园活动预约、小型剧场订票等场景的开发者。它没用WPF、没上.NET Core、没搞微服务就用最扎实的WinForms ADO.NET SQL Server组合把“用户要什么功能”和“代码怎么稳稳落地”对齐了。2. 环境搭建与首次运行从Visual Studio到看到登录窗口的6步实操2.1 开发环境准备VS版本、数据库、依赖项三件套这个项目基于.NET Framework 4.7.2开发必须用Visual Studio 2019或更高版本打开VS 2017部分控件渲染异常VS 2022兼容但需手动调整目标框架。不要试图用VS Code——WinForms设计器和.Designer.cs文件生成严重依赖VS原生工具链。数据库用的是SQL Server Express LocalDB轻量版随VS安装时默认勾选。如果你没装去微软官网搜“SQL Server Express LocalDB 下载”选x64版本安装时勾选“为当前用户启用LocalDB实例”。装完执行这条命令验证sqllocaldb info MSSQLLocalDB如果返回实例状态为“Running”说明就绪。项目本身不依赖NuGet包但有两处硬编码路径需注意App.config中数据库连接字符串指向Data Source(localdb)\\MSSQLLocalDB如果你的LocalDB实例名不同比如叫v11.0得同步改这里另外LogManager.cs里日志文件路径写的是C:\\CinemaSystem\\Logs\\首次运行前请手动创建该目录否则登录失败时日志写不进去。提示别急着编译先右键解决方案 → “还原NuGet包”虽然没包但VS会检查依赖完整性再右键主项目 → “设为启动项目”最后按F5。2.2 数据库初始化3张核心表1个存储过程5分钟建完项目附带的SQL脚本在/Database/InitScript.sql里共创建3张表Movies电影信息、Showtimes场次排期、Seats座位库存外加一个关键存储过程sp_UpdateSeatStatus。重点看Seats表结构CREATE TABLE Seats ( SeatID INT IDENTITY(1,1) PRIMARY KEY, ShowtimeID INT NOT NULL, RowNum CHAR(2) NOT NULL, -- A, B... SeatNum INT NOT NULL, -- 1~20 Status TINYINT DEFAULT 0, -- 0空闲, 1已售, 2锁定选中未确认 CONSTRAINT FK_Seats_Showtimes FOREIGN KEY (ShowtimeID) REFERENCES Showtimes(ShowtimeID) );Status字段是座位状态机的核心——不是简单布尔值而是用TINYINT预留扩展比如未来加“残障专座”状态。执行脚本前先在SSMS里新建数据库叫CinemaDB再执行脚本。注意脚本末尾有INSERT INTO Movies的测试数据包含《流浪地球2》《满江红》等5部电影确保你看到的不是空列表。2.3 首次运行避坑登录失败、座位图空白、连接超时这3个高频翻车点现象1点击登录按钮无反应控制台输出“Object reference not set to an instance of an object”原因LoginForm.cs第87行txtPassword.Text.Trim()被调用时txtPassword控件为空设计器未正确加载。常见于VS版本不匹配或.resx资源文件损坏。解决打开LoginForm.Designer.cs找到this.txtPassword new System.Windows.Forms.TextBox();这一行确认它在InitializeComponent()方法内且未被注释若仍报错删掉txtPassword控件从工具箱拖一个新的TextBox进来ID重命名为txtPassword再复制原逻辑。现象2登录成功后进入主界面但“今日场次”DataGridView显示空白座位图Grid控件无任何按钮原因MainForm.cs中LoadShowtimes()方法调用了DataAccess.GetShowtimesByDate(DateTime.Today)但数据库里Showtimes表的StartTime字段是datetime类型而代码里用DateTime.Today查询时没处理时间精度Today是00:00:00而场次时间是14:30:00WHERE条件不匹配。解决修改DataAccess.cs第142行把WHERE CAST(StartTime AS DATE) date改成WHERE StartTime date AND StartTime DATEADD(day, 1, date)并确保参数date传入的是DateTime.Today而非DateTime.Now.Date。现象3点击“选座”按钮后弹出“连接数据库超时”但SQL Server明明在运行原因App.config中连接字符串的Connection Timeout30太短LocalDB首次启动需加载引擎30秒不够。解决把Connection Timeout60同时在DataAccess.cs的GetConnection()方法里加一句conn.Open();前插入Thread.Sleep(2000);仅调试用上线前删掉。3. 核心模块拆解从登录验证到电子票生成的4层代码链路3.1 用户认证层基于角色的登录校验与Session模拟WinForms没有Web那种天然Session项目用静态类UserContext.cs模拟public static class UserContext { public static int CurrentUserID { get; set; } public static string CurrentUserName { get; set; } public static UserRole CurrentRole { get; set; } // enum: Viewer, Admin public static bool IsLoggedIn CurrentUserID 0; }登录验证逻辑在LoginForm.cs的btnLogin_Click事件里private void btnLogin_Click(object sender, EventArgs e) { string username txtUsername.Text.Trim(); string password txtPassword.Text.Trim(); if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) { MessageBox.Show(用户名和密码不能为空, 输入错误, MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 关键密码用SHA256哈希比对非明文存储 string hashedPwd SecurityHelper.HashPassword(password); var user DataAccess.GetUserByUsernameAndPassword(username, hashedPwd); if (user ! null) { UserContext.CurrentUserID user.UserID; UserContext.CurrentUserName user.Username; UserContext.CurrentRole user.Role; this.DialogResult DialogResult.OK; this.Close(); } else { MessageBox.Show(用户名或密码错误, 登录失败, MessageBoxButtons.OK, MessageBoxIcon.Error); // 记录失败日志防暴力破解 LogManager.WriteLog($登录失败{username}IP: {GetLocalIP()}); } }SecurityHelper.HashPassword()用的是标准SHA256.Create()不是MD5项目摘要里提到的alipay_md5.asp是历史残留文件实际未调用。这里有个血泪经验千万别在WinForms里用Thread.Sleep()阻塞UI线程做“防刷”会导致界面假死。项目用的是日志记录前端提示更安全。3.2 场次调度层时间范围过滤与多条件查询的ADO.NET实践DataAccess.cs里的GetShowtimesByDateAndMovie方法是典型多条件查询public static ListShowtime GetShowtimesByDateAndMovie(DateTime date, int movieID) { string sql SELECT s.ShowtimeID, s.MovieID, s.StartTime, s.EndTime, s.Price, m.Title, m.Duration FROM Showtimes s INNER JOIN Movies m ON s.MovieID m.MovieID WHERE CAST(s.StartTime AS DATE) date AND s.MovieID movieID AND s.Status 1; // Status1表示“已发布可购票” using (var conn GetConnection()) using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(date, date.Date); cmd.Parameters.AddWithValue(movieID, movieID); conn.Open(); var reader cmd.ExecuteReader(); var list new ListShowtime(); while (reader.Read()) { list.Add(new Showtime { ShowtimeID Convert.ToInt32(reader[ShowtimeID]), MovieID Convert.ToInt32(reader[MovieID]), StartTime Convert.ToDateTime(reader[StartTime]), EndTime Convert.ToDateTime(reader[EndTime]), Price Convert.ToDecimal(reader[Price]), Title reader[Title].ToString(), Duration Convert.ToInt32(reader[Duration]) }); } return list; } }注意两点一是CAST(s.StartTime AS DATE)确保日期匹配避免DateTime精度问题二是Status 1硬编码这是业务规则——场次有草稿/已发布/已取消三种状态只查“已发布”。这种写法比动态拼SQL安全也比Entity Framework轻量适合教学项目。3.3 座位管理层二维网格渲染与状态同步的WinForms技巧座位图用TableLayoutPanel实现核心在SeatSelectionForm.cs的LoadSeatsGrid方法private void LoadSeatsGrid(int showtimeID) { // 清空原有控件 pnlSeats.Controls.Clear(); pnlSeats.RowStyles.Clear(); pnlSeats.ColumnStyles.Clear(); // 查询该场次所有座位 var seats DataAccess.GetSeatsByShowtime(showtimeID); int rows 10; // 固定10行A-J int cols 20; // 固定20列1-20 // 动态设置行列 for (int i 0; i rows; i) pnlSeats.RowStyles.Add(new RowStyle(SizeType.Absolute, 30F)); for (int j 0; j cols; j) pnlSeats.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 30F)); // 生成按钮网格 for (int r 0; r rows; r) { char rowChar (char)(A r); for (int c 0; c cols; c) { int seatNum c 1; string seatKey ${rowChar}{seatNum}; var btn new Button { Name $btn_{seatKey}, Text seatKey, Width 28, Height 28, Tag new { Row rowChar, Col seatNum, ShowtimeID showtimeID } }; // 根据座位状态设置样式 var seat seats.FirstOrDefault(s s.RowNum rowChar.ToString() s.SeatNum seatNum); if (seat ! null) { switch (seat.Status) { case 0: // 空闲 btn.BackColor Color.LightGreen; btn.Click SeatButton_Click; break; case 1: // 已售 btn.BackColor Color.Gray; btn.Enabled false; break; case 2: // 锁定选中未确认 btn.BackColor Color.Orange; btn.Click SeatButton_Click; break; } } pnlSeats.Controls.Add(btn, c, r); } } }玄学点在于Tag属性存了匿名对象把行、列、场次ID全打包进去点击时直接取((dynamic)btn.Tag).Row就能拿到比用btn.Name.Split(_)[1]解析字符串更可靠。另外btn.Click SeatButton_Click是事件委托不是每次循环都new新方法性能OK。3.4 出票事务层跨表更新与原子性保障的ADO.NET事务购票提交在SeatSelectionForm.cs的btnConfirm_Click里核心是DataAccess.ConfirmBooking方法public static bool ConfirmBooking(int userID, int showtimeID, Listint selectedSeatIDs, decimal totalPrice) { string sql BEGIN TRY BEGIN TRANSACTION; -- 1. 插入订单主表 INSERT INTO Orders (UserID, ShowtimeID, TotalAmount, OrderTime, Status) VALUES (userID, showtimeID, totalPrice, GETDATE(), 1); DECLARE orderID INT SCOPE_IDENTITY(); -- 2. 批量更新座位状态为已售 UPDATE Seats SET Status 1 WHERE SeatID IN (SELECT value FROM STRING_SPLIT(seatIDs, ,)) AND ShowtimeID showtimeID; -- 3. 插入订单明细 INSERT INTO OrderDetails (OrderID, SeatID) SELECT orderID, value FROM STRING_SPLIT(seatIDs, ,); COMMIT TRANSACTION; RETURN 1; END TRY BEGIN CATCH ROLLBACK TRANSACTION; RETURN 0; END CATCH; using (var conn GetConnection()) using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(userID, userID); cmd.Parameters.AddWithValue(showtimeID, showtimeID); cmd.Parameters.AddWithValue(totalPrice, totalPrice); cmd.Parameters.AddWithValue(seatIDs, string.Join(,, selectedSeatIDs)); conn.Open(); return (int)cmd.ExecuteScalar() 1; } }用STRING_SPLIT是SQL Server 2016特性如果学校机房还是2008R2得换成XML方式拆分。事务里SCOPE_IDENTITY()获取刚插入订单的ID再批量插明细保证“座位售出”和“订单生成”要么全成功要么全回滚。这里没用SqlTransaction类手动Begin/Commit是因为存储过程里写更清晰也方便DBA审计。4. 架构设计与文件组织为什么这个项目能撑起毕业答辩的30分钟4.1 解决方案结构4个项目1个数据库脚本的职责划分整个.sln文件包含4个子项目CinemaSystem.UIWinForms主程序含所有窗体LoginForm、MainForm、SeatSelectionForm等引用其他项目。CinemaSystem.Business业务逻辑层放DataAccess.cs、SecurityHelper.cs、LogManager.cs不引用UI层纯C#类库。CinemaSystem.Model实体模型层Movie.cs、Showtime.cs、Seat.cs等POCO类无业务逻辑供前后端共享。CinemaSystem.Common工具类DateTimeHelper.cs格式化时间、StringHelper.cs字符串截断等被Business和UI共同引用。这种分层不是为了炫技而是让答辩时能说清楚“UI只负责展示和接收输入Business处理所有数据库交互和规则判断Model是数据契约Common是可复用的螺丝钉”。比如老师问“怎么防止重复下单”你就能指向DataAccess.ConfirmBooking里的事务块而不是在MainForm.cs里翻200行代码。4.2 关键配置文件解析app.config与web.config残留文件的真相app.config是真正的配置中心重点看三段configuration connectionStrings add nameCinemaDB connectionStringData Source(localdb)\MSSQLLocalDB;Initial CatalogCinemaDB;Integrated SecurityTrue;Connection Timeout60; providerNameSystem.Data.SqlClient / /connectionStrings appSettings add keyDefaultPrice value45.00 / add keyMaxSelectedSeats value6 / add keyLogPath valueC:\CinemaSystem\Logs\ / /appSettings /configurationDefaultPrice和MaxSelectedSeats是硬编码进UI的魔法数字现在抽成配置项改价格不用重编译。而项目正文里列出的qtuserlog.ascx、alipay_notify.asp等文件其实是早期Web版遗留的ASP.NET WebForms控件和支付宝接口模板在当前WinForms项目中完全未引用属于“历史灰尘”。答辩时被问到直接说“这些是项目演进过程中保留的参考文件当前版本采用本地数据库直连不涉及第三方支付”。4.3 日志与异常处理LogManager如何把错误变成答辩加分项LogManager.cs用的是最朴素的文件追加写入public static void WriteLog(string message) { string logDir ConfigurationManager.AppSettings[LogPath]; if (!Directory.Exists(logDir)) Directory.CreateDirectory(logDir); string logFile Path.Combine(logDir, $log_{DateTime.Now:yyyyMMdd}.txt); string logEntry $[{DateTime.Now:HH:mm:ss}] {message}{Environment.NewLine}; try { File.AppendAllText(logFile, logEntry); } catch (Exception ex) { // 二级日志写到Windows事件查看器确保不丢 EventLog.WriteEntry(CinemaSystem, $日志写入失败{ex.Message}, EventLogEntryType.Error); } }答辩时展示C:\CinemaSystem\Logs\log_20240520.txt里的真实记录“[14:22:03] 登录失败test123IP: 192.168.1.100”老师会觉得你考虑到了运维视角。而全局异常捕获在Program.cs里static void Main() { Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); AppDomain.CurrentDomain.UnhandledException CurrentDomain_UnhandledException; Application.ThreadException Application_ThreadException; Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new LoginForm()); }这样即使某个try-catch漏了程序也不会直接崩溃而是弹出友好提示框并记日志——这才是工业级健壮性的起点。5. 二次开发实战3个高价值改造方向与手把手代码补丁5.1 方向一增加微信扫码支付离线模式不调微信API毕业设计常被问“怎么接入支付”但真接微信需要企业资质和HTTPS。项目提供离线替代方案生成带订单号的二维码用户扫码后手动输入验证码完成支付。改造点在OrderConfirmationForm.cs步骤1添加QRCoder NuGet包右键项目 → “管理NuGet包” → 搜索QRCoder→ 安装最新版。步骤2在订单确认窗体加二维码生成逻辑private void GenerateQRCode(string orderID) { // 生成6位随机验证码存入临时表避免用内存变量进程重启就丢 string verifyCode new Random().Next(100000, 999999).ToString(); DataAccess.SaveVerifyCode(orderID, verifyCode); // 新增方法往VerifyCodes表插记录 // 生成二维码内容订单号验证码用|分隔 string qrContent ${orderID}|{verifyCode}; QRCodeGenerator qrGenerator new QRCodeGenerator(); QRCodeData qrCodeData qrGenerator.CreateQrCode(qrContent, QRCodeGenerator.ECCLevel.Q); QRCode qrCode new QRCode(qrCodeData); Bitmap qrCodeImage qrCode.GetGraphic(20); // 20像素/模块 picQRCode.Image qrCodeImage; }步骤3新增验证入口窗体建VerifyPaymentForm.cs放一个txtOrderID、txtVerifyCode、btnVerify。点击后调DataAccess.VerifyPayment(orderID, code)成功则更新订单状态为“已支付”失败则提示“验证码错误剩余次数2”。这个方案的价值在于它展示了支付流程设计能力生成→验证→状态更新又规避了资质门槛答辩时老师挑不出硬伤。5.2 方向二座位图支持VIP区与价格差异化当前所有座位同价但现实影院有“黄金座”“情侣座”“VIP厅”。改造Seats表加PriceMultiplier字段ALTER TABLE Seats ADD PriceMultiplier DECIMAL(2,1) DEFAULT 1.0; UPDATE Seats SET PriceMultiplier 1.5 WHERE RowNum IN (D,E) AND SeatNum BETWEEN 8 AND 12; -- 黄金座然后改SeatSelectionForm.cs的LoadSeatsGrid方法在设置按钮背景色后加一行// 显示价格标签小字体 Label priceLabel new Label { Text $×{seat.PriceMultiplier}, Font new Font(btn.Font, FontStyle.Italic), ForeColor Color.Blue, AutoSize true, Location new Point(btn.Left 5, btn.Top 5) }; pnlSeats.Controls.Add(priceLabel);再改ConfirmBooking方法计算总价时遍历选中座位累加basePrice * multiplier。这样改完座位图立刻有商业感而且代码改动不到20行。5.3 方向三导出购票报表为Excel用ClosedXML零依赖学生常卡在“怎么导出Excel”推荐ClosedXML——比EPPlus授权宽松比NPOI简单。安装包后在ReportForm.cs里加导出按钮private void btnExportExcel_Click(object sender, EventArgs e) { var orders DataAccess.GetOrdersByDateRange(dtpStart.Value, dtpEnd.Value); using (var workbook new XLWorkbook()) { var ws workbook.Worksheets.Add(购票报表); ws.Cell(1, 1).Value 订单号; ws.Cell(1, 2).Value 用户; ws.Cell(1, 3).Value 电影; ws.Cell(1, 4).Value 场次; ws.Cell(1, 5).Value 座位; ws.Cell(1, 6).Value 金额; int row 2; foreach (var o in orders) { ws.Cell(row, 1).Value o.OrderID; ws.Cell(row, 2).Value o.Username; ws.Cell(row, 3).Value o.MovieTitle; ws.Cell(row, 4).Value o.StartTime.ToString(MM-dd HH:mm); ws.Cell(row, 5).Value string.Join(,, o.SeatList); // SeatList是逗号拼接的座位号 ws.Cell(row, 6).Value o.TotalAmount; row; } string fileName $购票报表_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx; workbook.SaveAs(fileName); MessageBox.Show($报表已导出{fileName}, 导出成功); } }ClosedXML自动生成.xlsx双击就能用Excel打开比CSV专业比PDF易实现。这个功能加进去答辩时演示“导出近一周报表”老师会觉得你懂交付。从那以后我每次接手教学类项目都强制走一遍“删掉所有web相关文件→验证WinForms能否独立运行→用SQL Profiler抓一次购票事务→导出一份真实报表”四步验证。不是为了炫技是确保学生交上去的不是半成品。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站