☰
C# WinForms学生信息管理系统(选课)实战:数据库设计与事务处理
2026/10/4 3:29:44 网站建设 项目流程

简介:这是一份基于C#和SQL Server的学生信息管理系统(含选课功能)完整源码包,面向需要完成课程设计、毕业设计或练习三层架构开发的计算机专业学生;系统设计学生端与教师端两个登录入口。它覆盖学生信息增删查改、课程发布、学生选课、图片存储等核心业务,并附带数据库表结构代码与简要说明,便于快速生成数据库并运行项目。压缩包共141个文件,约1.5MB,其中以C#源码文件、动态链接库、调试符号文件、界面资源文件及图片为主,并附有解决方案文件和说明文档,可用Visual Studio直接打开运行。当前已有4416人学习下载。透过完整源码可直观理解三层架构中界面层、业务逻辑层与数据访问层的拆分方式,掌握SQL Server表结构设计与图片存储实现技巧,是练习C#数据库应用开发的实用参考。

1. 学生信息管理系统(选课)为什么是课设常青树:它到底解决了什么

每学期期末,C# windows窗体学生信息管理系统(选课)这种含数据库的课设题都会准时出现在各种学习群里。它不炫技,但特别能考验基本功:一张窗体连库、一个表格绑数据、一个按钮做增删改查,再套一个带外键的选课流程,把程序员日常要干的活浓缩成了一个完整项目。做它的意义不在界面多漂亮,而在你把「数据从哪里来、存到哪里去、怎么保证不坏」这条链路跑通了。它适合三类人:正在准备课程设计的在校生、想用 C# 练手但不知道做什么的入门开发者、以及需要快速交付一个小工具的职场新人。这篇文章从建库讲到选课事务,每段代码都能直接抄,边抄边理解参数为什么这么写。

2. 先设计数据库:学生、课程、选课三张表的关系与 SQL 脚本

管理系统第一步不是写窗体,是设计表结构。很多新手上来就拖控件,等写到选课的时候发现学生想选多门课程,字段根本存不下,整张表推倒重来。学生信息管理系统(选课)的数据模型很固定:一张学生表存基础信息,一张课程表存开课信息,再用一张中间表记录「谁选了哪门课」。学生和课程之间是多对多关系,必须靠中间表拆成两个一对多,否则选课数据没法扩展。

2.1 三张核心表的结构设计:字段、类型、主外键

先看字段怎么定。最容易踩坑的是学号:很多人会用 int 存,但学号像 20230101 这种,本身是编号不是数字,用整数存不仅存不了前导零,后面做模糊查询、导出 Excel 也处处别扭。所以学号用 varchar 或 nvarchar。姓名、班级必须用 nvarchar 才能正确存中文;性别一个字符,nchar(1) 就够;出生日期用 date 类型,代码里直接传 DateTime 即可;手机号也是 varchar,曾有同学把手机号存成 int,最后 138 开头的号码全部溢出报错。

课程表的课程号同样是 varchar,学分用 decimal(3,1),教师名 nvarchar(50),容量用 int。容量是选课业务的核心参数,判断「满没满」就靠它。选课中间表 Enrollment 的字段最少但最关键:学生 ID、课程 ID、选课时间、成绩。成绩允许为空,因为刚选上还没考试。

主键我有自己的习惯:每张表单独加一个自增 ID 做主键,学号、课程号只做唯一约束,不参与外键引用。业务号将来可能调整,自增 ID 永远不会变,外键引用更稳定。选课表的 (StudentId, CourseId) 必须建唯一约束,这是防止同一个人重复选同一门课的保险丝。

下面用选课表做例子,看完整字段设计:

字段类型约束说明
EnrollmentIdint identity主键自增 ID
StudentIdint外键 → Student学生 ID
CourseIdint外键 → Course课程 ID
EnrollDatedatetime默认值 GETDATE()选课时间
Scoredecimal(5,2)允许 NULL成绩,未录入为空

2.2 建库建表的完整 SQL 脚本:从零到可运行的 CREATE 语句

