C#会议室预约系统源码解析:三层架构与冲突检测实战
简介这是一套基于C#与ASP.NET Web Forms开发的会议室预约管理系统完整源码适合C#学习者用于课程设计、毕业设计或企业内部系统二次开发参考。系统覆盖会议室信息维护、在线预约、预约取消、用户及管理员后台管理等典型业务模块代码结构清晰便于快速理解完整业务闭环。资源包共177个文件压缩后仅1.8MB包含28个cs后端逻辑文件、25个aspx页面文件、8个js与8个css前端交互文件、2个数据库文件db/mdb以及gif、jpg等界面素材与可直接打开的sln解决方案可结合Visual Studio加载运行。目前已有182人学习通过adminDel、hysDel、classDel、wyyy、qxyyDel等页面文件可对照学习会议室增删改查、冲突检测、表单权限校验等核心功能的实现思路数据库脚本与mdb文件也一并提供方便本地部署调试。整体来看这份源码不仅展示了C#面向对象设计、ADO.NET数据访问、前端Ajax交互的配合方式也为后续扩展功能或重构为MVC架构提供了不错的实践范本。1. 会议室预约系统先用一个真实问题把它说清楚上个月朋友公司行政还在用纸质表格登记会议室周五下午黄金时段被同一间房订了三次三个部门的会议撞到一起场面相当难看。我直接把这套C#会议室预约系统源码翻出来改配置、连库、跑通半天时间就把这套流程搬到了他们内网从那以后再也没有发生过“我明明订了为什么有人坐着”的内耗。这套源码不是什么云端 SaaS而是一个经典的 C# 桌面端/Winform 工程覆盖了会议室录入、预约申请、冲突检测、时段校验、记录管理、状态变更这些核心环节数据层用了 SQL Server也可以按配置换成 Access界面层是标准的 DataGridView 加 ComboBox 联动。适合三类人刚学完 C# 想做课设的在校生、需要内部工具但不想买商业软件的小团队、想学习经典三层架构和事务处理的进阶开发者。它是能直接跑起来、也能直接拆开的黑匣子接下来我把架构、数据表、核心逻辑、部署和踩坑全部过一遍。2. 拆包看结构三层架构、数据表与预约状态机2.1 目录长什么样先看负责人怎么搭骨架解压之后建议先用 Visual Studio 打开根目录下的.sln解决方案文件别急着点运行。先看解决方案里的项目结构这套源码的骨架如果按传统三层来对应大致是下面这个模样常见的目录划分如下MF00535-CSharpMeetingRoom/ |-- MeetingRoom.sln |-- MeetingRoom.UI // WinForm 界面层登录窗体、主窗体、预约窗体 |-- MeetingRoom.BLL // 业务逻辑层冲突检测、预约状态流转、合法性校验 |-- MeetingRoom.DAL // 数据访问层SQLHelper、参数化查询、数据表操作 |-- MeetingRoom.Model // 实体类MeetingRoomInfo、ReservationInfo、UserInfo |-- SQLScripts/ // 建库建表脚本和初始化数据 |-- app.config // 连接字符串、数据库类型配置这套结构最值得看的地方在于界面层没有直接拼接 SQL 字符串所有数据库操作都集中在 DAL 层界面拿到的数据是业务层返回的结果。常见做法是 UI 只负责显示和收集用户输入BLL 负责业务规则判断DAL 层做参数化查询这种分层的好处在于后期如果要把窗体换成 WPF 或 Web API业务规则和数据访问层可以原样复用。需要留意实际解压后文件夹命名可能和上面略有出入但换汤不换药凡是看到Forms或UI开头的放在界面层BLL是业务层DAL是数据层Model是实体模型。如果你打开后发现项目只有一个大文件夹所有.cs混在一起也不要意外那就说明原作者的工程没有严格分层你需要自己按命名空间把它们归位这一步对后续改冲突检测逻辑很重要。2.2 数据表设计一张预约记录如何覆盖占用和冲突预约系统的数据表看起来简单做起来容易翻车。核心是MeetingRoomInfo和ReservationInfo两张表前者保存会议室的基础信息后者保存谁在什么时候用了哪间房。下面是我比较推荐的基础结构这套源码里的数据表也基本是按这个思路做的CREATE TABLE MeetingRoomInfo ( RoomId INT IDENTITY(1,1) PRIMARY KEY, RoomName NVARCHAR(50) NOT NULL, Location NVARCHAR(100), Capacity INT NOT NULL DEFAULT 10, HasProjector BIT NOT NULL DEFAULT 0, Status BIT NOT NULL DEFAULT 1 -- 1可用 0禁用 ); CREATE TABLE ReservationInfo ( ReservationId INT IDENTITY(1,1) PRIMARY KEY, RoomId INT NOT NULL, UserId INT NOT NULL, BookDate DATE NOT NULL, StartTime TIME NOT NULL, EndTime TIME NOT NULL, Subject NVARCHAR(200), Status INT NOT NULL DEFAULT 0, -- 0待审核 1已通过 2已拒绝 3已结束 4已取消 CreateTime DATETIME NOT NULL DEFAULT GETDATE(), CONSTRAINT FK_Reservation_Room FOREIGN KEY (RoomId) REFERENCES MeetingRoomInfo(RoomId) );字段设置有几个地方決定系统好不好用。Status用数字而不用字符串业务层直接做整数判断效率更高而且方便以后扩展。时间段用TIME类型而不是DATETIME预约不需要关心日期时间戳的具体秒数日期单独用BookDate管理这样“2026年3月27日下午两点到四点”直接写成BookDate2026-03-27StartTime14:00EndTime16:00。关于“占用”和“冲突”的定义完全靠StartTime和EndTime的比较来算。两条预约在同一房间、同一天而且 A 的结束时间大于 B 的开始时间、A 的开始时间小于 B 的结束时间就会发生重叠。这个重叠条件设计成状态机之后“已拒绝”和“已取消”的记录在冲突检测时直接跳过而不是物理删除。保留记录的好处是后续可以查历史、做统计也方便管理员看到谁经常取消。2.3 预约状态机为什么不用 DELETE我看到很多初学者在上删除功能时直接把记录从数据库里抹掉会议室预约系统如果这样做会丢审计数据。这套源码里的状态流是0 待审核 - 1 已通过 - 3 已结束或者0 待审核 - 2 已拒绝 / 4 已取消。状态机的好处在于每一步都留有依据之后统计会议室使用率可以按Status3的已结束记录来算比无差别统计有效得多。如果你接手时需要加“会议室管理员手动释放房间”的功能不要去 DELETE加一个状态5 已释放就行。业务层判断逻辑只需要改IsRoomAvailable方法把Status NOT IN (4, 5)作为可用预约的条件。这样状态机和业务规则是解耦的不会引发数据不一致的问题。3. 核心流程实现预约冲突检测、时段校验与界面联动3.1 冲突检测的写法网上十个版本九个错预约系统最核心的代码就是冲突检测。网上很多版本的“冲突”写法只判断了包含关系比如 newStart 是否落在 oldStart 和 oldEnd 之间结果漏掉了跨边界的情况。比如已有一条09:00-11:00的预约新增一条10:30-12:00就是跨出边界新增一条08:00-09:30是跨入边界新增一条10:30-11:30是完全包含在内部新增一条08:00-12:00是完全覆盖原有区间。这四种情况都算冲突所以最稳妥的判断只有一条逻辑start oldEnd end oldStart。public bool IsTimeConflict(int roomId, DateTime bookDate, TimeSpan start, TimeSpan end) { if (start end) { throw new ArgumentException(结束时间必须大于开始时间); } string sql SELECT COUNT(1) FROM ReservationInfo WHERE RoomId RoomId AND BookDate BookDate AND Status IN (0, 1) AND StartTime EndTime AND EndTime StartTime; SqlParameter[] parameters new SqlParameter[] { new SqlParameter(RoomId, SqlDbType.Int) { Value roomId }, new SqlParameter(BookDate, SqlDbType.Date) { Value bookDate }, new SqlParameter(StartTime, SqlDbType.Time) { Value start }, new SqlParameter(EndTime, SqlDbType.Time) { Value end } }; int count SqlHelper.ExecuteScalar(sql, parameters); return count 0; }这段代码的含义很直白如果已存在一条预约它的开始时间早于“新预约的结束时间”而且它的结束时间晚于“新预约的开始时间”那么两个时间段一定有重叠。四条边界情况全部被这个公式覆盖不需要写分支判断。需要注意两点。第一Status IN (0, 1)让“待审核”和“已通过”一起参与冲突判断避免出现两个待审核预约互相不知道对方存在都通过了之后撞车的情况。第二判断时用的是EndTime和StartTime参数而不是拿数据库的字段和固定值比较这样 SQL 执行效率没有问题也避免了TimeSpan转换字符串带来的格式麻烦。有些读者会问如果预约时长超过一天怎么办比如周五晚六点到周六早九点。这套源码的BookDate是单日字段理论上是支持不了的。工作中我一般会加一个EndDate字段然后改成(StartTime EndTime OR BookDate EndDate)这种更复杂的条件但这套源码里没做我建议不要为了这个需求给数据表强行打补丁不如把“跨天预约”做成一个独立的功能模块。3.2 界面层联动DataGridView 按日期刷新会议室状态界面上最常见的需求是用户选一个日期左侧 DataGridView 显示当天的预约情况再选一个会议室右侧显示该会议室的空闲时段。实现起来就是一个 ComboBox 的SelectedIndexChanged事件里触发表格刷新。private void cmbRoom_SelectedIndexChanged(object sender, EventArgs e) { if (cmbRoom.SelectedValue null) return; int roomId (int)cmbRoom.SelectedValue; DateTime date dtpBookDate.Value.Date; DataTable dt GetReservationsByRoomAndDate(roomId, date); dgvReservations.DataSource dt; dgvReservations.Columns[ReservationId].Visible false; dgvReservations.Columns[RoomId].Visible false; dgvReservations.Columns[StartTime].HeaderText 开始时间; dgvReservations.Columns[EndTime].HeaderText 结束时间; dgvReservations.Columns[Status].HeaderText 状态; }这段代码的重点是GetReservationsByRoomAndDate要把Status翻译成用户能看懂的文字。在 SQL 里可以直接用CASE WHEN Status0 THEN 待审核 ...来生成一个别名列这样 DataGridView 绑定出来的列就是中文状态不需要在界面里再做二次循环翻译。另一个细节是调用SelectedValue之前先判断是否为 null否则页面初次加载时事件被触发会抛空引用这是 WinForm 里非常经典的翻车点。3.3 新增预约事务里只做该做的事插入预约记录本身很简单难点在于多人同时操作时不能出现“两个人都在同一个时段提交成功”的情况。SQL Server 下最简单有效的方式是在冲突检测和 INSERT 中间用事务把查询和插入包起来加上UPDLOCK, HOLDLOCK表级锁提示这样第二次进来的提交会等待第一个事务完成后再检查。public bool CreateReservation(ReservationInfo reservation) { using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); using (SqlTransaction tran conn.BeginTransaction()) { try { string checkSql SELECT COUNT(1) FROM ReservationInfo WITH (UPDLOCK, HOLDLOCK) WHERE RoomId RoomId AND BookDate BookDate AND Status IN (0, 1) AND StartTime EndTime AND EndTime StartTime; // 第一步查冲突 int conflictCount SqlHelper.ExecuteScalar(checkSql, tran, parameters); if (conflictCount 0) { tran.Rollback(); return false; } // 第二步插入把状态写成“待审核” string insertSql INSERT INTO ReservationInfo (RoomId, UserId, BookDate, StartTime, EndTime, Subject, Status) VALUES (RoomId, UserId, BookDate, StartTime, EndTime, Subject, 0); SqlHelper.ExecuteNonQuery(insertSql, tran, parameters); tran.Commit(); return true; } catch { tran.Rollback(); throw; } } } }加了WITH (UPDLOCK, HOLDLOCK)之后第一条事务没结束之前第二条事务的SELECT COUNT会一直等待。常见的误区是只做“先查后插”而不加锁这种查完了别人插进去、自己再插又成功的情况在真实使用中一定会遇到。锁粒度没必要做得太精细会议室预约本身就属于低频写入系统锁表完全能接受。4. 部署与配置改三个地方就能在自己机器上跑起来4.1 数据库准备先执行建表和初始化脚本拿到源码后第一件事是把SQLScripts目录下的建表脚本执行一遍。如果源码包里没有单独的.sql文件可以打开DbHelper.cs或者SQLHelper.cs看连接字符串指向的库名然后手动创建同名数据库。-- SQLScripts/01_CreateTables.sql IF DB_ID(MeetingRoomDB) IS NULL CREATE DATABASE MeetingRoomDB; GO USE MeetingRoomDB; GO -- 执行前面 2.2 节的两张建表语句 -- 插入一些默认会议室数据方便界面跑起来就能看到东西 INSERT INTO MeetingRoomInfo (RoomName, Location, Capacity, HasProjector, Status) VALUES (第一会议室, A栋301, 12, 1, 1), (第二会议室, A栋302, 8, 0, 1), (第三会议室, B栋201, 20, 1, 1);初始化数据不用插太多三间会议室就够验证功能了。插入几条历史预约记录时要注意BookDate用相对日期比如GETDATE()或最近一周内的日期这样界面打开就能看到数据而不是空表。4.2 连接字符串不要把密码写死在代码里连接字符串通常在app.config或App.config里。打开后你看到的内容大概长这样需要按自己本机的 SQL Server 实例修改configuration connectionStrings add nameMeetingRoomDB connectionStringServer.;DatabaseMeetingRoomDB;Integrated Securitytrue; providerNameSystem.Data.SqlClient / /connectionStrings /configuration我自己的一贯做法是连接串只写在配置文件里代码中不允许出现Server...;Password...的硬编码。Server.表示本机的默认实例如果你的 SQL Server 是命名实例就要写成Server你的机器名\\SQLEXPRESS这种形式这里是最容易被坑的。4.3 最小可运行清单按顺序检查这五项跑不起来的时候不要乱猜按下面这个顺序过一遍绝大多数问题都能定位。以下是一种可用的检查清单也可以作为自己的环境确认表。检查项常见现象处理建议.NET Framework / .NET 版本启动报 MissingMethodException用 Visual Studio Installer 安装目标框架SQL Server 实例名连不上报服务器名称找不到确认 Server 后面写的实例名和数据库引擎一致数据库是否存在登录后打开窗体报对象名无效先手动执行 SQL 脚本建库建表SQL Server 端口远程部署时连接超时本地测试用127.0.0.1验证远程再调 TCP/IP 配置Windows 登录认证用密码登录失败改Integrated Securitytrue在本地跑最省事这套源码默认是本地单机 Windows 登录认证方式跑通之后再考虑改 SQL Server 账号和密码登录。部署到局域网时记得检查 Windows 防火墙是否放行了 1433 端口这个属于 SQL Server 远程访问的基础问题和 C# 代码关系不大但每次我帮同事部署时都会遇到一次。5. 避坑排查五个最容易翻车的点与真实原因5.1 明明没人预约却提示时间冲突现象新增一条 9:00-10:00 的预约系统提示和已有记录冲突但当天根本没有通过状态的预约手工查库也看不到任何记录。原因冲突检测只做了Status 1已通过的判断而系统里存在一条被退订或被拒绝但未删除的旧记录。比如之前有人预约了同一时段后来又取消了状态是 4已取消但有些版本的检测逻辑把所有状态都算进去了。解决改查询条件只让真正占用的状态参与冲突判断。比较合理的做法是Status IN (0, 1)或者至少把 2、4 排除掉。我曾经在排查时发现一个更隐蔽的问题自己写测试数据时把状态写成了5不存在的枚举值被数据库接受但业务层根本没处理这一段全靠调试才挖出来。5.2 会议室明明被占了DataGridView 里却显示空现象数据库中有预约数据但界面选择“第一会议室”和当天日期表格里一行数据都不显示。原因界面层绑定了 DataTable 之后列名和数据库字段大小写不匹配或者GetReservationsByRoomAndDate方法里把BookDate参数传错了例如用了DateTime.Now而不是界面上选择的dtpBookDate.Value.Date。解决先在 SQL Server Management StudioSSMS里直接执行那个查询语句用EXEC或者直接SELECT看看有没有结果。如果 SQL 有结果而界面没有检查 DataGridView 的AutoGenerateColumns是不是被改成false同时确认没有用dgv.DataSource null把它重置掉。5.3 结束时间等于开始时间怎么不认为是非法输入现象用户选了 14:00 到 14:00系统照样保存成功后续状态一路显示“已通过”造成数据污染。原因前端下拉框的时间控件只精确到分钟而业务层没有在入口做开始时间小于结束时间的校验。有的版本把StartTime EndTime补上了但只在校验方法里做保存方法没有复用这个方法于是绕过了检查。解决在 BLL 层的CreateReservation方法开头统一做if (start end) throw new ArgumentException(...)不要让 UI 层的 CheckBox 和 ComboBox 分别校验。把校验推到业务层是一种比较保险的统一收口做法UI 将来换掉之后规则仍然保留。5.4 SQL Server 换成 Access 之后TIME 类型的查询全部报错现象源码默认连 SQL Server但拿到手之后把连接串改成 Access 的.accdb数据库查询时频繁报FROM子句语法错误或参数类型不匹配。原因Access 的Jet/ACE引擎对TIME类型支持不友好而且参数语法要改成?占位符。如果对ORDER BY StartTime再加上别名或括号很容易触发 SQL 语法兼容性问题。解决如果确实要用 Access我建议不要硬改原有 SQL而是新建一套AccessSqlBase.cs把所有 SQL 语句里的RoomId换成?同时把TIME字段在 C# 代码里转成DateTime或string再比较。说句实话工作中我更推荐直接把本地部署的数据库换成 SQL Server Express因为功能和维护成本都更省心Access 在并发写入上实在太痛苦了。5.5 部署到服务器之后客户端连不上数据库现象本机跑得好好的把程序拷到另一台电脑上就提示“在建立与服务器的连接时出错”或者“对方计算机积极拒绝”。原因典型的连接字符串问题。服务器上的Server.只代表数据库服务器本机客户端电脑连不上另一个常见原因是 SQL Server 的 TCP/IP 协议默认可能被禁用。解决数据库服务器上确认 TCP/IP 已启用并监听 1433然后在客户端 app.config 里把地址改成真实的 IP比如Server192.168.1.100;DatabaseMeetingRoomDB;Integrated Securitytrue;。如果是跨域环境需要 SQL 认证连接串里补User Idsa;Password***;。切记发布给同事用的时候连接串一定要放到和exe同目录的.config文件里不要编译进程序集。6. 进阶验证围绕四个场景做测试再动手加一个提醒扩展拿到这套源码之后不要直接交付建议自己做一轮完整的测试这是熟悉系统最好的方式。我习惯的测试清单是四个场景正向预约、时间重叠拦截、取消后重约、会议室禁用后不可选。正向预约验证能保存成功时间重叠拦截验证状态为 0、1 的两条记录都能挡住取消后重约验证状态 4 的记录不再参与冲突检测禁用会议室验证Status0的会议室不出现在下拉框里。这四个场景覆盖了这套系统 80% 的业务逻辑跑通之后才敢说这个系统能交给别人用。测试通过后如果还想进阶一步可以试着加一个“会议开始前 30 分钟弹窗提醒”的功能。在 WinForm 主窗体上放一个System.Windows.Forms.Timer每隔一分钟扫描一次当天已经通过状态的预约凡是在当前时间 30 分钟内开始且没有提醒过的就弹一条提示并标记为已提醒private void timerRemind_Tick(object sender, EventArgs e) { string sql SELECT R.Subject, R.StartTime, M.RoomName FROM ReservationInfo R INNER JOIN MeetingRoomInfo M ON R.RoomId M.RoomId WHERE R.BookDate Today AND R.Status 1 AND R.StartTime Now AND R.StartTime RemindThreshold AND R.ReservationId NOT IN (SELECT ReservationId FROM RemindedList); // 对返回的每一行执行 MessageBox.Show / 或者用 NotifyIcon 做静默提醒 // 每处理完一条记录往 RemindedList 表或内存 HashSet 里写一条标记 }这里要注意timerRemind_Tick不要直接在事件里弹MessageBox否则一分钟一次的扫描频率和弹窗阻塞会把界面卡死。更好的做法是把提醒信息放进一个队列主线程用BeginInvoke异步处理或者直接用NotifyIcon.ShowBalloonTip做右下角气泡提示。这个扩展做完之后“预约系统”就算是真正能用了。我自己的习惯是把这套源码拆完、改完、测试完之后把一张“测试记录表”放到 SQLScripts 目录下里面记录每一条测试用的预约数据和预期结果。从那以后每次接手新的预约系统我都是强制自己先执行 SQL 脚本、再跑一遍四个场景、最后才敢动业务代码。这套方法虽然老但在会议室这种低频高冲突场景里从来不会漏事希望帮到你。本文还有配套的精品资源点击获取