ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C# WinForms学生选课系统实战:从数据库设计到并发控制

C# WinForms学生选课系统实战:从数据库设计到并发控制 简介这份资源是一套基于C# Windows窗体开发的学生信息管理与选课系统面向正在学习桌面应用开发、需要课程设计或毕业设计参考的高校学生与初级开发者。系统采用SQL Server数据库与三层架构包含学生端和教师端两个登录入口覆盖管理员注册登录、学生信息增删改查、课程发布、学生信息上传及选课等完整业务闭环并涉及图片存储等实用技术点。压缩包共141个文件以cs源码、dll动态库、pdb调试文件、resx资源文件及config配置为主整体约1.5MB另附数据库表结构代码与简要说明按提示生成数据库即可运行。目前已有4416人学习下载适合希望理解三层架构分层设计、掌握窗体与数据库交互及选课逻辑实现的读者参考借鉴。1. 从一份课程设计说起C# WinForms 学生选课系统到底要解决什么每年毕业季计算机专业的学生都会接到一份「数据库课程设计」任务其中出现频率最高的题目之一就是学生信息管理系统。而一旦把「选课」这个业务塞进去事情就变得有意思了——它不再是简单的增删改查而是涉及多表关联、事务控制、并发冲突和界面状态同步。用 C# Windows 窗体配合 SQL Server 或 MySQL 做一套带数据库的选课系统本质上是在训练一种能力把现实中的选课规则翻译成表结构再把表结构翻译成用户能点得动的界面。这套系统适合谁一是正在做课程设计、需要一份能跑起来、能答辩的作品的同学二是刚接触 C# 和 ADO.NET、想找一个完整项目练手的入门开发者三是需要快速搭一个教务类桌面工具原型的工程师。它不追求高并发分布式但要求你理解连接池、参数化查询、事务边界和 DataGridView 的数据绑定机制。下面我按实际开发顺序把选型、建库、核心功能、避坑和进阶技巧一层层拆开讲。2. 技术选型与环境搭建为什么是 WinForms ADO.NET SQL Server2.1 桌面端选 WinForms 而不是 WPF 的现实理由很多教程一上来就推 WPF说它界面现代、数据绑定强。但如果你做过课程设计答辩就知道老师看的是功能完整度和代码结构不是动画效果。WinForms 的优势在于拖控件快、事件模型直观、DataGridView 开箱即用一个下午就能把主界面搭出来。WPF 的 XAML 学习曲线对新手不友好数据绑定写错一个 PropertyChanged 就整个界面不刷新调试成本高。另一个现实因素是资料密度。你在搜索引擎里搜「C# 学生信息管理系统」90% 的示例是 WinForms。遇到问题能快速找到参考这在赶进度的时候比技术先进更重要。当然WinForms 也有它的边界高 DPI 缩放会糊、复杂自定义绘制麻烦、MVVM 支持弱。但对于选课系统这种以表格和表单为主的场景够用。2.2 数据库选 SQL Server Express 还是 MySQL标题里写的是「含数据库」没有指定具体产品。我一般推荐 SQL Server Express原因有三第一Visual Studio 对它的集成最顺服务器资源管理器里直接建库建表不用额外装客户端第二C# 的 SqlClient 是原生驱动不需要像 MySQL 那样额外引 NuGet 包第三课程设计环境里老师机器上大概率有 SQL Server。但如果你已经装了 MySQL或者学校要求用开源方案也完全可以。下面给一个连接字符串的对比方便你按自己的环境改数据库连接字符串示例需要额外安装SQL Server ExpressServer.\SQLEXPRESS;DatabaseCourseSelectDB;Trusted_ConnectionTrue;SQL Server ExpressMySQL 8.xServerlocalhost;DatabaseCourseSelectDB;Uidroot;Pwd你的密码;MySQL Connector/NETSQLiteData SourceCourseSelectDB.db;Version3;System.Data.SQLite提示课程设计建议用 SQL Server Express省去驱动配置的麻烦。如果换 MySQL记得在 NuGet 里装 MySql.Data并把代码里的 SqlConnection 换成 MySqlConnection。2.3 建库建表四张核心表的最小可用结构选课系统的数据模型不复杂但要把关系理清。核心是四张表学生表、课程表、选课记录表、用户表用于登录。选课记录表是典型的中间表用联合主键防止重复选课。-- 学生表 CREATE TABLE Students ( StudentId VARCHAR(20) PRIMARY KEY, -- 学号业务主键 Name NVARCHAR(50) NOT NULL, Gender CHAR(2), Major NVARCHAR(50), Grade INT ); -- 课程表 CREATE TABLE Courses ( CourseId VARCHAR(20) PRIMARY KEY, -- 课程号 CourseName NVARCHAR(100) NOT NULL, Credit DECIMAL(3,1), -- 学分保留一位小数 Capacity INT NOT NULL, -- 容量上限 Teacher NVARCHAR(50) ); -- 选课记录表联合主键防止同一学生重复选同一门课 CREATE TABLE Enrollments ( StudentId VARCHAR(20) NOT NULL, CourseId VARCHAR(20) NOT NULL, EnrollTime DATETIME DEFAULT GETDATE(), Score DECIMAL(5,1) NULL, -- 成绩选课时为空 PRIMARY KEY (StudentId, CourseId), FOREIGN KEY (StudentId) REFERENCES Students(StudentId), FOREIGN KEY (CourseId) REFERENCES Courses(CourseId) ); -- 用户表登录用角色区分学生和管理员 CREATE TABLE Users ( UserId VARCHAR(20) PRIMARY KEY, Password VARCHAR(64) NOT NULL, -- 存哈希不存明文 Role VARCHAR(10) NOT NULL -- student 或 admin );这里有几个设计决策值得说明。学号和课程号用 VARCHAR 而不是 INT因为真实学号可能带字母或前导零。选课记录表用 (StudentId, CourseId) 联合主键数据库层面就杜绝了重复选课比在代码里查一遍再插入可靠得多。Score 字段允许 NULL因为选课那一刻还没有成绩这个空值语义要保留。密码字段长度给 64是为了存 SHA256 哈希后的十六进制字符串。2.4 项目结构三层分离但不过度设计新手常犯的错是把所有代码写在 Form 的按钮事件里几百行堆在一起改一个查询要翻半天。我建议至少分三个文件夹Models 放实体类DAL 放数据库访问Forms 放界面。不用上依赖注入和仓储模式那对课程设计是过度设计。// Models/Student.cs public class Student { public string StudentId { get; set; } public string Name { get; set; } public string Gender { get; set; } public string Major { get; set; } public int Grade { get; set; } } // DAL/DbHelper.cs public static class DbHelper { // 连接字符串集中管理换数据库只改这一处 private static readonly string ConnStr Server.\SQLEXPRESS;DatabaseCourseSelectDB;Trusted_ConnectionTrue;; public static SqlConnection GetConnection() { var conn new SqlConnection(ConnStr); conn.Open(); return conn; } }把连接字符串集中在一个地方后面换环境只改一行。GetConnection 里直接 Open调用方用 using 包起来确保释放回连接池。ADO.NET 默认开启连接池不用手动管理但前提是你得及时 Dispose否则池子会被占满。3. 核心功能实现登录、选课、退课与容量控制3.1 登录模块参数化查询与密码哈希登录是第一个要跑通的功能也是新手最容易写出 SQL 注入的地方。下面这段代码演示正确的做法// DAL/UserDAL.cs public static bool ValidateUser(string userId, string password, out string role) { role null; // 先对输入密码做哈希与库中存储的哈希比对 string hash ComputeSha256(password); string sql SELECT Role FROM Users WHERE UserIduid AND Passwordpwd; using (var conn DbHelper.GetConnection()) using (var cmd new SqlCommand(sql, conn)) { // 参数化杜绝 OR 11 这类注入 cmd.Parameters.AddWithValue(uid, userId); cmd.Parameters.AddWithValue(pwd, hash); using (var reader cmd.ExecuteReader()) { if (reader.Read()) { role reader.GetString(0); return true; } } } return false; } private static string ComputeSha256(string input) { using (var sha System.Security.Cryptography.SHA256.Create()) { byte[] bytes sha.ComputeHash(Encoding.UTF8.GetBytes(input)); // 转成十六进制字符串长度 64与表结构对应 return BitConverter.ToString(bytes).Replace(-, ).ToLower(); } }关键点有三个。第一用 uid 和 pwd 占位符绝不用字符串拼接。第二密码存哈希不存明文即使用户表被拖走也拿不到原始密码。第三out 参数返回角色调用方根据角色决定跳转到学生界面还是管理员界面。登录成功后把 UserId 和 Role 存到一个静态类或主窗体的字段里后续选课操作要用。3.2 选课功能事务 容量检查的完整流程选课是整套系统的核心也是最容易出并发问题的地方。假设课程容量 50 人当前已选 49 人两个学生同时点选课如果先查人数再插入可能都查到 49然后都插入成功变成 51 人。解决办法是把检查和插入放在一个事务里并对课程行加锁。// DAL/EnrollDAL.cs public static string EnrollCourse(string studentId, string courseId) { using (var conn DbHelper.GetConnection()) using (var tran conn.BeginTransaction()) { try { // 第一步锁定课程行防止并发超选 string checkSql SELECT Capacity, (SELECT COUNT(*) FROM Enrollments WHERE CourseIdcid) AS Enrolled FROM Courses WITH (UPDLOCK, ROWLOCK) WHERE CourseIdcid; int capacity, enrolled; using (var cmd new SqlCommand(checkSql, conn, tran)) { cmd.Parameters.AddWithValue(cid, courseId); using (var reader cmd.ExecuteReader()) { if (!reader.Read()) return 课程不存在; capacity reader.GetInt32(0); enrolled reader.GetInt32(1); } } if (enrolled capacity) return 课程已满; // 第二步检查是否已选过联合主键也会拦但提前给出友好提示 string dupSql SELECT COUNT(*) FROM Enrollments WHERE StudentIdsid AND CourseIdcid; using (var cmd new SqlCommand(dupSql, conn, tran)) { cmd.Parameters.AddWithValue(sid, studentId); cmd.Parameters.AddWithValue(cid, courseId); int exists (int)cmd.ExecuteScalar(); if (exists 0) return 你已经选过这门课; } // 第三步插入选课记录 string insertSql INSERT INTO Enrollments(StudentId, CourseId) VALUES(sid, cid); using (var cmd new SqlCommand(insertSql, conn, tran)) { cmd.Parameters.AddWithValue(sid, studentId); cmd.Parameters.AddWithValue(cid, courseId); cmd.ExecuteNonQuery(); } tran.Commit(); return 选课成功; } catch (Exception ex) { tran.Rollback(); return 选课失败 ex.Message; } } }这段代码的逻辑链条是开事务 → 锁课程行并统计已选人数 → 判断容量 → 判断重复 → 插入 → 提交。WITH (UPDLOCK, ROWLOCK) 是 SQL Server 的锁提示UPDLOCK 表示在读取时就加更新锁阻止其他事务同时读取并准备更新同一行ROWLOCK 表示锁粒度到行而不是页。这两个提示配合事务能有效防止超选。参数说明sid 是学号cid 是课程号都通过 AddWithValue 传入。返回字符串直接给界面弹 MessageBox 用简单直接。如果你用 MySQL锁提示语法不同需要用 SELECT ... FOR UPDATE且要确保表引擎是 InnoDB。3.3 退课与选课列表刷新DataGridView 绑定技巧退课逻辑比选课简单删记录即可但要注意只有未出成绩的课才能退。界面这边选课列表用 DataGridView 展示刷新时不要每次重新 new 一个 DataGridView而是更新 DataSource。// 刷新学生已选课程列表 public static DataTable GetMyCourses(string studentId) { string sql SELECT c.CourseId, c.CourseName, c.Credit, c.Teacher, e.EnrollTime, e.Score FROM Enrollments e INNER JOIN Courses c ON e.CourseId c.CourseId WHERE e.StudentId sid ORDER BY e.EnrollTime DESC; using (var conn DbHelper.GetConnection()) using (var cmd new SqlCommand(sql, conn)) using (var adapter new SqlDataAdapter(cmd)) { cmd.Parameters.AddWithValue(sid, studentId); var dt new DataTable(); adapter.Fill(dt); return dt; } } // 窗体里调用 private void RefreshMyCourses() { dgvMyCourses.DataSource EnrollDAL.GetMyCourses(CurrentUser.UserId); // 隐藏不需要的列调整列宽 if (dgvMyCourses.Columns.Contains(EnrollTime)) dgvMyCourses.Columns[EnrollTime].HeaderText 选课时间; }用 SqlDataAdapter 填充 DataTable再赋给 DataSourceWinForms 会自动生成列。每次刷新重新查一遍数据库数据量小的时候完全没问题。如果数据量大可以考虑 BindingSource 做本地缓存但课程设计没必要。注意DataGridView 的 AutoSizeColumnsMode 建议设为 Fill否则列宽会很别扭。另外如果绑定的 DataTable 列名和界面显示名不一致用 HeaderText 改不要改 SQL 里的别名保持数据层和界面层解耦。4. 避坑与排查那些让我熬夜的选课系统 bug4.1 现象选课成功但列表不刷新重启程序才看到原因界面用了新的 DataGridView 实例或者手动 Add 行没有重新绑定 DataSource。WinForms 的数据绑定是单向通知DataTable 变了不会自动推给控件。解决每次增删改后调用 RefreshMyCourses()内部重新查库并赋 DataSource。如果坚持用本地缓存需要调用 BindingSource.ResetBindings(false)。4.2 现象连接池耗尽程序卡死报「超时时间已到」原因SqlConnection 没有用 using 包裹或者异常路径下没走到 Dispose连接一直占着不还。ADO.NET 默认连接池上限 100循环里开连接不关几十次就爆了。解决所有 SqlConnection、SqlCommand、SqlDataReader 一律用 using。DbHelper.GetConnection 返回已打开的连接调用方必须 using。排查时可以在连接字符串加Max Pool Size50;观察是否缓解但根治靠正确释放。4.3 现象并发选课时容量变成 51超出上限原因先 SELECT COUNT 再 INSERT两条语句之间没有锁两个事务都读到 49。这是典型的竞态条件。解决如 3.2 节所示把检查和插入放进同一个事务并对课程行加 UPDLOCK。另一种方案是在 Courses 表加一个 EnrolledCount 字段用UPDATE Courses SET EnrolledCountEnrolledCount1 WHERE CourseIdcid AND EnrolledCountCapacity根据受影响行数判断是否成功这样更简洁但需要维护冗余字段。4.4 现象中文课程名在数据库里变成问号原因数据库排序规则不是中文或者连接字符串没指定字符集。SQL Server 里如果字段是 VARCHAR 而不是 NVARCHAR中文会丢。解决建表时中文列一律用 NVARCHAR。连接字符串对 MySQL 要加CharSetutf8mb4;。已经建错表的用 ALTER TABLE 改列类型但已有数据可能已经损坏需要重新导入。4.5 现象登录时输入 OR 11竟然进去了原因SQL 拼接。SELECT * FROM Users WHERE UserId userId 这种写法输入特殊字符就能绕过验证。解决全部改参数化查询如 3.1 节所示。这是血泪教训课程设计答辩时老师很可能当场试这个。改完之后顺便把密码改成哈希存储一步到位。5. 进阶技巧用存储过程收口选课逻辑与成绩录入当基本功能跑通后我习惯把选课这种多步骤操作收进存储过程。好处是逻辑集中在数据库端C# 只负责调用和展示界面层变薄也避免了不同客户端重复实现同一套规则。下面这个存储过程把容量检查、重复检查和插入合并在一起配合事务使用。CREATE PROCEDURE sp_EnrollCourse StudentId VARCHAR(20), CourseId VARCHAR(20), Result NVARCHAR(50) OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; -- 锁定课程行 DECLARE Capacity INT, Enrolled INT; SELECT Capacity Capacity FROM Courses WITH (UPDLOCK, ROWLOCK) WHERE CourseId CourseId; IF Capacity IS NULL BEGIN SET Result N课程不存在; ROLLBACK TRANSACTION; RETURN; END SELECT Enrolled COUNT(*) FROM Enrollments WHERE CourseId CourseId; IF Enrolled Capacity BEGIN SET Result N课程已满; ROLLBACK TRANSACTION; RETURN; END IF EXISTS (SELECT 1 FROM Enrollments WHERE StudentIdStudentId AND CourseIdCourseId) BEGIN SET Result N已选过该课程; ROLLBACK TRANSACTION; RETURN; END INSERT INTO Enrollments(StudentId, CourseId) VALUES(StudentId, CourseId); SET Result N选课成功; COMMIT TRANSACTION; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRANSACTION; SET Result N系统错误 ERROR_MESSAGE(); END CATCH ENDC# 端调用时用 SqlCommand 的 CommandType.StoredProcedure把 Result 设为 Output 参数public static string EnrollViaSp(string studentId, string courseId) { using (var conn DbHelper.GetConnection()) using (var cmd new SqlCommand(sp_EnrollCourse, conn)) { cmd.CommandType CommandType.StoredProcedure; cmd.Parameters.AddWithValue(StudentId, studentId); cmd.Parameters.AddWithValue(CourseId, courseId); var outParam new SqlParameter(Result, SqlDbType.NVarChar, 50) { Direction ParameterDirection.Output }; cmd.Parameters.Add(outParam); cmd.ExecuteNonQuery(); return outParam.Value.ToString(); } }这样 C# 端只剩三行有效逻辑规则全在数据库里。成绩录入也是类似思路写一个 sp_UpdateScore限制只有管理员角色能调用并在存储过程里校验分数范围 0 到 100。验证方法上我一般会开两个 SSMS 查询窗口同时执行选课存储过程观察是否有一个返回「课程已满」。如果两个都成功说明锁没生效检查是否漏了 UPDLOCK 或者隔离级别被改过。另一个习惯是每次改完存储过程先在 SSMS 里用 EXEC 带参数跑一遍确认输出符合预期再回到 C# 里调。这套系统做完最大的收获不是 WinForms 拖控件而是理解了「业务规则放在哪一层」这件事。我踩过最深的坑是把容量检查写在按钮点击事件里结果测试时用两个客户端同时点数据就乱了。后来把规则收进事务和存储过程世界才安静下来。如果你也在做类似的东西先把并发场景想清楚再动手写界面能省下很多后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表