这段 SQL 直接粘到 SQL Server 的「新建查询」窗口执行。先建库,再建学生表和课程表,最后建选课表,因为选课表依赖前两张表的外键。脚本里加了唯一约束和默认值,这两处决定后面选课逻辑能少写多少代码。

CREATE DATABASE StudentCourseDB; GO USE StudentCourseDB; GO CREATE TABLE Student ( StudentId INT IDENTITY(1,1) PRIMARY KEY, StudentNo VARCHAR(20) NOT NULL UNIQUE, Name NVARCHAR(20) NOT NULL, Gender NCHAR(1) NOT NULL DEFAULT '男', BirthDate DATE NULL, ClassName NVARCHAR(50) NULL, Phone VARCHAR(20) NULL ); CREATE TABLE Course ( CourseId INT IDENTITY(1,1) PRIMARY KEY, CourseNo VARCHAR(20) NOT NULL UNIQUE, CourseName NVARCHAR(50) NOT NULL, Credit DECIMAL(3,1) NOT NULL DEFAULT 2.0, TeacherName NVARCHAR(20) NULL, Capacity INT NOT NULL DEFAULT 60 ); CREATE TABLE Enrollment ( EnrollmentId INT IDENTITY(1,1) PRIMARY KEY, StudentId INT NOT NULL FOREIGN KEY REFERENCES Student(StudentId), CourseId INT NOT NULL FOREIGN KEY REFERENCES Course(CourseId), EnrollDate DATETIME NOT NULL DEFAULT GETDATE(), Score DECIMAL(5,2) NULL, CONSTRAINT UQ_Student_Course UNIQUE (StudentId, CourseId) ); GO

逻辑说明:IDENTITY 是 SQL Server 的自增列,程序里不需要手动生成 ID;UNIQUE 约束保证学号、课程号不重复;选课表的唯一约束是数据库层的防重闸门,就算上层代码漏判断,重复插入也会被数据库拦下;外键约束保证删学生或删课程时,选课表不会残留孤儿数据。

参数说明:VARCHAR(20) 对 20 位以内的编号够用,NVARCHAR(50) 能覆盖中文课程名;DECIMAL(3,1) 表示三位整数一位小数,学分最大 999.9,够用;DECIMAL(5,2) 成绩最大 999.99,百分制绰绰有余;GETDATE() 是 SQL Server 当前时间函数。

2.3 预置测试数据:给系统喂第一批学生与课程

空表跑不起来,先插几行测试数据。选课表引用的是 StudentId 和 CourseId,也就是上面两张表自增出来的值,所以插入顺序必须先生成学生和课程。

INSERT INTO Student (StudentNo, Name, Gender, BirthDate, ClassName, Phone) VALUES ('20230101', N'张伟', '男', '2005-03-12', N'计算机2301', '13800000001'), ('20230102', N'李娜', '女', '2005-07-24', N'计算机2301', '13800000002'), ('20230103', N'王强', '男', '2004-11-02', N'软件2301', '13800000003'); INSERT INTO Course (CourseNo, CourseName, Credit, TeacherName, Capacity) VALUES ('C001', N'C#程序设计', 3.0, N'刘老师', 60), ('C002', N'数据库原理', 2.5, N'陈老师', 50), ('C003', N'数据结构', 4.0, N'赵老师', 40); INSERT INTO Enrollment (StudentId, CourseId, EnrollDate) VALUES (1, 1, '2026-03-01 09:00:00'), (1, 2, '2026-03-01 09:05:00'), (2, 1, '2026-03-01 09:10:00');

参数说明:所有中文字符串前加 N 前缀,SQL Server 才会按 Unicode 处理,不加在中文环境下偶尔会变成乱码;自增 ID 从 1 开始,所以选课表里 1 号学生对应张伟,2 号学生对应李娜,顺序不会错。这批数据覆盖了「一个学生选多门课、一门课被多个学生选」的情况,后面测试退课和成绩录入都方便。

