实战:数据库设计与事务处理)
简介这是一份基于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主键自增 IDStudentIdint外键 → Student学生 IDCourseIdint外键 → Course课程 IDEnrollDatedatetime默认值 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 的自增列程序里不需要手动生成 IDUNIQUE 约束保证学号、课程号不重复选课表的唯一约束是数据库层的防重闸门就算上层代码漏判断重复插入也会被数据库拦下外键约束保证删学生或删课程时选课表不会残留孤儿数据。参数说明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, NC#程序设计, 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 ServerAccess 做课设也能跑但并发能力和数据类型都比 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 nameStudentDB connectionStringData Source.;Initial CatalogStudentCourseDB;Integrated SecurityTrue;MultipleActiveResultSetsTrue providerNameSystem.Data.SqlClient / /connectionStrings逻辑说明Data Source 写 . 表示本机默认实例如果你的 SQL Server 是命名实例要写成 本机名\SQLEXPRESS 这种格式。Integrated SecurityTrue 使用 Windows 身份登录课设机器最省事。MultipleActiveResultSetsTrue 允许同一连接上同时打开多个结果集后面下拉框联动、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 的 WinFormsConfigurationManager 不再默认引用需要在 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直接传性别下拉框做成 ComboBoxSelectedItem 可能是 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 StudentIdid」再删学生但建议把这一步写进同一个事务里。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 选中的就是参数值来源交互直观代码量也小。注意不使用 MARSMultipleActiveResultSets时一个连接上同时只能有一个 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 StudentIdsid AND CourseIdcid; 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 StudentIdsid AND CourseIdcid; 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.CastDataColumn().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 传入。这功能不是必须但写上会让老师觉得你考虑过数据量增长的问题。课设做多了你会发现真正拉开差距的不是控件数量而是边界处理重复选课会不会崩、删有成绩的学生会不会误删、导出的文件会不会被占用。每次交之前我会把异常情况自己点一遍这是吃过亏才养成的习惯。希望帮到你。本文还有配套的精品资源点击获取