C# + SQL Server 实现学生选课与成绩管理系统全方案 简介这份源码包是一个基于C#与SQL Server数据库开发的学生选课及成绩查询管理系统面向需要完成课程设计、毕业设计或希望学习桌面数据库应用开发的人群可直接用来熟悉从界面设计、数据表创建到增删改查的完整流程。系统使用C#窗体作为前端采用ADO.NET连接后台数据库实现了学生管理、课程管理、选课记录、成绩查询与权限控制等典型功能同时配有数据库建表脚本、运行说明以及界面预览图片方便初学者快速理解项目结构。压缩包中共有123个文件主要包括C#源代码、窗体资源文件、可执行程序与依赖库、数据库物理文件及SQL脚本此外还有相关文档和配置文件整体压缩后大小为4.15MB便于下载和本地部署。目前已有366人浏览学习适合作为课程设计或毕业设计的参考资料开发者打开解决方案后既可以查看各功能模块的代码组织方式也能借助数据库脚本直接搭建环境为二次开发和功能扩展提供良好基础。1. 学生选课及成绩查询管理系统从课设题目到一个能交差的完整方案很多人在拿到“基于C#与SQL Server数据库的学生选课及成绩查询管理系统”这个题目时第一反应是上网找现成源码。但真正让人头疼的往往不是业务逻辑而是这一套东西能不能在自己机器上跑起来、能不能在答辩现场演示流畅、能不能在交给别人部署时少挨骂。这套系统本质上就是 C# 写界面、SQL Server 存数据、用增删改查把学生选课和成绩查询串起来的一个典型课设项目。文章会从建表、数据访问层、登录角色一路写到选课并发、成绩录入和源码打包后遇到的坑。适合正在做课设的本科生、准备转向.NET方向的新人以及想快速搭一个内部选课原型的一线开发。2. 先把架子立起来选课系统的表结构设计与三张核心表2.1 选课业务拆成三张表Student、Course、SC为什么够用选课系统的业务实体并不复杂学生要登录、要选课、要查成绩课程有基本信息、有容量限制学生和课程之间是“选课”这个关系关系上还要挂一个成绩字段。所以三张表就能覆盖全部核心场景不需要在一开始就引入角色表、权限表、日志表这些扩展设计。Student学生信息包含学号、姓名、班级、登录密码。Course课程信息包含课程号、课程名、学分、教师、容量、已选人数。SC选课关系表记录哪个学生选了哪门课、考了多少分。把密码直接放在 Student 表里对课设是可接受的但要知道这在生产环境是不合格的。如果想让项目看起来更完整可以多建一张 Teacher 表和 Admin 表登录时分表查询也就是极简的 RBAC。我一般建议在核心三表跑通后再加不要一开始就把联表复杂度堆上去否则后面调试时光是区分登录角色就会耗掉大量时间。表结构设计阶段多花十分钟想清楚字段类型后面写 C# 会少很多“类型转换失败”的报错。2.2 建库建表与初始数据一份可重复执行的SQL脚本-- -- 选课系统建库脚本适用于 SQL Server 2008 R2 及以上版本 -- 文件编码建议保存为 UTF-8 with BOM避免中文乱码 -- IF DB_ID(CourseDB) IS NULL CREATE DATABASE CourseDB; GO USE CourseDB; GO -- 学生表登录密码用 nvarchar课设场景可以先放明文 IF OBJECT_ID(dbo.Student, U) IS NULL BEGIN CREATE TABLE dbo.Student ( StudentId NVARCHAR(20) NOT NULL PRIMARY KEY, -- 学号如 20230001 StudentName NVARCHAR(50) NOT NULL, -- 姓名 ClassName NVARCHAR(50) NULL, -- 班级 Pwd NVARCHAR(50) NOT NULL DEFAULT 123456 ); END GO -- 课程表 IF OBJECT_ID(dbo.Course, U) IS NULL BEGIN CREATE TABLE dbo.Course ( CourseId NVARCHAR(10) NOT NULL PRIMARY KEY, -- 课程号 CourseName NVARCHAR(50) NOT NULL, -- 课程名 Credit DECIMAL(3,1) NOT NULL DEFAULT 2, -- 学分支持3.5这种值 Teacher NVARCHAR(50) NULL, -- 任课教师 Capacity INT NOT NULL DEFAULT 60, -- 选课容量 Selected INT NOT NULL DEFAULT 0 -- 已选人数 ); END GO -- 选课关系表联合主键成绩允许为空表示未录入 IF OBJECT_ID(dbo.SC, U) IS NULL BEGIN CREATE TABLE dbo.SC ( StudentId NVARCHAR(20) NOT NULL, CourseId NVARCHAR(10) NOT NULL, Score DECIMAL(5,1) NULL, PRIMARY KEY (StudentId, CourseId) ); END GO -- 插入少量测试数据 INSERT INTO dbo.Student (StudentId, StudentName, ClassName) VALUES (20230001, N张伟, N计科2301), (20230002, N李娜, N计科2301), (20230003, N王强, N软工2302); INSERT INTO dbo.Course (CourseId, CourseName, Credit, Teacher, Capacity) VALUES (CS101, NC#程序设计, 3.0, N刘老师, 60), (CS102, N数据库原理, 3.0, N陈老师, 50), (MA101, N高等数学, 4.0, N赵老师, 120); GO逻辑说明脚本里用了IF OBJECT_ID(dbo.表名, U) IS NULL做前置判断执行前会检查对象是否已存在所以脚本可以重复跑而不会报错。这在你反复建库、或者把脚本交给同学执行时非常省心。数据库名固定为 CourseDBSQL Server 2008 R2 到 2019 都能直接执行。参数说明StudentId 用 NVARCHAR(20) 而不是 INT因为学号经常以 0 开头用整数类型会把前导零丢掉。Credit 用 DECIMAL(3,1) 而不是 FLOAT避免浮点比较的精度问题。Pwd 默认给123456便于开发阶段快速登录但答辩前最好换成散列值第 6 章会写具体做法。2.3 增删改查之外外键与CHECK约束让数据库兜住脏数据很多新手喜欢把所有校验写在 C# 界面里数据库表建成“光秃秃”的四张表什么约束都不加。这样做的后果是C# 端漏掉一个判断库里就多一条脏数据。选课关系表应该加上外键和 CHECK 约束让数据库成为第二道防线。-- 外键约束保证选课记录里的学生和课程真实存在 ALTER TABLE dbo.SC ADD CONSTRAINT FK_SC_Student FOREIGN KEY (StudentId) REFERENCES dbo.Student(StudentId); ALTER TABLE dbo.SC ADD CONSTRAINT FK_SC_Course FOREIGN KEY (CourseId) REFERENCES dbo.Course(CourseId); -- 课程表约束已选人数不能为负数且不能超过容量 ALTER TABLE dbo.Course ADD CONSTRAINT CK_Course_Selected CHECK (Selected 0 AND Selected Capacity);逻辑说明加上外键后往 SC 表插入一条不存在的 StudentId 会直接报错C# 端只要捕获 SqlException 就能给用户提示。CHECK 约束保证 Selected 永远不可能超过 Capacity哪怕 C# 端的并发判断出了问题数据库也会拒绝更新。这个是“双保险”思路课设答辩时很加分。2.4 数据访问前的自检三条SQL确认没问题再动C#建好表后不要急着写 WinForms 界面。先打开 SSMS手工执行三条查询确认数据、中文、联表都没问题-- 验证学生登录 SELECT StudentId, StudentName FROM dbo.Student WHERE StudentId 20230001 AND Pwd 123456; -- 验证选课联表查询 SELECT c.CourseId, c.CourseName, sc.Score FROM dbo.SC sc JOIN dbo.Course c ON sc.CourseId c.CourseId WHERE sc.StudentId 20230001; -- 验证课程容量 SELECT CourseId, CourseName, Capacity, Selected FROM dbo.Course WHERE CourseId CS101;这三条语句分别对应登录、成绩查询、课程列表三个核心功能的 SQL 骨架。能在 SSMS 里跑通再去写 C# 代码排错时会轻松很多——至少你知道问题不在 SQL 而在连接或参数传递上。3. 用C#连上SQL Server连接字符串、通用数据访问层与登录角色3.1 连接字符串的两种写法连实例与连mdf文件的取舍C# 连 SQL Server 最常用的是 SqlClient也就是 System.Data.SqlClient 命名空间。连接字符串有两类典型写法一种是连本机或远程的 SQL Server 实例另一种是用 AttachDbFilename 直接附加 .mdf 文件。许多开发者在本地用第二种写法跑得很顺换台机器就因为权限或路径问题彻底连不上。我一般建议课设直接使用实例连接部署时只要目标机器装了 SQL Server 并且创建了同名数据库即可。附加 .mdf 的方式只适合演示时临时用不适合作为交付方案。using System.Data; using System.Data.SqlClient; public static class DbHelper { // 1) 实例连接推荐Server 可写成 localhost 或你的服务器名 // private static readonly string ConnStr // Serverlocalhost;DatabaseCourseDB;User Idsa;Password123456;; // 2) 附加mdf配合 |DataDirectory| 使用打包后容易出问题 // private static readonly string ConnStr // Serverlocalhost\SQLEXPRESS;AttachDbFilename|DataDirectory|\CourseDB.mdf; // Integrated Securitytrue;User Instancetrue;; // 实际建议从 App.config 读取部署时改配置即可 public static string GetConnectionString() { return System.Configuration.ConfigurationManager .ConnectionStrings[CourseDB].ConnectionString; } // 执行增删改返回受影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] pars) { using (var conn new SqlConnection(GetConnectionString())) using (var cmd new SqlCommand(sql, conn)) { if (pars ! null) cmd.Parameters.AddRange(pars); conn.Open(); return cmd.ExecuteNonQuery(); } } // 执行查询返回 DataTable可直接绑定 DataGridView public static DataTable ExecuteQuery(string sql, params SqlParameter[] pars) { using (var conn new SqlConnection(GetConnectionString())) using (var cmd new SqlCommand(sql, conn)) using (var da new SqlDataAdapter(cmd)) { if (pars ! null) cmd.Parameters.AddRange(pars); var dt new DataTable(); da.Fill(dt); return dt; } } }逻辑说明连接字符串放在 App.config 里部署时用记事本改 Server、User Id、Password 即可不需要重新编译。ExecuteNonQuery 负责 INSERT、UPDATE、DELETEExecuteQuery 负责 SELECT 并返回 DataTableUI 层拿到 DataTable 后直接设置 DataGridView.DataSource 就能显示。using 块保证 SqlConnection 用完即释放避免连接泄漏。参数说明这里所有方法都接受params SqlParameter[]强制调用方用参数化方式拼 SQL。选课、登录、成绩录入这些操作一旦用字符串拼接输入单引号或分号就可能导致注入或语法错误。答辩老师喜欢盯着这个问务必养成参数化习惯。3.2 登录功能按角色分流到不同表登录是系统的门户。常见做法是学生、教师、管理员分表存放登录时依次查询命中哪个表就返回哪个角色然后由主窗体根据角色打开不同的子界面。public string Login(string userId, string pwd, out string role) { role ; // 查学生表 var dt DbHelper.ExecuteQuery( SELECT StudentName FROM dbo.Student WHERE StudentIdid AND Pwdpwd, new SqlParameter(id, userId), new SqlParameter(pwd, pwd)); if (dt.Rows.Count 0) { role student; return dt.Rows[0][0].ToString(); } // 查教师表假设已建好 Teacher 表字段同 Student dt DbHelper.ExecuteQuery( SELECT TeacherName FROM dbo.Teacher WHERE TeacherIdid AND Pwdpwd, new SqlParameter(id, userId), new SqlParameter(pwd, pwd)); if (dt.Rows.Count 0) { role teacher; return dt.Rows[0][0].ToString(); } // 查管理员表 dt DbHelper.ExecuteQuery( SELECT AdminName FROM dbo.Admin WHERE AdminIdid AND Pwdpwd, new SqlParameter(id, userId), new SqlParameter(pwd, pwd)); if (dt.Rows.Count 0) { role admin; return dt.Rows[0][0].ToString(); } return ; }逻辑说明登录时按顺序查三张表命中后返回显示名称和角色值。主窗体拿到 role 之后决定显示学生主界面还是教师主界面。参数化的 id 和 pwd 是底线不要写成WHERE StudentId userId 这是 C# 入门阶段最容易犯的错误之一。3.3 为什么不在连接字符串里写死sa密码项目交付时最尴尬的场面是数据库在另一台机器上起不来。如果 sa 密码写死在代码里换一台机器就得改代码重新编译如果连接字符串放在 App.config 里对方用记事本打开就能修改风险最低。集成安全Integrated SecuritySSPI在开发机上非常好用但部署到没有域环境、当前 Windows 账户没有被加入 SQL Server 登录项的机器上时会遇到登录失败。所以在部署文档里写清两种认证方式的切换步骤比帮对方远程调试更高效。开发者自己要知道的是用 sa 账号时必须先确认 SQL Server 的“混合认证模式”已开启否则就算密码正确也连不进去。4. 从登录到业务选课、退课、成绩录入与查询的C#实现4.1 学生端加载可选课程列表与我的成绩学生登录后的核心诉求很直接看看这学期能选哪些课、选了的课考了多少分。可选课程列表需要过滤掉“已满”的和“自己已经选过”的课程成绩查询则是简单的联表查询。public DataTable GetAvailableCourses(string studentId) { string sql SELECT c.CourseId, c.CourseName, c.Credit, c.Teacher, c.Capacity, c.Selected FROM dbo.Course c WHERE c.Selected c.Capacity AND c.CourseId NOT IN ( SELECT sc.CourseId FROM dbo.SC sc WHERE sc.StudentId sid ); return DbHelper.ExecuteQuery(sql, new SqlParameter(sid, studentId)); } public DataTable GetMyScores(string studentId) { string sql SELECT c.CourseName, c.Credit, sc.Score, c.Teacher FROM dbo.SC sc JOIN dbo.Course c ON sc.CourseId c.CourseId WHERE sc.StudentId sid; return DbHelper.ExecuteQuery(sql, new SqlParameter(sid, studentId)); }逻辑说明GetAvailableCourses 用Selected Capacity过滤掉满员课程用NOT IN过滤掉已选课程两个条件缺一不可。GetMyScores 返回的是成绩视图Score 为 NULL 表示还未出分。把这两段 SQL 拿到 SSMS 里验证过再贴进代码能省不少调试时间。4.2 选课与退课一个UPDATE加一个INSERT的事务边界选课的核心不是一个简单的 INSERT而是“先判断容量、再更新已选人数、最后插入选课记录”三步。这三步如果不用事务包起来两个学生同时抢最后一门课时就会出现超卖容量 60 的课第 61 个人也能选上。事务加条件更新是最稳的解法。public string SelectCourse(string studentId, string courseId) { using (var conn new SqlConnection(DbHelper.GetConnectionString())) { conn.Open(); using (var tx conn.BeginTransaction()) { try { // 第一步条件更新只有未满才成功 var cmd1 new SqlCommand( UPDATE dbo.Course SET Selected Selected 1 WHERE CourseId cid AND Selected Capacity, conn, tx); cmd1.Parameters.AddWithValue(cid, courseId); int rows cmd1.ExecuteNonQuery(); if (rows 0) { tx.Rollback(); return 该课程已满或不存在; } // 第二步插入选课记录 var cmd2 new SqlCommand( INSERT INTO dbo.SC (StudentId, CourseId) VALUES (sid, cid), conn, tx); cmd2.Parameters.AddWithValue(sid, studentId); cmd2.Parameters.AddWithValue(cid, courseId); cmd2.ExecuteNonQuery(); tx.Commit(); return 选课成功; } catch { tx.Rollback(); return 选课失败可能已选过该课程; } } } }逻辑说明这段代码的关键在于把 UPDATE 当成原子判断来用。UPDATE ... WHERE CourseId cid AND Selected Capacity执行后如果影响行数是 0说明容量已满直接回滚如果是 1说明当前连接成功占到了一个名额再插入选课记录。两个并发请求同时执行这条 UPDATE数据库行锁会保证只有一个成功。这比“先 SELECT 再 UPDATE”可靠得多。参数说明AddWithValue 在并发不高和字段类型明确时可以用但在索引列上有可能因为类型推断为 NVARCHAR(1) 而引发隐式转换。严格做法是new SqlParameter(cid, SqlDbType.NVarChar, 10)课设阶段不展开性能问题但要知道这个坑。4.3 教师端录入成绩与批量更新教师选择一门课程后界面显示选课学生列表教师可以直接在 DataGridView 里输入成绩然后点保存批量更新。更新逻辑要用事务包起来避免一半成功一半失败。public void SaveScores(DataTable dt) { using (var conn new SqlConnection(DbHelper.GetConnectionString())) { conn.Open(); using (var tx conn.BeginTransaction()) { try { var cmd new SqlCommand( UPDATE dbo.SC SET Score score WHERE StudentId sid AND CourseId cid, conn, tx); var p1 new SqlParameter(score, SqlDbType.Decimal); var p2 new SqlParameter(sid, SqlDbType.NVarChar, 20); var p3 new SqlParameter(cid, SqlDbType.NVarChar, 10); cmd.Parameters.AddRange(new[] { p1, p2, p3 }); foreach (DataRow row in dt.Rows) { p1.Value row[Score] DBNull.Value ? (object)DBNull.Value : row[Score]; p2.Value row[StudentId].ToString(); p3.Value row[CourseId].ToString(); cmd.ExecuteNonQuery(); } tx.Commit(); MessageBox.Show(成绩保存成功); } catch { tx.Rollback(); throw; } } } }逻辑说明这里没有用 SqlDataAdapter 的 Update 方法而是手动遍历 DataRow 逐条执行 UPDATE。这样事务边界最清晰哪一行失败都能整体回滚。选课系统的成绩录入量一般不大这种写法的性能完全够用。如果以后要支持几百人批量录入再改用临时表或 TableAdapter 也不迟。4.4 管理员端课程维护与选课人数超限管理员端的核心功能是课程维护新增课程、调整容量、查看每门课的选课名单。新增课程时Capacity 不能为 0 且应为正数调整容量时新容量不能小于当前已选人数否则会制造出Selected Capacity的脏数据。界面上要校验数据库里也要有 CHECK 约束兜底。if (newCapacity currentSelected) { MessageBox.Show(新容量不能小于当前已选人数); return; }这段逻辑放在界面上是第一道防线。配合 2.3 里的 CHECK 约束即使 C# 层漏检数据库也会在 UPDATE 时报错。管理员界面只需要简单调用 DbHelper.ExecuteNonQuery 执行 INSERT 或 UPDATE不需要额外复杂逻辑。5. 避坑与排查清单从连不上库到源码打包的6个典型问题5.1 连接字符串没问题却连不上先看SQL Server实例是否在运行现象代码在同学电脑上运行正常自己机器上一直报provider: Named Pipes Provider, error: 40 - Could not open a connection to SQL Server。 原因SQL Server 服务没有启动或者实例名写错。最常见的错误是把服务器名写成 localhost但本机实际安装的实例是.\SQLEXPRESS。 解决先打开“服务”管理器确认 SQL Server (SQLEXPRESS) 服务正在运行再用 SSMS 以localhost\SQLEXPRESS登录一次确认能进最后把连接字符串 Server 改成localhost\SQLEXPRESS。如果装的是默认实例Server 写localhost或.都可以。这条血泪经验排在第一位因为大多数连不上问题根本不是代码问题。5.2 集成认证在部署机上报登录失败现象开发机用Integrated SecurityTrue一切正常拷贝到另一台机器后报Cannot open database CourseDB requested by the login。 原因目标机器上的 SQL Server 没有为当前 Windows 账户创建登录名或者数据库权限没有授予给该账户。 解决在目标机器上用 sa 登录执行一条授权脚本CREATE LOGIN [MACHINE\Username] FROM WINDOWS; USE CourseDB; CREATE USER [MACHINE\Username] FOR LOGIN [MACHINE\Username]; EXEC sp_addrolemember db_owner, MACHINE\Username;如果不想折腾 Windows 账户就直接在连接字符串里改用 SQL Server 身份验证并保证 sa 登录已启用。两种方式二选一不要混合使用。5.3 中文乱码脚本文件编码与N前缀缺一不可现象执行建表脚本后表里的中文变成??。 原因SQL 脚本文件编码不是 UTF-8 with BOM且 INSERT 语句中的中文字符串没有加 N 前缀。SSMS 按 ANSI 代码页保存脚本时中文会被转换成乱码。 解决在 SSMS 里把脚本另存为“UTF-8 with signature”编码编写 INSERT 时凡是插入到 NVARCHAR 字段的中文都写成N中文。表结构设计阶段就把字符串列定为 NVARCHAR从根上避免编码问题。5.4 选课超卖先查再插一定翻车现象学生同时提交选课请求时实际上选人数超过课程容量。 原因用了“先 SELECT 判断容量、再 UPDATE、再 INSERT”的写法两个请求同时读到 Selected59都认为自己还有名额结果都执行了更新。 解决采用 4.2 里的条件更新写法用UPDATE ... WHERE Selected Capacity让数据库行锁仲裁同时在 Course 表上保留 CHECK 约束作为兜底。如果能接受存储过程方案直接使用存储过程实现同样逻辑。5.5 源码打包交付后数据库路径失效现象把整个项目文件夹拷给同学对方打开后连接数据库报找不到文件。 原因开发时用了AttachDbFilename|DataDirectory|\CourseDB.mdf写法DataDirectory 默认指向 bin\Debug 目录打包时 .mdf 文件没有被复制到输出目录或者目标机器上的 SQL Server Express 权限不足。 解决第一种做法是把 .mdf 文件属性设置为 Content Copy if newer确保生成时包含进输出目录第二种做法是改成实例连接把建库脚本一并放进 deliverables。我更建议用第二种因为验收老师通常希望看到你会用 SSMS 建库而不是只提供一个黑匣子文件。5.6 发布后的exe双击无反应现象部署到另一台机器后双击 exe 没有任何反应事件查看器提示版本或框架问题。 原因目标机器只安装了 .NET Framework 4.0而项目目标框架是 4.7.2 或更高。 解决在项目属性里把目标框架降到 4.5 或 4.0前提是你用到的 API 都兼容或者写明白交付文档说明需要安装对应版本的 .NET Framework。WinForms 项目目标框架选 4.5 是一个稳妥的兼容水位选课系统用到的 API 基本不会超出这个范围。6. 进阶验收存储过程、密码散列与演示时的数据准备6.1 把选课逻辑收进存储过程把 4.2 的事务代码搬进 SQL Server 存储过程代码会精简很多也方便在 SSMS 里直接演示CREATE PROCEDURE usp_SelectCourse sid NVARCHAR(20), cid NVARCHAR(10) AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; UPDATE dbo.Course SET Selected Selected 1 WHERE CourseId cid AND Selected Capacity; IF ROWCOUNT 0 BEGIN ROLLBACK; RETURN -1; -- 满员或课程不存在 END INSERT INTO dbo.SC (StudentId, CourseId) VALUES (sid, cid); COMMIT; RETURN 0; END逻辑说明存储过程的好处是业务规则集中在一处C# 端只需要调用存储过程名并传入两个参数。答辩时如果老师问“怎么防止超选”直接打开存储过程指给老师看比翻一整段 C# 事务代码更清晰。6.2 密码至少换成散列答辩时最容易被挑战的点是“数据库里存明文密码”。花一点时间用 SHA256 散列替换 Pwd 字段存储C# 端加密后再入库即可。开发阶段保留123456明文可以方便测试但交付前建议把存储逻辑改成散列版本至少在文档里说明这是待优化项。6.3 演示用的三组数据准备演示前务必在库里造好一组有完整生命周期的数据一个学生账号、三个班级名额分别剩 0、5、10 的课程以及一条已录入成绩的记录。这样演示时可以直接展示“选课成功后名额减少”“再次点选同一门课给出提示”“成绩查询显示已录分数”三个动作。数据的准备时间不超过十分钟但能让演示流程不冷场。我的经验是这类课设项目真正决定成败的不是代码量而是数据库在不在、能不能当场跑起来、有没有一组能讲出故事的数据。源码打包交付前我会逐一检查连接字符串的配置项、确认建库脚本放在压缩包里最显眼的位置再手动执行一遍建库和登录流程。这套习惯帮我避免过太多次现场翻车希望帮到你。本文还有配套的精品资源点击获取