为什么不用 Access 而用 SQL Server?Access 做课设也能跑,但并发能力和数据类型都比 SQL Server 弱,而且工作里问得多的也是 SQL Server 这套写法。SQL Server Express 免费,连接串写法更主流,学一次以后迁 MySQL 也有概念。这个系统里的 SELECT、INSERT、UPDATE、DELETE 四种数据库增删改查操作,在哪个数据库里思路都一样。

3. 用 WinForms 搭界面:DataGridView 绑定与增删改查核心代码

数据库准备完毕,开始写 C# windows窗体 这一侧。项目类型选「Windows 窗体应用」,.NET Framework 4.7.2 或 .NET 8 都行,下面代码按最通用的 .NET Framework 写法,两种环境都能跑。核心三件事:连接串配置、数据访问类、窗体事件绑定。

3.1 连接串配置与数据访问类:先写一个 SqlHelper

新建项目后的第一件事是把连接串写进 App.config,不要写死在代码里。这样换库、换电脑只改一行,老师检查项目文件时也显得规范。SQL Server 本机默认实例的连接串如下。

<connectionStrings> <add name="StudentDB" connectionString="Data Source=.;Initial Catalog=StudentCourseDB;Integrated Security=True;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>

逻辑说明:Data Source 写 "." 表示本机默认实例;如果你的 SQL Server 是命名实例,要写成 "本机名\SQLEXPRESS" 这种格式。Integrated Security=True 使用 Windows 身份登录,课设机器最省事。MultipleActiveResultSets=True 允许同一连接上同时打开多个结果集,后面下拉框联动、DataGridView 绑定时能少踩坑。

接着写数据访问类 SqlHelper。很多人一开始会用 SqlConnection 和 SqlCommand 在窗体里裸写,十几个按钮十几个连接,代码又长又容易漏关连接。封装一个公共类,三种场景各留一个方法,窗体里只写 SQL 和参数。

using System.Configuration; using System.Data; using System.Data.SqlClient; public class SqlHelper { public static string ConnString = ConfigurationManager.ConnectionStrings["StudentDB"].ConnectionString; public static DataTable ExecuteDataTable(string sql, SqlParameter[] parameters = null) { using (SqlConnection conn = new SqlConnection(ConnString)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); SqlDataAdapter adapter = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } } public static int ExecuteNonQuery(string sql, SqlParameter[] parameters = null) { using (SqlConnection conn = new SqlConnection(ConnString)) { conn.Open(); using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } } }

逻辑说明:ExecuteDataTable 内部用 SqlDataAdapter 一次把结果取回内存,用于 DataGridView 显示;ExecuteNonQuery 执行增删改,返回受影响行数,大于 0 表示有数据变化。两个方法都用 using 包住连接和命令,无论正常结束还是抛异常,连接都会自动归还连接池。

参数说明:如果用的是 .NET 6/8 的 WinForms,ConfigurationManager 不再默认引用,需要在 csproj 里加 PackageReference 到 System.Configuration.ConfigurationManager,或者改用 Microsoft.Extensions.Configuration 读配置。课设环境一般还是 VS 的 .NET Framework 模板,直接就能用。

3.2 学生信息管理窗体:DataGridView 加载与增删改查

学生管理界面是系统的门面。窗体上放一个 DataGridView,下面放学号、姓名、性别、班级等文本框和三个按钮。窗体加载时调用 LoadData。

private void FrmStudent_Load(object sender, EventArgs e) { LoadData(); } private void LoadData() { string sql = @"SELECT StudentId, StudentNo AS 学号, Name AS 姓名, Gender AS 性别, BirthDate AS 出生日期, ClassName AS 班级, Phone AS 手机号 FROM Student"; DataTable dt = SqlHelper.ExecuteDataTable(sql); dgvStudents.DataSource = dt; }

说明:SELECT 里直接写中文别名,DataGridView 表头自动显示中文,不用再在界面设计器里一列一列设 HeaderText。StudentId 列也查出来但没起别名,列头会显示 StudentId,正好用它做删除和修改的条件。

新增按钮。这里必须用参数化,一是防 SQL 注入,二是让日期、性别这类特殊值正确入库。

