
简介这是一套基于C#与SQL Server的学生选课及成绩查询管理系统源码采用CS架构WinForm实现适合计算机专业学生完成课程设计或毕业设计参考。系统内置学生、管理员两类角色覆盖课程开设、选课、成绩录入与统计、课表及成绩单查询、密码修改等完整业务流程。资源包共147个文件包含44个C#源文件、数据库脚本与日志文件、可执行程序及配置文件等压缩包大小3.93MB可直接编译运行并基于SQL Server还原数据库。已有581人学习下载配有项目说明文件与界面截图便于理解模块划分、数据库表结构和核心代码实现可帮助读者快速掌握CS架构下的信息管理系统开发思路。1. 基于C#SQL ServerCS界面的学生选课及成绩查询管理系统到底在解决什么问题一个学期开始前教务老师最怕的就是选课那几天——学生涌入、课程冲突、名额超选、成绩录入对不上号。市面上各种 OA 和教务平台确实多但很多学校或院系至今还在用 Excel 加纸质表单或者干脆外包了一套 B/S 系统改个字段都要走流程。这时候一套基于 C# SQL Server 的 CS 界面学生选课及成绩查询管理系统恰恰是桌面端最实在的答案C# 负责把窗体、按钮、下拉框这些界面交互做利落SQL Server 在后台管学生、课程、选课记录和成绩数据CS 架构让教务老师在自己电脑上就能完成选课审核和成绩录入不需要依赖浏览器和服务器端的复杂部署。这套系统解决的是「数据一致性」和「操作效率」两个核心痛点选课要防止同一学生重复选同一门课、防止选课人数超过课程容量成绩录入要保证分数和学分对得上、学号不能被输错。标题里的「CS界面」点明了架构取向这种模式对于中小规模教务场景非常合适——局域网内跑得稳数据全在本地数据库服务器权限控制也更直接。适合谁适合计算机相关专业的学生做课程设计或毕业设计也适合小范围教务管理的二次开发参考。下面我按自己做过类似项目的路径把这个系统的拆解成数据库设计、C#界面搭建、选课事务处理、成绩查询、部署与避坑几个部分讲透。2. 数据库设计先行选课系统的表结构、约束和关系别急着写代码做管理系统最忌讳一上来就拖控件、写窗体。C# 界面只是壳SQL Server 里的表结构和约束才是这套系统的灵魂。选课和成绩查询的复杂度不高但如果表设计漏了外键后面查成绩会出现各种脏数据。2.1 五张核心表学生表、课程表、选课表、成绩表、管理员表我一般会先建这五张表。学生表存学号、姓名、性别、班级、入学年份课程表存课程编号、课程名、学分、授课教师、上课时间、课程容量、已选人数选课表记录学生选了哪门课、什么时候选的成绩表存学号、课程编号、平时分、期末分、总评、绩点管理员表存教务老师的登录账号和密码哈希。学生和课程之间是多对多关系所以选课表就是中间表成绩表里的学号和课程编号又和选课表对得上。建表脚本的核心在于约束。拿选课表举例主键必须是联合主键学号 课程编号这样数据库层面就杜绝了同一学生重复选同一门课。已选人数这个字段容易踩坑很多人直接在课程表里放一个 Int 字段每次选课就 UPDATE 一下但实际上更好的做法是加入「课程容量」字段通过事务里先检查再插入的方式保证不超选这一点在第 4 章会讲事务写法。CREATE TABLE Student ( StudentID NVARCHAR(20) PRIMARY KEY, Name NVARCHAR(50) NOT NULL, Gender NCHAR(1) CHECK (Gender IN (N男, N女)), ClassName NVARCHAR(50), EnrollmentYear INT ); CREATE TABLE Course ( CourseID NVARCHAR(10) PRIMARY KEY, CourseName NVARCHAR(50) NOT NULL, Credit DECIMAL(3, 1) CHECK (Credit 0), Teacher NVARCHAR(50), Schedule NVARCHAR(100), Capacity INT CHECK (Capacity 0), SelectedCount INT DEFAULT 0 ); CREATE TABLE Enrollment ( StudentID NVARCHAR(20), CourseID NVARCHAR(10), SelectTime DATETIME DEFAULT GETDATE(), PRIMARY KEY (StudentID, CourseID), FOREIGN KEY (StudentID) REFERENCES Student(StudentID), FOREIGN KEY (CourseID) REFERENCES Course(CourseID) ); CREATE TABLE Score ( StudentID NVARCHAR(20), CourseID NVARCHAR(10), UsualScore DECIMAL(5, 2), FinalScore DECIMAL(5, 2), TotalScore DECIMAL(5, 2), GradePoint DECIMAL(3, 2), PRIMARY KEY (StudentID, CourseID), FOREIGN KEY (StudentID) REFERENCES Student(StudentID), FOREIGN KEY (CourseID) REFERENCES Course(CourseID) );建表脚本里我特意加了CHECK约束性别只能填男或女、学分和容量必须大于 0。这样即便 C# 界面上的输入校验漏了数据库这一层也能兜底。联合主键在Enrollment表上尤其关键——没有它就得靠 C# 代码先查一遍再加锁并发高的时候照样可能插重复。SelectedCount字段单独列在Course表里目的不是为了存储冗余而是让选课界面能快速判断剩余名额不用每次 COUNT 一下子表。2.2 视图和存储过程成绩查询和选课统计的查询效率从哪里来CS 界面的成绩查询有个很容易被忽略的性能问题成绩表关联学生表、课程表之后如果每次都直接查三张表 JOIN局域网里数据量小的时候感觉不到但一旦学生上千、课程上百窗口打开就会卡顿。常见做法是建一个视图把学生姓名、班级、课程名、学分、总评、绩点这些常用字段提前关联好C# 端只需要一条SELECT * FROM v_ScoreDetail WHERE StudentID id就能取到完整成绩单。选课统计也建议用存储过程。比如查询某门课的选课名单、计算某学生的已修学分总和这些逻辑写进存储过程有三个好处减少网络传输、SQL 执行计划被缓存、教务老师端的权限可以被收紧到只执行存储过程而不能直接改表。我通常会把「学生选课」这个核心操作封装成存储过程因为它涉及多步操作直接写在 C# 代码里容易漏掉事务边界。CREATE VIEW v_ScoreDetail AS SELECT s.StudentID, s.Name, s.ClassName, c.CourseID, c.CourseName, c.Credit, sc.UsualScore, sc.FinalScore, sc.TotalScore, sc.GradePoint FROM Score sc INNER JOIN Student s ON sc.StudentID s.StudentID INNER JOIN Course c ON sc.CourseID c.CourseID;视图的逻辑很简单就是把三张表的内连接结果固定下来。查询成绩时 C# 端只需要把学号作为参数传进去。需要提醒的是视图里不要加ORDER BY排序放到 C# 端的 DataGridView 上去做否则后续做分页和筛选会绑手绑脚。GradePoint字段我建议在录入成绩时就由触发器或存储过程算好不要在查询时现算绩点不然每次打开成绩单都要做一遍分数到绩点的映射逻辑。2.3 事务边界用 BEGIN TRAN 保住选课和成绩录入的原子性选课不是一条 INSERT 就能完成的要先查课程剩余容量再插入选课记录最后把课程表的SelectedCount加一。这三步里任何一步失败比如容量刚好满了、学生学号不存在前面已经插入的数据就得回滚。SQL Server 里用事务包住这三步是最稳妥的。成绩录入也一样插入成绩时如果学生还没选这门课就不能让他有成绩这时候要先检查选课表里有没有记录。CREATE PROCEDURE usp_EnrollCourse StudentID NVARCHAR(20), CourseID NVARCHAR(10) AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; -- 检查课程容量 DECLARE Capacity INT, SelectedCount INT; SELECT Capacity Capacity, SelectedCount SelectedCount FROM Course WHERE CourseID CourseID; IF SelectedCount Capacity BEGIN ROLLBACK TRANSACTION; RAISERROR(N该课程已选满, 16, 1); RETURN; END -- 插入选课记录 INSERT INTO Enrollment (StudentID, CourseID, SelectTime) VALUES (StudentID, CourseID, GETDATE()); -- 更新已选人数 UPDATE Course SET SelectedCount SelectedCount 1 WHERE CourseID CourseID; COMMIT TRANSACTION; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRANSACTION; THROW; END CATCH; END;存储过程里我把容量检查、插入选课记录、更新已选人数放在同一个事务里。RAISERROR抛出的错误码会被 C# 端捕获到直接在界面上弹出提示「该课程已选满」。注意SET NOCOUNT ON必须写在开头否则每执行一条 SQL 都会返回一个受影响行数SqlDataReader 在处理多个结果集会非常别扭。这里的WITH (UPDLOCK, HOLDLOCK)锁提示我暂时没加因为单机或小规模并发场景下事务隔离级别默认的 Read Committed 已经够用。如果后续并发量真的上来了可以考虑给查询容量的 SELECT 语句加WITH (UPDLOCK, ROWLOCK)防止两个学生同时查到剩余名额为 1 然后一起选上。3. C# 客户端搭建从连接字符串到 DataGridView 联动CS 界面的骨架这样搭数据库准备好之后C# 端要做的事情就是把数据从 SQL Server 拉出来、显示在界面上、把用户的改动写回数据库。CS 界面的优势在这里体现得很直接窗体和控件是编译好的原生应用响应速度、按钮手感、DataGridView 的编辑体验都远好于网页。但 C# 端同样有讲究尤其连接字符串不能写死在代码里否则换一台机器部署就要重新编译。3.1 SqlConnection 封装与连接字符串的三种配置方式每个窗体都写一遍连接字符串是初学阶段最常见的做法后面改一个服务器地址就要全局查找替换。我的习惯是单独建一个DBHelper.cs把SqlConnection的创建集中到一个静态方法里连接字符串从 App.config 里读取。这样换数据库服务器、改登录账号只需要改配置文件。using System; using System.Configuration; using System.Data.SqlClient; public static class DBHelper { private static readonly string ConnStr ConfigurationManager.ConnectionStrings[StudentDB].ConnectionString; public static SqlConnection GetConnection() { var conn new SqlConnection(ConnStr); conn.Open(); return conn; } }App.config 里的连接字符串长这样connectionStrings add nameStudentDB connectionStringData Sourcelocalhost;Initial CatalogStudentCourseDB;User IDsa;Password123456;EncryptFalse / /connectionStrings连接字符串有几个容易翻车的点EncryptFalse这个参数在 SQL Server 2022 和 SqlClient 4.0 之后会变得很关键因为新版本驱动默认强制加密不显式关掉会报 SSL 连接错误User IDsa只在开发环境能用部署到教务老师机器上应该用 Windows 身份验证或者单独建一个低权限账号Initial Catalog一定要和建表时用的数据库名一致否则连上了也看不到表。我见过不少人在代码里用new SqlConnection(server.;uidsa;pwd123)这种写法程序跑通了但非常难维护。连接字符串集中管理之后多窗体的系统才谈得上规范。3.2 主窗体设计左侧树状菜单 右侧 TabPage 布局学生选课及成绩查询管理系统的界面布局常见做法是主窗体左侧一个 TreeView 菜单右侧一个 TabControl。TreeView 节点分成三类学生管理、选课管理、成绩管理。点击节点时动态添加对应的 TabPage而不是把所有功能提前堆在窗体上省得打开程序就加载一堆数据。这种布局的好处是功能分区清楚学生管理里做增删改查选课管理里做课程列表和选课操作成绩管理里做成绩录入和成绩查询。TabControl 的每个 TabPage 独立维护自己的 DataGridView 数据源互不干扰。切换 TabPage 时再用RefreshData()方法重新拉一次数据保证不同窗口看到的数据是最新的。private void tvMenu_AfterSelect(object sender, TreeViewEventArgs e) { switch (e.Node.Name) { case StudentManage: OpenTabPage(学生管理, new StudentManageForm()); break; case CourseManage: OpenTabPage(课程管理, new CourseManageForm()); break; case EnrollManage: OpenTabPage(选课管理, new EnrollManageForm()); break; case ScoreManage: OpenTabPage(成绩管理, new ScoreManageForm()); break; } } private void OpenTabPage(string title, Form childForm) { // 如果已打开相同 TabPage直接切换 foreach (TabPage page in tabMain.TabPages) { if (page.Text title) { tabMain.SelectedTab page; return; } } var newPage new TabPage(title); childForm.TopLevel false; childForm.FormBorderStyle FormBorderStyle.None; childForm.Dock DockStyle.Fill; newPage.Controls.Add(childForm); tabMain.TabPages.Add(newPage); tabMain.SelectedTab newPage; childForm.Show(); }childForm.TopLevel false这一行很多人会漏掉直接往 TabPage 里塞窗体结果窗体自己弹出来根本不嵌进去。设置成false之后还要把FormBorderStyle设成None否则标题栏会占据 TabPage 的空间。每个子窗体自己负责拉数据、绑定 DataGridView主窗体只做承载和切换这样模块之间的耦合度最低。3.3 DataGridView 数据绑定选课列表刷新与已选课程状态显示选课管理界面是整个系统中交互最重的部分。左边一个 DataGridView 显示所有可选课程课程号、课程名、学分、教师、容量、已选人数右边一个 DataGridView 显示当前学生已选的课程中间一个「选课」按钮和一个「退选」按钮。选中左边某行课程点选课按钮就调用第 2 章的usp_EnrollCourse存储过程选成功后两个 DataGridView 都要刷新。private void btnEnroll_Click(object sender, EventArgs e) { if (dgvAllCourses.CurrentRow null) return; string studentId txtStudentId.Text.Trim(); string courseId dgvAllCourses.CurrentRow.Cells[CourseID].Value.ToString(); using (var conn DBHelper.GetConnection()) using (var cmd new SqlCommand(usp_EnrollCourse, conn)) { cmd.CommandType CommandType.StoredProcedure; cmd.Parameters.AddWithValue(StudentID, studentId); cmd.Parameters.AddWithValue(CourseID, courseId); try { cmd.ExecuteNonQuery(); MessageBox.Show(选课成功); LoadStudentCourses(studentId); LoadAllCourses(); } catch (SqlException ex) { MessageBox.Show(ex.Message, 选课失败, MessageBoxButtons.OK, MessageBoxIcon.Warning); } } }AddWithValue在传字符串参数时有个需要注意的行为如果 SQL Server 表字段是NVARCHAR(20)而传入的字符串长度超过 20会直接报截断错误。所以 C# 端在调用存储过程之前要先做长度校验或者用new SqlParameter指定Size属性。选课成功后调LoadStudentCourses和LoadAllCourses两个方法刷新界面但这种做法的缺点是每次都全表刷新。数据量大了之后更优的方案是只更新受影响的那两行这个我们在第 6 章讲进阶优化时再展开。4. 选课事务与并发冲突为什么两条 SQL 也会出现数据不一致很多人觉得 C# 连接 SQL Server 执行几条 SQL 是再简单不过的事但选课系统天然有并发问题两个学生同时选最后一门课或者一个学生快速点了两次「选课」按钮。第 2 章的存储过程已经用事务保证了单条选课不会超容量但事务隔离级别和锁策略会影响实际并发下的表现。4.1 默认隔离级别下的竞态条件Lost Update 是怎么发生的SQL Server 默认的隔离级别是 Read Committed在这个级别下事务 A 读取课程容量为 1事务 B 也同时读取容量为 1两个事务都通过IF SelectedCount Capacity检查然后各自插入选课记录。结果就是两个学生都选上了课程容量溢出数据不一致。这是典型的 Lost Update 问题。修复方案有两种。一种是在存储过程里加锁提示SELECT Capacity Capacity, SelectedCount SelectedCount FROM Course WITH (UPDLOCK, ROWLOCK) WHERE CourseID CourseID;UPDLOCK让查询拿到更新锁而不是共享锁ROWLOCK把锁粒度限制在行级别避免锁住整个表。这样事务 A 读取容量并拿到更新锁期间事务 B 的读取会被阻塞直到 A 提交或回滚B 再读取时看到的SelectedCount已经是更新后的值。另一种方案是改用sp_getapplock做应用级锁但那是重量级做法对课程表行锁已经足够。4.2 重复选课校验除了数据库约束前端按钮要加防重复提交数据库层面有联合主键重复选课会报主键冲突错误但用户看到的是「违反主键约束」这种不友好的报错。更好的做法是在存储过程里先检查Enrollment表是否已有记录如果有就自定义错误消息。同时C# 界面上的选课按钮要在用户点击后立刻置灰等操作完成再恢复。双保险才能防止用户在数据库响应慢的时候连点两下造成两条一模一样的请求。4.3 死锁处理为什么我的存储过程偶尔报「事务死锁」错误选课事务里先更新Course表再插入Enrollment表如果另一个事务的反向顺序操作就可能造成死锁。SQL Server 会自动选择一个事务作为牺牲者回滚并抛出错误 1205。C# 端捕获到死锁错误后不应该直接给用户弹窗而是应该自动重试两到三次。这个场景在选课系统里不算高频但如果选课和退选功能同时被多个学生操作就会出现。// 死锁重试 const int maxRetry 3; int retryCount 0; while (retryCount maxRetry) { try { cmd.ExecuteNonQuery(); break; } catch (SqlException ex) when (ex.Number 1205) { retryCount; if (retryCount maxRetry) throw; System.Threading.Thread.Sleep(200 * retryCount); } }5. 避坑与排查学生选课及成绩查询系统最常见的 5 个故障点这套系统在开发、部署、日常使用过程中我积累了不少血泪经验。以下 5 个问题按发生频率排列都是真实场景里跑出来的新手照着排错能省很多时间。5.1 连接字符串报错用户登录失败 / 找不到服务器现象程序一运行弹窗显示「用户 sa 登录失败」或「在建立与服务器的连接时出错」。原因通常有两种SQL Server 的 TCP/IP 协议没启用或者混合认证模式没开。解决方法是打开 SQL Server 配置管理器启用 TCP/IP然后在 SSMS 里把服务器认证模式改成「SQL Server 和 Windows 身份验证模式」重启 SQL Server 服务。还有一点容易被忽略SQL Server 2019 默认不启用 sa 账号需要手动在安全里把 sa 启用并设密码。5.2 DataGridView 显示列顺序不对数字列全变成了文本现象绑定 DataTable 后DataGridView 自动生成列但顺序比较混乱而且像学分、容量这种数字列默认是右对齐的看起来不整齐。解决方法是关闭AutoGenerateColumns手动定义列绑定DataPropertyName。这一步在选课管理界面尤其重要因为课程号、课程名、教师这些列名和表字段名不一定一一对应。5.3 输入学号查询成绩没结果参数带空格现象用户在 TextBox 里输入学号按查询按钮没有结果但直接在 SSMS 里查同一个学号却能查到。原因大多是用户在输入时手滑带了空格或者从 Excel 复制过来的学号隐藏了不可见字符。解决办法是在 C# 端做Trim()再配合 SQL 查询时用LTRIM(RTRIM())。最省心的方案是在TextChanged事件里直接禁掉输入空格。5.4 成绩录入保存失败外键冲突现象录成绩保存时提示「INSERT 语句与 FOREIGN KEY 约束冲突」。这是因为成绩表要求学生必须先选这门课才能录成绩但实际操作时教务老师可能直接在成绩管理界面里录入某个没有选课记录的学生。解决办法有两个方向一是选课表和成绩表都用视图展示学生姓名录成绩时只允许选择「已经选了这门课」的学生二是在保存成绩的存储过程里先查询选课表不存在则拒绝写入并给出友好提示。5.5 程序部署到别的电脑跑不起来缺少运行环境或配置文件现象开发机上一切正常拷贝到教务老师的电脑上双击运行提示缺少 .NET Framework 版本或者没有权限读取配置文件。解决方法是把目标框架改成 .NET Framework 4.6.2 或更高用 Inno Setup 或 ClickOnce 打包把依赖的运行时和配置文件一起装好。另外 App.config 里如果有EncryptFalse在旧版 .NET Framework 上运行不会有问题但如果目标机器只装了 .NET 5 或更高版本就要小心 System.Data.SqlClient 的版本差异。6. 进阶实践SQL Profiler 排查慢查询、备份策略与日志审计系统能跑通只是第一步真正让它能长期稳定服务的是性能调优和数据安全。6.1 用 SQL Server Profiler 定位成绩查询慢的原因成绩查询界面如果偶尔卡顿不要猜直接用 SQL Server Profiler 抓一下实际执行的 SQL。在 Profiler 里新建跟踪过滤掉Login事件和Logout事件只看SQL:BatchCompleted和RPC:Completed就会看到 C# 端发出的完整 SQL 语句和执行耗时。常见慢查询原因有两个一是表缺少索引比如Score表上只有主键索引而查询经常按StudentIDCourseID两个条件过滤这种时候要给Score表建立复合索引二是存储过程里的参数嗅探问题某个查询的预估行数失真导致执行计划不理想这时可以加OPTION (RECOMPILE)或者在 C# 端捕获到异常时重试。6.2 定期备份教务数据不能没有后悔药数据库备份是这套系统里最值钱的运维动作。SQL Server 的备份有三种方式完整备份、差异备份、事务日志备份。学生选课系统数据量不大我建议每天做一次完整备份保留最近 7 天的备份文件就够了。自动化备份可以直接用 SQL Server Agent 建作业也可以写一个简单的 PowerShell 脚本配合 Windows 任务计划程序。备份文件应该拷贝到另一台机器或网盘避免数据库服务器硬盘坏了连带备份一起丢。BACKUP DATABASE StudentCourseDB TO DISK ND:\Backup\StudentCourseDB_20250101.bak WITH INIT, NAME NStudentCourseDB-Full Backup;上面这条 SQL 手动执行没问题但自动化时要注意WITH INIT会覆盖同名文件所以备份文件名里带上日期。恢复备份时要注意数据库当前是否有人连接如果有人连接RESTORE 会报「数据库正在使用中」。解决办法是先ALTER DATABASE StudentCourseDB SET SINGLE_USER WITH ROLLBACK IMMEDIATE恢复完再切回多用户模式。6.3 日志审计谁在什么时间改了成绩教务数据容易出纠纷学生质疑成绩被篡改是常见问题。解决办法是在Score表上建审计触发器把每次 INSERT、UPDATE、DELETE 操作连同操作人、操作时间写入一张ScoreLog表。C# 端在登录时把当前用户 ID 存在一个静态变量里SQL Server 端触发器通过SUSER_SNAME()获取登录名两者配合就能记录操作人。CREATE TRIGGER trg_ScoreAudit ON Score AFTER INSERT, UPDATE, DELETE AS BEGIN SET NOCOUNT ON; INSERT INTO ScoreLog (StudentID, CourseID, OldTotal, NewTotal, OperateTime, OperateUser) SELECT COALESCE(i.StudentID, d.StudentID), COALESCE(i.CourseID, d.CourseID), d.TotalScore, i.TotalScore, GETDATE(), SUSER_SNAME() FROM inserted i FULL OUTER JOIN deleted d ON i.StudentID d.StudentID AND i.CourseID d.CourseID; END;这个触发器用FULL OUTER JOIN同时处理了插入和删除的情况——插入时deleted表为空删除时inserted表为空更新时两个表都有数据。无论是新增成绩、修改成绩还是删除成绩都能在ScoreLog表里留下痕迹。这套审计机制做完教务老师再也不用怕「成绩被谁改过」这类扯皮问题。6.4 我的几条实践经验这套系统的开发周期正常来说一个人一周能从零到能用。数据库设计花两天C# 窗体开发花三天剩下的时间全部投入联调和避坑。我自己的习惯是每做完一个功能模块先用 SQL Profiler 看一下发出的 SQL 是否符合预期再手动在 SSMS 里造几条脏数据测试边界条件——比如让选课人数恰好等于容量、录一个超出范围的成绩。这套系统看似简单但把选课并发、成绩约束、操作审计这些细节做扎实之后能直接用得很好。希望帮到你。本文还有配套的精品资源点击获取