ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C# WinForms学生选课系统源码解析:三层架构与SQL Server实战

C# WinForms学生选课系统源码解析:三层架构与SQL Server实战 简介这份资源是一套基于 C# Windows 窗体与 SQL Server 的学生信息管理系统源码采用三层架构设计适合正在学习桌面应用开发、数据库编程与选课业务逻辑的初学者和课程设计者参考。系统区分学生端与教师端两个登录入口覆盖管理员注册登录、学生信息增删改查、课程发布、学生自我信息上传以及在线选课等完整流程并涉及图片存储等实用技术点。压缩包共 141 个文件以 45 个 cs 源码文件、8 个 resx 资源文件、4 个 csproj 工程文件及若干 dll、config、exe 为主整体约 1.5MB源码、数据库建表代码与简要说明均已包含按文档生成数据库即可运行。目前已有 4416 人学习下载读者可借此理解三层架构分层方式、窗体间数据传递与选课逻辑实现并对照源码完成课程设计或二次开发。1. 从一份 C# Windows 窗体学生选课系统源码说起它到底能跑通什么如果你手头正好有一份 C# Windows 窗体学生信息管理系统选课源码还带着 SQL Server 数据库脚本那大概率是课程设计或者自学练手用的。这类资源网上一抓一大把但真正能一次跑通、结构清晰、数据库脚本齐全的并不多。这份资源的核心价值在于它把学生端和教师端两个登录入口、学生信息增删改查、课程发布、学生选课、图片存储这些功能都串起来了而且用的是三层架构数据库给了建表代码不用自己逆向猜表结构。适合谁正在做 C# 课程设计的学生、想拿一个完整 WinForms 项目练手增删改查和选课逻辑的初学者以及需要快速搭一个带数据库的桌面端 Demo 的开发者。它不涉及高并发、不涉及分布式就是一个典型的单机 SQL Server WinForms 组合但恰恰是这种组合能把 C# 连接数据库、窗体传值、三层分层这些基本功一次性踩透。2. 三层架构在 WinForms 里怎么落地从 Models 到 DAL 再到 BLL2.1 为什么这个项目值得按三层架构去读很多 WinForms 练手项目把所有逻辑塞在窗体后台代码里按钮一点SQL 语句直接写在 Click 事件里。这种写法跑起来没问题但一旦要改字段、换数据库、加一个业务规则就得满文件找 SQL。这份资源用了 StudentManager.Models、StudentManager.DAL、StudentManager.BLL 三个类库外加 Stu 主窗体项目说明作者至少有意愿把数据模型、数据访问、业务逻辑分开。你拿到源码后第一件事不是急着 F5 运行而是先看这三个类库的引用关系Stu 引用 BLL 和 ModelsBLL 引用 DAL 和 ModelsDAL 引用 Models。这个引用链不能乱乱了就是循环引用编译直接报错。常见做法是Models 里放实体类比如 Student、Course、SelectionDAL 里放 SqlHelper 和各个表的操作类BLL 里放业务判断比如选课之前先查这门课人数满了没有、学生有没有重复选。你读源码的时候重点看 BLL 里有没有做这些判断如果 BLL 只是透传 DAL那这个三层就只是形式上的三层业务逻辑还是漏到了窗体里。这一点直接决定你后面改功能时会不会翻车。2.2 实体类与数据库表字段的对应关系先看 Models 里的实体类。以学生为例通常会有 StudentId、StudentName、Gender、BirthDate、Phone、Photo 这些属性。Photo 字段就是摘要里提到的图片存储一般存的是图片的二进制数据或者图片路径。如果是二进制数据库字段类型是 varbinary(max)如果是路径就是 nvarchar(200) 左右。你拿到数据库脚本后先确认这一点因为后面窗体里 PictureBox 的赋值方式完全不同。// Models/Student.cs 实体类示例 public class Student { public string StudentId { get; set; } // 学号同时作为登录账号 public string StudentName { get; set; } // 姓名 public string Gender { get; set; } // 性别 public DateTime BirthDate { get; set; } // 出生日期 public string Phone { get; set; } // 联系电话 public byte[] Photo { get; set; } // 照片二进制对应 varbinary(max) public string Password { get; set; } // 登录密码 }这段代码的关键在于 Photo 的类型。如果是 byte[]说明图片直接存库如果是 string说明存的是路径。你打开数据库脚本找到 Student 表的 Photo 列看类型是 varbinary 还是 nvarchar就能反推实体类该怎么写。参数说明StudentId 既是主键又是登录账号所以注册和登录时都用它不要额外再搞一个自增 ID 当账号否则学生登录时还得先查 ID多一步没必要。2.3 DAL 层 SqlHelper 的封装与连接字符串配置DAL 层一般会有一个 SqlHelper 类封装 ExecuteNonQuery、ExecuteScalar、ExecuteDataTable 这几个方法。连接字符串通常写在 App.config 里而不是硬编码在代码里。你拿到源码后第一件事就是改 App.config 里的连接字符串指向你自己的 SQL Server 实例。!-- App.config 连接字符串配置 -- connectionStrings add nameStuConn connectionStringData Source.;Initial CatalogStudentDB;Integrated SecurityTrue providerNameSystem.Data.SqlClient/ /connectionStrings逻辑说明Data Source. 表示本机默认实例如果你装的是 SQL Server Express要改成 .\SQLEXPRESS。Initial Catalog 是数据库名Integrated SecurityTrue 表示用 Windows 身份验证如果你用 SQL Server 账号密码登录要改成 User IDsa;Password你的密码。改完这里再去看 SqlHelper 里读连接字符串的代码确认它读的是 nameStuConn 还是别的名字名字对不上运行时直接报“未将对象引用设置到对象的实例”这是血泪经验。2.4 BLL 层选课逻辑的边界判断选课功能是这份资源的重点。BLL 里应该有一个 SelectionManager 或者 CourseManager里面至少有两个方法一个是判断学生是否已经选过这门课另一个是判断课程容量是否已满。如果源码里没写这两个判断你运行时会发现同一个学生可以无限次选同一门课或者课程人数超了还能继续选。这不是玄学就是业务逻辑缺失。// BLL/SelectionManager.cs 选课前的双重校验 public bool CanSelect(string studentId, int courseId) { // 查是否已选 if (dal.IsSelected(studentId, courseId)) return false; // 查课程余量 int capacity dal.GetCourseCapacity(courseId); int selected dal.GetSelectedCount(courseId); return selected capacity; }参数说明studentId 是学号courseId 是课程编号。dal.IsSelected 返回 booldal.GetCourseCapacity 返回课程最大人数dal.GetSelectedCount 返回已选人数。只有两个条件都满足才返回 true。你如果要在窗体里调用记得先调 CanSelect再调 InsertSelection顺序反了就会出现选了但没校验的情况。3. 数据库脚本怎么生成从建表到初始数据的完整流程3.1 用 SQL Server Management Studio 执行建表脚本资源包里给了数据库代码通常是一个 .sql 文件。你打开 SQL Server Management Studio连接到你的实例新建一个查询窗口把脚本内容贴进去点执行。注意脚本开头一般会有 CREATE DATABASE StudentDB如果数据库已经存在会报错。稳妥做法是先手动建一个空库或者把脚本里的 CREATE DATABASE 改成 IF NOT EXISTS 判断。-- 建库如果不存在 IF NOT EXISTS (SELECT * FROM sys.databases WHERE name StudentDB) CREATE DATABASE StudentDB; GO USE StudentDB; GO -- 学生表 CREATE TABLE Student ( StudentId NVARCHAR(20) PRIMARY KEY, StudentName NVARCHAR(50) NOT NULL, Gender NVARCHAR(4), BirthDate DATE, Phone NVARCHAR(20), Photo VARBINARY(MAX), Password NVARCHAR(50) NOT NULL ); GO逻辑说明StudentId 设为主键同时作为登录账号所以不允许重复。Password 字段长度给 50 够用如果你要做加密存储可以改成 128 存哈希值。Photo 用 VARBINARY(MAX) 存二进制如果你打算存路径改成 NVARCHAR(200)。执行完建表脚本后再执行初始数据脚本一般会插入一个管理员账号和几门示例课程。3.2 教师端发布课程与学生端选课的表关联选课功能至少涉及三张表Student、Course、Selection。Course 表里有 CourseId、CourseName、TeacherId、Capacity、SelectedCount 等字段。Selection 表是中间表记录哪个学生选了哪门课通常有 StudentId、CourseId、SelectTime 三个字段联合主键防止重复选课。-- 课程表 CREATE TABLE Course ( CourseId INT IDENTITY(1,1) PRIMARY KEY, CourseName NVARCHAR(100) NOT NULL, TeacherId NVARCHAR(20) NOT NULL, Capacity INT DEFAULT 50, SelectedCount INT DEFAULT 0 ); GO -- 选课中间表 CREATE TABLE Selection ( StudentId NVARCHAR(20) NOT NULL, CourseId INT NOT NULL, SelectTime DATETIME DEFAULT GETDATE(), PRIMARY KEY (StudentId, CourseId), FOREIGN KEY (StudentId) REFERENCES Student(StudentId), FOREIGN KEY (CourseId) REFERENCES Course(CourseId) ); GO参数说明CourseId 用 IDENTITY 自增教师发布课程时不用手动填编号。Selection 表的联合主键 (StudentId, CourseId) 从数据库层面杜绝了重复选课但你在 BLL 里还是要做一次判断因为直接插数据库报主键冲突的异常信息对用户不友好。SelectedCount 字段是冗余设计每次选课成功后加一退课时减一这样查余量时不用 COUNT(*)速度快一点。3.3 图片存储的两种方案与字段类型选择摘要里提到图片存储这是很多 WinForms 项目的加分项。两种方案一是存二进制到 VARBINARY(MAX)二是存路径到 NVARCHAR。存二进制的好处是数据库备份时图片一起走不会出现“数据库恢复了但图片文件夹丢了”的情况坏处是数据库体积膨胀快图片大了查询变慢。存路径的好处是数据库轻坏处是迁移时要连图片文件夹一起拷。我一般会建议课程设计用二进制因为部署简单一个 .bak 文件全搞定。你在窗体里用 OpenFileDialog 选图片读成 byte[]然后赋值给 PictureBox.Image。读取时从数据库拿到 byte[]用 MemoryStream 转成 Image。// 图片转二进制存入数据库 byte[] photo File.ReadAllBytes(openFileDialog1.FileName); student.Photo photo; // 从数据库读取并显示 MemoryStream ms new MemoryStream(student.Photo); pictureBox1.Image Image.FromStream(ms);注意Image.FromStream 之后不要立即关闭 MemoryStream否则图片显示不出来这是 WinForms 里经典的坑。要么用 using 包住但延迟释放要么直接不关靠 GC 回收。4. 双端登录与窗体传值学生端和教师端怎么各走各的路4.1 登录窗体的角色判断与跳转逻辑系统有两个登录端口一个学生端一个教师端。常见做法是登录窗体上放一个 ComboBox 选角色或者两个 RadioButton。登录按钮点击后先根据角色查对应的表学生查 Student 表教师查 Teacher 表。查到了就打开对应的主窗体同时把当前登录的账号传过去。// 登录按钮逻辑 string role cmbRole.Text; // 学生 或 教师 string id txtId.Text.Trim(); string pwd txtPwd.Text.Trim(); if (role 学生) { Student stu studentBll.Login(id, pwd); if (stu ! null) { StudentMainForm frm new StudentMainForm(stu); frm.Show(); this.Hide(); } else MessageBox.Show(学号或密码错误); } else { Teacher tea teacherBll.Login(id, pwd); if (tea ! null) { TeacherMainForm frm new TeacherMainForm(tea); frm.Show(); this.Hide(); } else MessageBox.Show(工号或密码错误); }逻辑说明把 Student 或 Teacher 对象直接传给主窗体构造函数主窗体里就能拿到当前登录人的信息不用再查一次数据库。参数说明txtId 和 txtPwd 要做 Trim防止用户复制粘贴时带空格。this.Hide() 而不是 this.Close()因为 Close 会触发应用程序退出Hide 只是隐藏登录窗体。4.2 学生端主窗体的信息上传与选课入口学生登录后主窗体一般有“个人信息”“选课”“已选课程”几个按钮。个人信息页面可以修改电话、上传照片这就是摘要里说的“学生自我信息上传”。选课页面用一个 DataGridView 列出所有可选课程每行后面加一个“选课”按钮列。// 选课按钮点击 int courseId Convert.ToInt32(dgvCourses.CurrentRow.Cells[CourseId].Value); if (selectionBll.CanSelect(currentStudent.StudentId, courseId)) { selectionBll.SelectCourse(currentStudent.StudentId, courseId); MessageBox.Show(选课成功); RefreshCourseList(); // 重新加载课程列表刷新余量 } else { MessageBox.Show(已选过该课程或课程已满); }参数说明currentStudent 是从登录窗体传过来的 Student 对象。RefreshCourseList 方法重新查数据库并绑定 DataGridView这样 SelectedCount 变了能立刻看到。如果你不刷新用户会以为没选上又点一次然后报重复选课体验很差。4.3 教师端发布课程与查看选课名单教师端主窗体的核心功能是发布课程和查看某门课的选课名单。发布课程时填课程名、容量TeacherId 从当前登录教师对象里取。查看选课名单时用 CourseId 去 Selection 表关联 Student 表查出所有选了这门课的学生。-- 查看某门课的选课名单 SELECT s.StudentId, s.StudentName, s.Phone, sel.SelectTime FROM Selection sel INNER JOIN Student s ON sel.StudentId s.StudentId WHERE sel.CourseId CourseId ORDER BY sel.SelectTime;参数说明CourseId 是 SqlParameter不要用字符串拼接防止 SQL 注入。虽然课程设计里注入风险不大但养成参数化查询的习惯没坏处。ORDER BY SelectTime 让先选的人排在前面教师看起来直观。5. 避坑与排查这份源码跑不起来时先查这五条5.1 现象编译报错“找不到类型或命名空间”原因三个类库的引用关系没配好或者 .NET Framework 版本不一致。Stu 主项目引用了 BLL但 BLL 没引用 DAL或者 Models 被多个项目引用但版本不同。解决右键每个项目查看“目标框架”确保四个项目都是同一个版本比如都是 .NET Framework 4.7.2。然后检查引用Stu 引用 BLL 和 ModelsBLL 引用 DAL 和 ModelsDAL 引用 Models。缺哪个补哪个循环引用就删掉反向引用。5.2 现象运行时报“未将对象引用设置到对象的实例”原因连接字符串读不到SqlHelper 里的 ConfigurationManager.ConnectionStrings[StuConn] 返回 null。或者 App.config 里的 name 和代码里写的不一致。解决打开 App.config确认 connectionStrings 节点下有 add nameStuConn。再打开 SqlHelper看它读的是不是 StuConn。如果用的是 .NET Core 或 .NET 5ConfigurationManager 要装 System.Configuration.ConfigurationManager 包这一点和 .NET Framework 不同。5.3 现象登录时提示“登录失败”但数据库里明明有账号原因密码字段存的是明文但登录时做了 Trim 或者大小写转换导致匹配不上。或者学生表的 StudentId 是 NVARCHAR但登录时传的是 INT。解决在 SQL Server 里直接查 SELECT * FROM Student WHERE StudentId你的学号看能不能查到。查不到就是数据没插进去查到了但登录失败就在 C# 里下断点看 txtId.Text 和 txtPwd.Text 的实际值对比数据库里的值。常见的是密码前后有空格或者数据库里存的是加密后的哈希但登录时用明文比对。5.4 现象选课成功但课程余量不变原因SelectedCount 字段没有更新或者更新了但 DataGridView 没有重新绑定数据。解决在 SelectCourse 方法里插入 Selection 记录后执行 UPDATE Course SET SelectedCount SelectedCount 1 WHERE CourseId CourseId。然后在窗体里调用 RefreshCourseList 重新查一遍数据。如果用了 DataTable 缓存要清空再重新 Fill。5.5 现象图片上传后显示不出来或者报“参数无效”原因Image.FromStream 的流被提前关闭或者数据库里的 byte[] 是空值。解决读取图片时先判断 student.Photo ! null student.Photo.Length 0。然后用 MemoryStream 包住不要用 using 立即释放或者把 byte[] 复制到新流里再转 Image。如果还是不行检查数据库字段类型是不是 VARBINARY(MAX)如果是 IMAGE 类型虽然也能存但微软已经标记为弃用建议改成 VARBINARY(MAX)。6. 进阶技巧用事务保证选课数据一致性以及一个我常做的验证习惯选课这个动作表面上是插一条 Selection 记录实际上还涉及 Course 表的 SelectedCount 更新。如果插入成功但更新失败就会出现“学生选了课但余量没减”的脏数据。稳妥做法是把这两个操作放在一个事务里。// 用事务保证选课和余量更新要么都成功要么都回滚 using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); try { // 插入选课记录 string sql1 INSERT INTO Selection(StudentId, CourseId) VALUES(sid, cid); SqlCommand cmd1 new SqlCommand(sql1, conn, tran); cmd1.Parameters.AddWithValue(sid, studentId); cmd1.Parameters.AddWithValue(cid, courseId); cmd1.ExecuteNonQuery(); // 更新课程已选人数 string sql2 UPDATE Course SET SelectedCount SelectedCount 1 WHERE CourseId cid; SqlCommand cmd2 new SqlCommand(sql2, conn, tran); cmd2.Parameters.AddWithValue(cid, courseId); cmd2.ExecuteNonQuery(); tran.Commit(); return true; } catch { tran.Rollback(); return false; } }参数说明connStr 从 App.config 读tran 绑定到两个 SqlCommand 上任何一个 ExecuteNonQuery 抛异常catch 里回滚Selection 表和 Course 表都不会变。注意 SqlCommand 的构造函数要传 tran否则命令不在事务里执行回滚不起作用。除了事务我每次拿到这类源码都会先做一件事把数据库脚本在一个全新的 SQL Server 实例上跑一遍然后手动插入一条学生数据、一条课程数据再用源码里的登录功能去登看能不能登进去。这一步能同时验证连接字符串、表结构、密码字段、登录逻辑四个点。如果登不进去问题一定在这四个里面不用瞎猜。从那以后我每次拿到带数据库的 WinForms 项目都强制走一遍“空库重建 手动插数据 登录验证”的流程省得后面调半天才发现是脚本没跑全。希望帮到你。本文还有配套的精品资源点击获取
返回列表