private void btnAdd_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtStudentNo.Text) || string.IsNullOrWhiteSpace(txtName.Text)) { MessageBox.Show("学号和姓名不能为空"); return; } string sql = @"INSERT INTO Student (StudentNo, Name, Gender, BirthDate, ClassName, Phone) VALUES (@no, @name, @gender, @birth, @class, @phone)"; SqlParameter[] ps = { new SqlParameter("@no", SqlDbType.VarChar, 20) { Value = txtStudentNo.Text.Trim() }, new SqlParameter("@name", SqlDbType.NVarChar, 20) { Value = txtName.Text.Trim() }, new SqlParameter("@gender", SqlDbType.NChar, 1) { Value = cmbGender.SelectedItem?.ToString() ?? "男" }, new SqlParameter("@birth", SqlDbType.Date) { Value = dtpBirth.Value.Date }, new SqlParameter("@class", SqlDbType.NVarChar, 50) { Value = txtClass.Text.Trim() }, new SqlParameter("@phone", SqlDbType.VarChar, 20) { Value = txtPhone.Text.Trim() } }; int rows = SqlHelper.ExecuteNonQuery(sql, ps); if (rows > 0) { MessageBox.Show("添加成功"); LoadData(); } }

参数说明:SqlParameter 显式指定类型和长度,与表结构一一对应;日期控件 DateTimePicker 的 Value 是 DateTime,直接传;性别下拉框做成 ComboBox,SelectedItem 可能是 null,用 ?.ToString() ?? "男" 兜底。Trim() 处理用户误输入的首尾空格。

修改按钮和新增几乎是同一套 SQL,只是把 WHERE StudentId = 当前选中行 ID 加上,UPDATE 语句里的参数一样。删除按钮要注意外键:这个学生已经有选课记录时,直接 DELETE 会撞上外键约束,捕获 SqlException 并把错误弹出来,比提前猜一个提示更准确。

private void btnDelete_Click(object sender, EventArgs e) { if (dgvStudents.CurrentRow == null) return; int studentId = Convert.ToInt32(dgvStudents.CurrentRow.Cells["StudentId"].Value); string sql = "DELETE FROM Student WHERE StudentId = @id"; try { int rows = SqlHelper.ExecuteNonQuery(sql, new SqlParameter("@id", SqlDbType.Int) { Value = studentId }); MessageBox.Show(rows > 0 ? "删除成功" : "删除失败"); LoadData(); } catch (SqlException ex) { MessageBox.Show("删除失败,可能是该学生已有选课记录:\n" + ex.Message); } }

说明:外键冲突时 SQL Server 抛 547 号错误,错误信息里会带表名和约束名,直接展示给用户,比笼统提示「有数据在使用」更容易定位。如果想允许删除并清理选课记录,可以加一条「先 DELETE FROM Enrollment WHERE StudentId=@id」再删学生,但建议把这一步写进同一个事务里。

3.3 课程管理窗体:课程列表与已选人数统计

课程管理窗体与学生窗体结构一样,区别是课程要展示「已选人数」,这样选课时能直观看到哪些课满了。这个统计用子查询就能实现。

string sql = @"SELECT c.CourseId, c.CourseNo AS 课程号, c.CourseName AS 课程名, c.Credit AS 学分, c.TeacherName AS 教师, c.Capacity AS 容量, (SELECT COUNT(*) FROM Enrollment e WHERE e.CourseId = c.CourseId) AS 已选人数 FROM Course c"; dgvCourses.DataSource = SqlHelper.ExecuteDataTable(sql);

说明:子查询每行执行一次,课设数据量下性能没有压力,代码却比 JOIN + GROUP BY 更直白。课程的新增和修改就用 NumericUpDown 控件输入容量和学分,避免用户在文本框里输 "2.5学分" 这种脏数据。到这里,学生、课程两张主表的增删改查全部完成,这一套窗体写法在 C# 的 WinForms 里可以复制给任何管理类项目。

4. 选课业务怎么写:事务、重复校验与成绩回填

