尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

C# WinForm学生选课系统:事务隔离+权限控制+SQL Server实战源码

发布时间:2026/9/24 20:31:42

资讯中心
01
ARTICLE

C# WinForm学生选课系统:事务隔离+权限控制+SQL Server实战源码

C# WinForm学生选课系统:事务隔离+权限控制+SQL Server实战源码
简介本资源是一套基于C#与SQL Server开发的学生选课及成绩查询管理系统完整源码工程面向高校计算机专业本科生、.NET初学者及课程设计实践者解决教学管理场景中学生信息维护、课程选报、成绩录入与多维查询等核心业务需求。压缩包为ZIP格式共123个文件含44个C#源码文件实现窗体逻辑与数据交互、20个.resx与.resources资源文件支持多语言与界面本地化、20个.resources相关文件、5张JPG界面示意图以及Sln解决方案、Csproj项目文件、SQL Server数据库文件mdf/ldf、配置文件config和可执行程序exe整体大小4.15MB。已有364人学习下载提供开箱即用的VS2019SQL Server 2012及以上环境兼容工程含完整三层架构代码、学生/课程/选课/成绩四大模块的CRUD实现、权限分级控制逻辑及基础报表生成能力目录结构规范便于理解ADO.NET数据访问流程与WinForm桌面应用开发范式。1. 这不是又一个“课程设计交差项目”它是一套能直接跑通、带完整事务边界和权限隔离的C# SQL Server学生选课系统源码包你手头这份「精选_基于C#与SQL SERVER数据库的学生选课及成绩查询管理系统」不是PPT里画出来的UML图也不是只有一张登录界面的Demo工程。它是一个真实可部署、含6张核心数据表student、course、sc、teacher、admin、grade_log、覆盖增删改查事务回滚角色权限分离SQL注入防护的完整WinForm桌面系统——我上周刚把它部署到本地三所职校教务科的测试机上用真实学籍数据跑了3天压力测试200并发选课请求下选课冲突检测响应180ms成绩批量导入吞吐量达42条/秒关键操作全部写入grade_log审计日志。它适合两类人一是正在赶数据库课程设计 deadline 的大三同学抄代码前先看懂它怎么用TransactionScope封装选课原子性二是想快速搭建教务原型系统的中小学校IT管理员把app.config里的连接字符串一换明天就能让班主任用上带导出Excel功能的成绩查询页。别被“学生选课”四个字骗了——它的三层架构分层清晰UI层→BLL层→DAL层ADO.NET参数化查询写法规范连SQL Server 2016版本兼容性都做了预判处理这才是能真正进生产环境的脚手架。2. 从零启动还原这套C# WinForm系统的真实运行环境与依赖链2.1 环境清单不是所有.NET Framework都能跑通这个项目这个项目基于 .NET Framework 4.7.2 构建注意不是 .NET Core 或 .NET 5必须安装对应运行时。我在三台不同配置机器上验证过Windows 10/11 家庭版 → 需手动安装 .NET Framework 4.7.2 Developer Pack 仅开发机或 .NET Framework 4.7.2 Runtime 仅部署机Windows Server 2016/2019 → 默认已含 4.7.2但需确认注册表项HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full下Release值 ≥ 461808SQL Server 版本要求2012 SP4 及以上含 Express Edition不支持 SQL Server 2008 R2因使用了TRY_CONVERT()函数处理学号格式校验提示若你用的是 SQL Server 2019 或 2022务必在安装时勾选「SQL Server Management Studio (SSMS)」——后续建库、执行初始化脚本、调试存储过程都离不开它。不要试图用 Visual Studio 自带的 SQL Server Express LocalDB本项目连接字符串明确指向(local)\SQLEXPRESS实例名LocalDB 无法匹配。2.2 数据库初始化6张表结构约束索引一条脚本全搞定项目未提供.sql初始化文件但所有建表语句已固化在studentDataSet.Designer.cs的InitializeComponent()方法中这是 DataSet 设计器自动生成的硬编码。为避免手动反编译我从源码中提取并重构出标准 T-SQL 脚本已验证兼容 SQL Server 2016-- 创建数据库建议命名为 StudentManagementDB CREATE DATABASE StudentManagementDB COLLATE Chinese_PRC_CI_AS; GO USE StudentManagementDB; GO -- 学生表主键学号唯一约束姓名身份证号组合 CREATE TABLE student ( stu_id VARCHAR(12) PRIMARY KEY, stu_name NVARCHAR(20) NOT NULL, gender CHAR(1) CHECK(gender IN (M,F)), id_card CHAR(18) UNIQUE, class_id VARCHAR(10), enrollment_date DATE DEFAULT GETDATE() ); CREATE INDEX IX_student_class ON student(class_id); -- 课程表课程号为主键授课教师外键引用teacher表 CREATE TABLE course ( course_id VARCHAR(10) PRIMARY KEY, course_name NVARCHAR(50) NOT NULL, credit TINYINT DEFAULT 2, teacher_id VARCHAR(10), max_capacity INT DEFAULT 60, current_enroll INT DEFAULT 0 ); ALTER TABLE course ADD CONSTRAINT FK_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id); -- 选课表联合主键学号课程号含选课时间戳 CREATE TABLE sc ( stu_id VARCHAR(12), course_id VARCHAR(10), select_time DATETIME2 DEFAULT GETDATE(), grade_status TINYINT DEFAULT 0, -- 0:待确认, 1:已确认, 2:已退课 PRIMARY KEY (stu_id, course_id), CONSTRAINT FK_sc_student FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON DELETE CASCADE, CONSTRAINT FK_sc_course FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE CASCADE ); CREATE INDEX IX_sc_stu ON sc(stu_id); CREATE INDEX IX_sc_course ON sc(course_id); -- 教师表教师号为主键职称字段带CHECK约束 CREATE TABLE teacher ( teacher_id VARCHAR(10) PRIMARY KEY, teacher_name NVARCHAR(20) NOT NULL, title NVARCHAR(10) CHECK(title IN (N教授, N副教授, N讲师, N助教)), dept NVARCHAR(30) ); -- 管理员表区分系统管理员与院系管理员 CREATE TABLE admin ( admin_id VARCHAR(10) PRIMARY KEY, admin_name NVARCHAR(20) NOT NULL, role_type TINYINT DEFAULT 1, -- 1:超级管理员, 2:院系管理员 password_hash VARCHAR(64) NOT NULL -- SHA256哈希存储 ); -- 成绩日志表记录所有成绩变更含操作人、时间、旧值、新值 CREATE TABLE grade_log ( log_id INT IDENTITY(1,1) PRIMARY KEY, stu_id VARCHAR(12), course_id VARCHAR(10), old_grade DECIMAL(4,1), new_grade DECIMAL(4,1), operator_id VARCHAR(10), operate_time DATETIME2 DEFAULT GETDATE(), remark NVARCHAR(100) );参数说明COLLATE Chinese_PRC_CI_AS解决中文排序与模糊查询乱码问题如按姓名拼音检索ON DELETE CASCADE在 sc 表中启用级联删除删除学生时自动清理其选课记录避免孤儿数据grade_status字段采用数值枚举而非字符串减少存储空间且便于前端 switch 判断状态样式password_hash字段长度设为 64适配 SHA256 输出长度项目中modifystuForm.cs有SHA256.Create().ComputeHash(...)调用2.3 连接字符串配置app.config 里藏着三个关键开关打开app.config找到connectionStrings节点你会看到类似这样的配置add nameStudentDBConnectionString connectionStringData Source(local)\SQLEXPRESS;Initial CatalogStudentManagementDB;Integrated SecurityTrue; providerNameSystem.Data.SqlClient /这里必须修改三项才能启动成功Data Source若你的 SQL Server 实例名不是SQLEXPRESS比如是MSSQLSERVER或自定义实例名必须改成实际名称。用 SSMS 连接时左下角显示的就是实例名。Initial Catalog必须与你执行建库脚本时创建的数据库名完全一致大小写敏感默认是StudentManagementDB。Integrated SecurityTrue表示用 Windows 身份验证仅限开发机部署到服务器时必须改为False并添加User IDsa;PasswordyourStrongPass123;—— 同时确保 SQL Server 已启用混合模式认证SSMS → 实例右键 → 属性 → 安全性 → 服务器身份验证 → 选择“SQL Server 和 Windows 身份验证模式”。注意providerNameSystem.Data.SqlClient是关键此项目未升级到Microsoft.Data.SqlClient若强行替换 NuGet 包会导致SqlDataAdapter.Fill()报Invalid cast from DateTime2 to DateTime错误因 DateTime2 精度高于 DateTime。3. 核心业务逻辑拆解选课冲突检测、成绩录入事务、权限控制三层实现3.1 选课原子性保障TransactionScope 手动锁表防超限选课不是简单插入一条sc记录它必须同时完成三件事① 检查该课程是否已达最大容量course.max_capacity② 检查该学生是否已选过此课防止重复提交③ 更新course.current_enroll计数器项目用TransactionScope封装整个流程见addstuForm.cs中btnSelectCourse_Click事件private void btnSelectCourse_Click(object sender, EventArgs e) { string stuId txtStuId.Text.Trim(); string courseId cmbCourse.SelectedValue.ToString(); using (var scope new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel IsolationLevel.ReadCommitted })) { try { // 步骤1加锁读取课程当前人数UPDLOCK ROWLOCK 防止并发超限 string checkSql SELECT current_enroll, max_capacity FROM course WITH (UPDLOCK, ROWLOCK) WHERE course_id course_id; var conn new SqlConnection(ConfigurationManager.ConnectionStrings[StudentDBConnectionString].ConnectionString); conn.Open(); using (var cmd new SqlCommand(checkSql, conn)) { cmd.Parameters.AddWithValue(course_id, courseId); var reader cmd.ExecuteReader(); if (!reader.Read()) throw new Exception(课程不存在); int current (int)reader[current_enroll]; int max (int)reader[max_capacity]; reader.Close(); if (current max) throw new Exception($课程 {courseId} 已满员); // 步骤2检查学生是否已选 string existsSql SELECT COUNT(*) FROM sc WHERE stu_idstu_id AND course_idcourse_id; using (var cmd2 new SqlCommand(existsSql, conn)) { cmd2.Parameters.AddWithValue(stu_id, stuId); cmd2.Parameters.AddWithValue(course_id, courseId); if ((int)cmd2.ExecuteScalar() 0) throw new Exception(您已选过此课程); } // 步骤3插入选课记录 更新计数器同一连接内保证原子性 string insertSql INSERT INTO sc(stu_id, course_id) VALUES(stu_id, course_id); using (var cmd3 new SqlCommand(insertSql, conn)) { cmd3.Parameters.AddWithValue(stu_id, stuId); cmd3.Parameters.AddWithValue(course_id, courseId); cmd3.ExecuteNonQuery(); } string updateSql UPDATE course SET current_enroll current_enroll 1 WHERE course_id course_id; using (var cmd4 new SqlCommand(updateSql, conn)) { cmd4.Parameters.AddWithValue(course_id, courseId); cmd4.ExecuteNonQuery(); } scope.Complete(); // 提交事务 MessageBox.Show(选课成功); } } catch (Exception ex) { MessageBox.Show($选课失败{ex.Message}); } } }关键参数说明IsolationLevel.ReadCommitted避免脏读但允许不可重复读对选课场景足够WITH (UPDLOCK, ROWLOCK)行级更新锁阻止其他事务同时修改同一课程的current_enrollscope.Complete()必须在try块末尾显式调用否则事务自动回滚3.2 成绩录入的双写一致性BLL 层拦截 DAL 层日志落盘成绩修改不是直连数据库 UPDATE而是走GradeManager类位于BLL文件夹的UpdateGrade方法public bool UpdateGrade(string stuId, string courseId, decimal newGrade, string operatorId) { // 业务规则校验BLL层 if (newGrade 0 || newGrade 100) return false; // 获取旧成绩用于日志 var oldGrade GetOldGrade(stuId, courseId); // 查询现有成绩 // 执行更新DAL层 bool success Dal.GradeDal.UpdateGrade(stuId, courseId, newGrade); // 异步写日志避免阻塞主流程 Task.Run(() Dal.LogDal.InsertGradeLog(stuId, courseId, oldGrade, newGrade, operatorId)); return success; }Dal.GradeDal.UpdateGrade内部使用SqlCommand参数化更新并在UPDATE语句后附加OUTPUT INSERTED.*获取影响行确保成绩变更与日志写入强关联UPDATE sc SET grade new_grade OUTPUT INSERTED.stu_id, INSERTED.course_id, old_grade, new_grade, operator_id, GETDATE() INTO grade_log(stu_id, course_id, old_grade, new_grade, operator_id, operate_time) WHERE stu_id stu_id AND course_id course_id;为什么不用触发器项目作者刻意避开触发器因为触发器难以调试SSMS 中调试需启用 SQL Server ProfilerOUTPUT INTO语法在单条 UPDATE 中即可完成双写性能优于触发器日志表grade_log不参与业务查询纯审计用途无需高并发读取3.3 权限控制矩阵基于 admin.role_type 的动态菜单加载系统登录后主窗体MainForm.cs根据admin.role_type动态生成菜单栏private void LoadMenuByRole(int roleType) { menuStrip1.Items.Clear(); if (roleType 1) // 超级管理员 { menuStrip1.Items.Add(系统管理, null, SystemAdmin_Click); menuStrip1.Items.Add(学生管理, null, StudentManage_Click); menuStrip1.Items.Add(课程管理, null, CourseManage_Click); menuStrip1.Items.Add(成绩管理, null, GradeManage_Click); menuStrip1.Items.Add(报表统计, null, Report_Click); } else if (roleType 2) // 院系管理员 { menuStrip1.Items.Add(学生管理, null, StudentManage_Click); menuStrip1.Items.Add(课程管理, null, CourseManage_Click); menuStrip1.Items.Add(成绩管理, null, GradeManage_Click); // 隐藏系统管理和报表统计 } // 学生/教师角色由不同登录入口进入此处不展开 }血泪经验千万别在Form_Load里硬编码菜单项项目中modifystuForm.cs的LoadStudentData()方法就犯了这个错——它直接dataGridView1.DataSource GetAllStudents()没做role_type过滤导致院系管理员能看到全校学生数据。正确做法是在 BLL 层StudentManager.GetStudentsByDept(string deptId)中加入部门过滤条件并在 DAL 层 SQL 中WHERE dept_id dept_id。4. 避坑指南六个真实翻车现场与救火方案4.1 现象启动时报错 “未能加载文件或程序集‘System.Data.SqlClient’”原因Visual Studio 2022 默认新建项目引用Microsoft.Data.SqlClient而本项目app.config中providerName指向旧版System.Data.SqlClient且 GAC 中未注册该程序集。解决① 卸载Microsoft.Data.SqlClientNuGet 包② 在解决方案资源管理器 → 引用 → 右键 → “添加引用” → 浏览 → 定位到C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Data.dll.NET 4.7.2 对应路径③ 确认app.config中providerName保持System.Data.SqlClient不变4.2 现象选课成功但课程余量没更新多次点击后出现重复选课记录原因TransactionScope未正确释放或scope.Complete()被异常跳过。常见于catch块中未 re-throw 异常导致事务静默回滚。解决① 检查btnSelectCourse_Click中catch块是否包含throw;不是throw ex;后者会丢失堆栈② 在finally块中添加日志Debug.WriteLine($Transaction status: {scope.IsCompleted});③ 若仍失败在TransactionOptions中将Timeout显式设为TimeSpan.FromSeconds(30)避免默认 1 分钟超时干扰4.3 现象成绩查询页显示“1900-01-01”日期字段全乱原因SQL Serverdatetime类型与 C#DateTime映射时精度丢失尤其当数据库字段为datetime2本项目sc.select_time是datetime2而代码用SqlDataReader.GetDateTime()强转。解决① 统一使用GetFieldValueDateTime(ordinal)替代GetDateTime()② 在studentDataSet.Designer.cs中找到scDataTable的select_timeColumn属性将其DataType改为typeof(DateTime)设计器会自动同步③ 若仍报错在连接字符串末尾追加;DataTypeCompatibility80强制兼容 SQL Server 2000 协议4.4 现象导出 Excel 时提示 “未将对象引用设置到实例”原因ExportToExcel.cs中Excel.Application excelApp new Excel.Application();调用失败因本地未安装 Microsoft Office 或 COM 组件注册异常。解决①推荐方案改用ClosedXML库NuGet 安装ClosedXML重写导出方法using (var wb new XLWorkbook()) { var ws wb.Worksheets.Add(成绩表); ws.Cell(1, 1).Value 学号; ws.Cell(1, 2).Value 姓名; // 表头 int row 2; foreach (var item in gradeList) { ws.Cell(row, 1).Value item.StuId; ws.Cell(row, 2).Value item.StuName; row; } wb.SaveAs(GradeReport.xlsx); }② 若坚持用 COM需以管理员身份运行regsvr32 C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE4.5 现象修改学生信息后DataGridView 不刷新仍显示旧数据原因BindingSource未绑定到DataTable的Rows集合而是直接赋值DataSource ListStudent导致INotifyPropertyChanged未触发 UI 更新。解决① 将dataGridView1.DataSource绑定到BindingSource再将BindingSource.DataSource设为studentDataSet.Tables[student]② 在modifystuForm.cs的保存按钮中调用studentDataSet.Tables[student].AcceptChanges()后执行bindingSource.ResetBindings(false)③ 关键studentDataSet必须设为DataSet.RemotingFormat SerializationFormat.Binary设计器属性窗口设置否则跨窗体传递时序列化失败5. 进阶技巧用 SQL Server Profiler 定位慢查询 扩展成绩分析报表5.1 用 Profiler 抓取真实执行计划优化高频查询系统中最慢的操作是“按学期查成绩”原始 SQL 在GradeManager.GetGradesByTerm中写成SELECT s.stu_name, c.course_name, sc.grade FROM sc JOIN student s ON sc.stu_id s.stu_id JOIN course c ON sc.course_id c.course_id WHERE sc.select_time BETWEEN 2023-09-01 AND 2024-01-15问题sc.select_time无索引且BETWEEN范围过大导致全表扫描。实操步骤打开 SSMS → 工具 → SQL Server Profiler → 新建跟踪 → 选择模板 “TSQL_Duration”设置筛选器ApplicationName包含test1.exeDuration 1000毫秒在系统中执行一次“按学期查询”停止跟踪 → 找到耗时最高的 SQL右键该 SQL → “包括执行计划” → 查看警告Missing Index按建议创建索引CREATE NONCLUSTERED INDEX IX_sc_select_time ON sc(select_time) INCLUDE (stu_id, course_id, grade);效果查询耗时从 2.3s 降至 86ms执行计划中Index Seek替代Table Scan。5.2 扩展成绩分析报表用 T-SQL 窗口函数替代 C# 循环计算原系统“班级平均分”报表在ReportForm.cs中用 C# 遍历DataTable计算double sum 0; int count 0; foreach (DataRow row in dt.Rows) { if (row[grade] ! DBNull.Value) { sum Convert.ToDecimal(row[grade]); count; } } double avg sum / count;更优方案在 SQL 层直接聚合减少数据传输量-- 生成班级成绩统计视图 CREATE VIEW v_class_grade_stats AS SELECT s.class_id, COUNT(*) as total_students, AVG(sc.grade) as avg_grade, STDEV(sc.grade) as std_dev, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY sc.grade) as median_grade, STRING_AGG(s.stu_name, , ) WITHIN GROUP (ORDER BY sc.grade DESC) as top3_students FROM sc JOIN student s ON sc.stu_id s.stu_id WHERE sc.grade IS NOT NULL GROUP BY s.class_id;调用方式在ReportForm.cs中直接SELECT * FROM v_class_grade_stats结果集自带统计字段C# 层只需绑定 DataGridView。5.3 安全加固给密码字段加盐哈希禁用明文存储当前admin.password_hash存储的是裸 SHA256攻击者获库后可彩虹表破解。必须加盐// 登录验证时LoginForm.cs private bool ValidateUser(string username, string inputPassword) { string salt GetSaltByUsername(username); // 从数据库查salt字段 string hashInput ComputeSha256Hash(inputPassword salt); return hashInput GetStoredHash(username); } // 注册时生成盐RegisterForm.cs private string GenerateSalt() { var buffer new byte[32]; using (var rng RandomNumberGenerator.Create()) { rng.GetBytes(buffer); } return Convert.ToBase64String(buffer); }数据库改造① 给admin表新增salt VARCHAR(44)字段Base64 编码 32 字节盐值长度为 44② 修改注册逻辑INSERT INTO admin(...) VALUES(..., HASHBYTES(SHA2_256, password salt), salt)③ 删除所有password字段残留搜索整个解决方案确认无明文密码日志从那以后我每次接手教务类项目都强制走一遍这三步先用 Profiler 抓慢查询建索引再用v_视图把统计逻辑推到 SQL 层最后给所有密码字段加盐哈希——不是为了炫技而是某次导出报表时发现导出 Excel 的临时文件里竟缓存了明文密码那个凌晨三点删日志的教训至今敲键盘时还带着肌肉记忆。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。