选课是这个系统真正有含金量的地方。表面上看只是往 Enrollment 表插一行,但实际要处理「查容量、查重复、插记录」三个动作。三个动作之间如果有一步失败,数据库可能留下半条记录。解决办法是事务:要么全成功,要么全回滚。这一节把选课、退课、录成绩的代码走一遍。

4.1 选课提交:检查容量、重复校验与事务回滚

选课按钮的事件里,先拿到当前学生 ID 和课程 ID,再在事务里做三步操作。

private int GetCurrentStudentId() { if (dgvStudents.CurrentRow == null) return 0; return Convert.ToInt32(dgvStudents.CurrentRow.Cells["StudentId"].Value); } private void btnSelectCourse_Click(object sender, EventArgs e) { int studentId = GetCurrentStudentId(); int courseId = GetSelectedCourseId(); if (studentId <= 0 || courseId <= 0) { MessageBox.Show("请先选择学生和课程"); return; } using (SqlConnection conn = new SqlConnection(SqlHelper.ConnString)) { conn.Open(); SqlTransaction tx = conn.BeginTransaction(); try { string sqlCheck = @"SELECT c.Capacity, COUNT(e.EnrollmentId) FROM Course c LEFT JOIN Enrollment e ON e.CourseId = c.CourseId WHERE c.CourseId = @courseId GROUP BY c.Capacity"; SqlCommand cmd = new SqlCommand(sqlCheck, conn, tx); cmd.Parameters.AddWithValue("@courseId", courseId); SqlDataReader reader = cmd.ExecuteReader(); int capacity = 0, enrolled = 0; if (reader.Read()) { capacity = Convert.ToInt32(reader[0]); enrolled = Convert.ToInt32(reader[1]); } reader.Close(); if (enrolled >= capacity) { tx.Rollback(); MessageBox.Show("课程已满,选课失败"); return; } string sqlInsert = @"INSERT INTO Enrollment (StudentId, CourseId, EnrollDate) VALUES (@studentId, @courseId, GETDATE())"; SqlCommand cmdInsert = new SqlCommand(sqlInsert, conn, tx); cmdInsert.Parameters.AddWithValue("@studentId", studentId); cmdInsert.Parameters.AddWithValue("@courseId", courseId); cmdInsert.ExecuteNonQuery(); tx.Commit(); MessageBox.Show("选课成功"); LoadEnrollmentList(); } catch (SqlException ex) { tx.Rollback(); if (ex.Number == 2627 || ex.Number == 2601) MessageBox.Show("你已经选过这门课了"); else MessageBox.Show("选课失败:" + ex.Message); } } }

代码逻辑按三段说明。第一,事务为什么不能像 SqlHelper 那样自动开闭?因为事务里的多条命令必须共用同一个 SqlConnection,中途不能关闭连接,所以这里手动创建连接、手动 BeginTransaction。Commit 代表事务内所有操作一起生效,Rollback 代表撤销到事务开始之前。

第二,LEFT JOIN + GROUP BY 的查询一次拿到容量和已选人数,不满足就直接回滚并返回。注意 reader 用完后要 Close,否则同一个连接上无法执行下一条命令,这是 ADO.NET 最常见的坑之一。

第三,插入语句执行后 Commit。如果数据库唯一约束被触发,SQL Server 抛 2601 或 2627 号错误,catch 里根据错误号给出友好提示。catch 之后 tx.Rollback() 保证连接不残留未提交的事务。选课界面上通常放两个 DataGridView,左侧学生列表、右侧课程列表,CurrentRow 选中的就是参数值来源,交互直观,代码量也小。

注意:不使用 MARS(MultipleActiveResultSets)时,一个连接上同时只能有一个 DataReader,读者必须在执行插入前调用 reader.Close(),否则会收到「已有打开的与此连接相关联的 DataReader」。

4.2 防止重复选课:数据库约束与代码双保险

代码里已经在事务中做了容量查询,为什么还要再强调重复校验?因为课设阶段虽然并发量几乎为零,但「先查后插」永远存在竞态窗口。假设老师同时打开两个窗口,给同一个学生选同一门课,两边同时查到「没选过」,两边同时插入,如果没有唯一约束,就出现两条重复记录。这个约束在建表脚本里已经加上了。

所以选课的三层防线是:代码里先查一遍(给用户友好提示)、数据库唯一约束(兜底防竞态)、catch 约束异常(把抛出的错误转成可读文案)。三层缺一不可。只做第一层,并发下数据会脏;只做第二层,异常信息直接甩英文给用户,体验不好。

还有种常见误用:不建唯一约束,用「SELECT COUNT(*) 查出来为 0 再 INSERT」。这在单用户界面上绝大部分时间没问题,但掩盖不了设计缺陷。你无法控制别人会不会多开一个窗体,更不能防止以后把系统部署到网络环境。数据库约束是最后一道锁,必须放在数据库里。

4.3 退课与成绩录入:事务里的一收一放

退课的 SQL 是 DELETE,但业务上应该先判断成绩是否已录入。如果老师已经给了分,学生不能随意退课,否则成绩单就缺了。这段逻辑同样放在事务里。

private void btnDropCourse_Click(object sender, EventArgs e) { int studentId = GetCurrentStudentId(); int courseId = GetSelectedCourseId(); using (SqlConnection conn = new SqlConnection(SqlHelper.ConnString)) { conn.Open(); SqlTransaction tx = conn.BeginTransaction(); try { string sqlCheck = "SELECT Score FROM Enrollment WHERE StudentId=@sid AND CourseId=@cid"; SqlCommand cmdCheck = new SqlCommand(sqlCheck, conn, tx); cmdCheck.Parameters.AddWithValue("@sid", studentId); cmdCheck.Parameters.AddWithValue("@cid", courseId); object score = cmdCheck.ExecuteScalar(); if (score != null && score != DBNull.Value) { tx.Rollback(); MessageBox.Show("该课程已录入成绩,不能退课"); return; } string sqlDelete = "DELETE FROM Enrollment WHERE StudentId=@sid AND CourseId=@cid"; SqlCommand cmdDelete = new SqlCommand(sqlDelete, conn, tx); cmdDelete.Parameters.AddWithValue("@sid", studentId); cmdDelete.Parameters.AddWithValue("@cid", courseId); cmdDelete.ExecuteNonQuery(); tx.Commit(); MessageBox.Show("退课成功"); } catch (Exception ex) { tx.Rollback(); MessageBox.Show("退课失败:" + ex.Message); } } }

逻辑说明:ExecuteScalar 返回第一行第一列的值,用来拿成绩字段。成绩为 NULL 说明还没考试,允许退课;成绩已录入则回滚并提示。退课成功后没有释放课程名额的显式操作,因为名额是通过 COUNT(Enrollment) 实时计算的,删除记录后名额自动释放,这也是为什么容量统计要用子查询而不是存一个已选数字段。

成绩录入的代码结构与退课几乎一致,只是把 DELETE 换成 UPDATE,并且校验 rows == 0 来提示「没有找到选课记录」。核心思路仍是事务包一层,方便以后继续加业务动作。

5. 避坑排查:从连接失败到并发冲突的排查清单

写这个系统翻车最多的不是大模块,而是小细节。界面搭得再漂亮,数据库连着连不上照样零分。以下几条是按出现频率排的坑,每一条都写清现象、原因和解决路径,写代码遇到相同的报错可以直接照着查。每年课设季这些问题都会被不同人问一遍,与其让每个人重新踩一遍,不如集中写下来。

5.1 程序启动报「建立连接时出错」,或窗体一直卡在加载

现象:编译通过,一运行就抛 SqlException「在与 SQL Server 建立连接时出错」,或者窗体 Load 事件里查数据的代码让窗口卡住十几秒。

原因:一般是三个原因之一。第一,SQL Server 服务根本没启动;第二,连接串 Data Source 写错,比如把 "." 写成本机 IP,但 SQL Server 只监听了 1433 端口以外的端口;第三,登录模式是混合认证,但连接串还是 Windows 集成认证,反过来也可能。

解决:按顺序排。先打开任务管理器或服务列表确认 SQL Server 服务在运行;再用 SQL Server Management Studio 用相同的账号密码试着连一次,能连上就证明服务器本身没问题;最后检查连接串。本机默认实例用 "." 或 "(local)",命名实例写成 "机器名\SQLEXPRESS",实例名不确定时在 SSMS 连接窗口左侧看服务器名。很多连接问题看着像玄学,实际就是这一行字符串的问题。

5.2 学号或手机号变成 int 后前导零丢失、数据溢出

现象:插入学号 020230101,库里变成 20230101;手机号 13812345678 插入报「转换溢出」。运行时把文本框里的数字 Convert.ToInt32,再塞进 WHERE 条件,查询结果为空。

原因:表结构里用了 INT 类型存编号类数据。学号、手机号、身份证都是编码,不是数值,不具备加减乘除意义。

解决:建表时一律用 VARCHAR。已经建错表的,用 ALTER TABLE 改列类型,再把丢掉的零补回来。

ALTER TABLE Student ALTER COLUMN StudentNo VARCHAR(20) NOT NULL; UPDATE Student SET StudentNo = '0' + StudentNo WHERE LEN(StudentNo) = 8;

代码侧也要改:新增和修改的 SqlParameter 用 SqlDbType.VarChar,别再把 Convert.ToInt32 用于学号。这个问题越早发现越好,改表结构时如果选课表已经引用,外键关联的字段类型也要一起改,越拖越难收。

5.3 DataGridView 表头显示英文列名,改列名总是被覆盖

现象:绑定 DataTable 后,DataGridView 显示 StudentNo、Name,不是「学号」「姓名」。在设计器里手动改了 HeaderText,一执行 LoadData 又被覆盖回英文。

原因:DataGridView 的 AutoGenerateColumns 为 true 时,每次绑定 DataSource 都按结果集列名重建列。设计器里改的 HeaderText,在代码重新赋值 DataSource 后全部丢失。

解决:最省事的是让 SQL 里给字段起中文别名。如果不想动 SQL,就用代码在绑定后循环设置 HeaderText。

dgvStudents.DataSource = dt; dgvStudents.Columns["StudentNo"].HeaderText = "学号"; dgvStudents.Columns["Name"].HeaderText = "姓名";

我一般推荐前者,SQL 别名一次写好,窗体代码干净,HeaderText 还不会和绑定时机打架。

5.4 选课按钮双击,插入两条重复记录

现象:学生选课时手快点了两下按钮,Enrollment 里多了两条相同 StudentId + CourseId 的记录。有时弹了两个「选课成功」。

原因:Click 事件执行期间按钮仍可再次点击。第一次点击的事务未提交时,第二次点击的容量查询和重复校验都看不到未提交数据,于是顺利插入。这也是并发竞态在单机上的体现。

解决:两件事一起做。第一,进入事件立刻禁用按钮,提交完成后再恢复;第二,依赖数据库唯一约束兜底,真出现并发也只能插入一条,SQL Server 会抛唯一键冲突,catch 里提示「你已经选过这门课」。

btnSelectCourse.Enabled = false; try { SelectCourseCore(); } finally { btnSelectCourse.Enabled = true; }

5.5 数据库文件被占用,备份或拷 U 盘失败

现象:项目答辩前想把 StudentCourseDB.mdf 拷到 U 盘,系统提示文件被另一程序使用,拷不走。

原因:SQL Server 服务进程一直持有 mdf 文件的句柄,直接复制文件当然被锁。

解决:用 SQL 语句备份成 .bak 文件,再拷贝 .bak 是最稳的。

BACKUP DATABASE StudentCourseDB TO DISK = 'D:\StudentCourseDB.bak' WITH FORMAT;

想从代码里触发备份时也一样,执行这句 SQL 就行,不要用 File.Copy 去复制正在使用的 mdf。这和 C# 里常见的「强制关闭被其他程序占用的文件」思路不同,数据库文件不该靠关闭句柄解决,而应该用数据库自己的备份通道。

5.6 连接池耗尽,程序越跑越慢直到报错

现象:程序运行一段时间后,操作速度明显变慢,最后报「超时时间已到」,错误信息里提到连接池。

原因:某个查询方法里 SqlConnection 没有 Dispose,连接没有归还连接池,池被占满。最常见于事件里写了 conn.Open() 却忘了关闭,或者用了 DataReader 却没有及时 Close。

解决:最简单有效的方法就是全文统一用 using 管理连接,SqlHelper 里的写法本身就是标准,不要在窗体里另写裸连接。排查时在 SQL Server 的 sys.dm_exec_connections 视图里能看到大量来自本机的睡眠连接,指向哪个进程基本就是哪段代码漏关了。

6. 课设加分技巧:搜索、分页与导出 Excel

这一章给三个立刻能用的小功能。它们不需要你重构任何东西,加在现有窗体上就能让作业质量明显提升,答辩时也容易多聊几句。

6.1 关键字搜索:参数化 LIKE 的写法

学生管理窗体加一个文本框和查询按钮,按学号或姓名模糊查最实用。

string keyword = txtSearch.Text.Trim(); string sql = @"SELECT StudentId, StudentNo AS 学号, Name AS 姓名, Gender AS 性别, ClassName AS 班级 FROM Student WHERE StudentNo LIKE @kw OR Name LIKE @kw"; SqlParameter p = new SqlParameter("@kw", SqlDbType.NVarChar, 50) { Value = "%" + keyword + "%" }; dgvStudents.DataSource = SqlHelper.ExecuteDataTable(sql, new SqlParameter[] { p });

说明:LIKE 的条件拼接在参数值里,SQL 语句保持参数化,不存在注入问题。空关键字时 % 匹配全部记录,正好当作「显示全部」按钮用。

6.2 导出一份选课名单:CSV 方案最省心

答辩老师最常问的是「选课名单能不能导出来」。用 Office 的 Interop.Excel 做导出,机器上必须装 Office,否则直接翻车。CSV 格式 Excel 能打开,不依赖任何组件,代码量也短。

using (SaveFileDialog dlg = new SaveFileDialog() { Filter = "CSV文件|*.csv", FileName = "选课名单.csv" }) { if (dlg.ShowDialog() != DialogResult.OK) return; StringBuilder sb = new StringBuilder(); DataTable dt = GetCourseReport(); // 返回选课名单 DataTable sb.AppendLine(string.Join(",", dt.Columns.Cast<DataColumn>().Select(c => c.ColumnName))); foreach (DataRow row in dt.Rows) sb.AppendLine(string.Join(",", row.ItemArray.Select(v => v?.ToString()))); File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8); MessageBox.Show("导出成功"); }

说明:两个坑要记住。第一,文件编码必须用 UTF8,否则 Excel 打开中文乱码;第二,如果字段内容里可能含逗号或换行,导出前要把内容用双引号包起来,否则列会错位。想省事可以只用中文别名当表头,课设场景基本够用。反过来,如果你要做 Excel 导入而不是导出,用 CSV 也比读 xlsx 稳定,避开 COM 组件这个黑匣子。

6.3 分页显示:数据量大了再考虑

如果不想把所有学生一次性加载,可以给查询加分页。SQL Server 2012 以后用 OFFSET-FETCH 最简单:

SELECT StudentId, StudentNo, Name, ClassName FROM Student ORDER BY StudentId OFFSET @pageIndex * @pageSize ROWS FETCH NEXT @pageSize ROWS ONLY;

C# 侧配合 NumericUpDown 控件做页码,把 @pageIndex 和 @pageSize 作为 SqlParameter 传入。这功能不是必须,但写上会让老师觉得你考虑过数据量增长的问题。

课设做多了你会发现,真正拉开差距的不是控件数量,而是边界处理:重复选课会不会崩、删有成绩的学生会不会误删、导出的文件会不会被占用。每次交之前我会把异常情况自己点一遍,这是吃过亏才养成的习惯。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询