简介:这份资源是一套基于C# Windows窗体开发的学生信息管理与选课系统,面向学习桌面应用开发、数据库编程与三层架构的初学者及课程设计需求者。系统采用SQL Server数据库,划分学生端与教师端两个登录入口,覆盖管理员注册登录、学生信息增删改查、课程发布、学生信息上传与在线选课等完整业务闭环,并涉及图片存储等实用技术点。压缩包共141个文件,约1.5MB,以cs源码、dll动态库、resx与resources资源文件、csproj工程文件及config配置为主,另附数据库表结构代码与简要说明,导入后即可生成数据库运行。目前已有4416人学习下载,适合作为课程设计、毕业设计或C#与SQL Server综合练习的参考方案,帮助读者理解三层架构分层方式、窗体间数据传递与选课逻辑的实现思路。
1. 从一份带选课的学生信息管理系统源码说起:C# Windows 窗体 + SQL Server 能跑出什么
很多做课程设计或者刚接手实验室管理系统的同学,都会在搜索框里敲下「C# Windows 窗体 学生信息管理系统 选课 数据库」这几个词。搜出来的东西要么是只有界面没有数据库的壳,要么是数据库脚本缺失、跑起来直接报连接错误。我这次拆的这份资源,是一个相对完整的 C# Windows Forms 项目,后端用 SQL Server,采用三层架构,功能覆盖了学生端和教师端两个登录入口,核心业务是学生信息管理和选课。它适合谁?适合正在做 C# 课程设计、需要一份能跑通「增删查改 + 选课逻辑 + 图片存储」的参考实现的人,也适合想看看三层架构在 WinForms 里怎么落地的新手。资源包里给了源码、数据库建表代码和一份简要说明,数据库表结构是直接给的,不需要你从零设计。接下来我会按「这是什么 → 怎么跑起来 → 坑在哪 → 怎么改」的顺序,把这份资源拆开讲清楚。
2. 三层架构在 WinForms 里怎么落地:从 Models 到 DAL 再到 BLL
2.1 为什么这个项目要用三层架构,而不是把 SQL 写在按钮事件里
很多 WinForms 入门项目会把SqlConnection、SqlCommand直接写在按钮的Click事件里,改一个字段要翻遍所有窗体。这份资源用了三层架构,目录里能看到StudentManager.Models、StudentManager.DAL、StudentManager.BLL三个类库项目,加上主项目Stu。分层的好处很直接:Models 放实体类,DAL 只负责和数据库打交道,BLL 写业务规则,窗体只负责拿数据展示和收集用户输入。选课这种有业务规则的操作——比如同一门课不能重复选、选课人数上限——放在 BLL 里比散落在窗体事件里好维护得多。常见做法是 DAL 里每个实体一个类,比如StudentDAL、CourseDAL、EnrollmentDAL,BLL 里对应StudentBLL、CourseBLL,窗体通过 BLL 调 DAL,不直接碰数据库连接。
2.2 实体类与数据库表的映射关系
从摘要描述看,系统涉及学生信息、课程信息、选课记录,还有登录账号。学生登录账号直接用学号,这意味着学生表里学号字段既是主键也是登录凭证。图片存储说明学生表里有一个字段存照片,常见做法是存图片的二进制数据(varbinary(max))或者存图片路径。资源说明里提到「图片存储」,结合 WinForms 的PictureBox控件,大概率是存二进制。下面是一个典型的学生表结构,字段名可以根据实际脚本调整:
-- 学生表:学号为主键,同时作为登录账号 CREATE TABLE Student ( StudentNo VARCHAR(20) PRIMARY KEY, -- 学号,登录账号 StudentName NVARCHAR(50) NOT NULL, -- 姓名 Gender NVARCHAR(10), -- 性别 BirthDate DATE, -- 出生日期 Major NVARCHAR(50), -- 专业 Photo VARBINARY(MAX), -- 照片二进制 Password VARCHAR(50) NOT NULL -- 登录密码 ); -- 课程表:教师发布课程 CREATE TABLE Course ( CourseId INT IDENTITY(1,1) PRIMARY KEY, CourseName NVARCHAR(100) NOT NULL, TeacherName NVARCHAR(50), Credit DECIMAL(3,1), Capacity INT DEFAULT 50, -- 选课容量 Selected INT DEFAULT 0 -- 已选人数 ); -- 选课记录表:学生和课程的多对多关系 CREATE TABLE Enrollment ( Id INT IDENTITY(1,1) PRIMARY KEY, StudentNo VARCHAR(20) FOREIGN KEY REFERENCES Student(StudentNo), CourseId INT FOREIGN KEY REFERENCES Course(CourseId), SelectTime DATETIME DEFAULT GETDATE() );这段脚本的关键点在于:StudentNo既是主键又是外键关联字段,选课记录表通过StudentNo和CourseId把学生和课程连起来。Selected字段用来记录已选人数,选课时要在 BLL 里判断Selected < Capacity,否则就超选了。照片字段用VARBINARY(MAX)存二进制,读取时用byte[]接收再转成Image。
2.3 从登录到选课的完整调用链
以学生选课为例,调用链是这样的:学生在窗体上选中一门课,点击「选课」按钮 → 窗体调用EnrollmentBLL.SelectCourse(studentNo, courseId)→ BLL 先检查该学生是否已经选过这门课(查Enrollment表),再检查课程容量是否已满(查Course表的Selected和Capacity)→ 两个检查都通过后,调用EnrollmentDAL.InsertEnrollment()插入记录,同时调用CourseDAL.IncrementSelected()把已选人数加一。这两步操作应该放在同一个事务里,否则插入成功但人数没加,或者人数加了但插入失败,都会导致数据不一致。常见做法是在 DAL 里开SqlTransaction,把两条 SQL 包在一起提交。
// BLL 层选课逻辑示意 public class EnrollmentBLL { private EnrollmentDAL enrollDal = new EnrollmentDAL(); private CourseDAL courseDal = new CourseDAL(); public string SelectCourse(string studentNo, int courseId) { // 检查是否重复选课 if (enrollDal.Exists(studentNo, courseId)) return "你已经选过这门课了"; // 检查容量 Course course = courseDal.GetById(courseId); if (course.Selected >= course.Capacity) return "这门课已经选满了"; // 事务:插入选课记录 + 更新已选人数 return enrollDal.InsertWithTransaction(studentNo, courseId) ? "选课成功" : "选课失败,请重试"; } }这段代码里Exists方法查Enrollment表判断重复,GetById拿课程当前状态,InsertWithTransaction在 DAL 里用事务包住插入和更新。参数studentNo来自登录后保存的会话变量,courseId来自课程列表选中行的主键值。返回字符串直接给窗体弹MessageBox用,简单直接。
3. 把源码跑起来:数据库生成、连接字符串配置与登录入口切换
3.1 用给定的表结构代码生成数据库
资源包里给了数据库建表代码,不需要你手动设计表。打开 SQL Server Management Studio,新建一个数据库,名字随意,比如StuManager,然后在新查询窗口里执行资源包里的建表脚本。脚本执行完可以用SELECT * FROM Student验证表是否创建成功。注意脚本里如果有USE [数据库名]这样的语句,要改成你实际创建的数据库名,否则会报「数据库不存在」。如果脚本里包含初始数据插入语句,执行后学生表和课程表里会有测试数据,方便你直接登录测试。
3.2 修改 App.config 里的连接字符串
WinForms 项目的数据库连接字符串通常放在App.config里,DAL 层通过ConfigurationManager.ConnectionStrings读取。资源包里的连接字符串大概率是作者本机的配置,你需要改成自己的 SQL Server 实例名和数据库名。常见格式如下:
<connectionStrings> <!-- 把 Data Source 改成你的实例名,Initial Catalog 改成你的数据库名 --> <add name="StuConn" connectionString="Data Source=.;Initial Catalog=StuManager;Integrated Security=True" providerName="System.Data.SqlClient"/> </connectionStrings>Data Source=.表示本机默认实例,如果你用的是命名实例比如SQLEXPRESS,要写成Data Source=.\SQLEXPRESS。Integrated Security=True表示用 Windows 身份验证,如果你用 SQL Server 账号密码登录,改成User Id=sa;Password=你的密码。改完保存,重新生成解决方案。如果运行时提示「连接字符串未配置」或者「找不到名为 StuConn 的连接字符串」,检查App.config里的name属性和 DAL 里读取时用的名字是否一致。
3.3 两个登录端口的切换逻辑
系统设计了学生端和教师端两个登录入口。常见实现方式有两种:一种是在登录窗体上加一个下拉框或者单选按钮,选择「学生」或「教师」,登录时根据选择去不同的表验证;另一种是两个独立的登录按钮,分别进入不同的窗体。从摘要描述看,管理员(教师端)负责学生信息管理和课程发布,学生端负责选课和个人信息上传。登录验证的 SQL 大致是:
// 学生登录:学号 + 密码 string sql = "SELECT COUNT(*) FROM Student WHERE StudentNo=@no AND Password=@pwd"; // 教师登录:如果教师表独立,则查教师表;如果共用一张表,用角色字段区分 string sqlTeacher = "SELECT COUNT(*) FROM Teacher WHERE TeacherId=@no AND Password=@pwd";参数@no和@pwd来自文本框输入,用SqlParameter传参而不是字符串拼接,避免 SQL 注入。登录成功后把学号或教师 ID 存到一个静态变量或者Program类的全局字段里,后续窗体和 BLL 调用时直接取,不用反复传。
3.4 图片存储的读写操作
学生自我信息上传里包含照片上传,这是 WinForms 里比较典型的操作。读取图片用OpenFileDialog选文件,转成byte[]存进数据库;显示时从数据库读出byte[]转成Image绑到PictureBox。
// 把图片转成 byte[] 存入数据库 byte[] photoBytes = File.ReadAllBytes(openFileDialog.FileName); string sql = "UPDATE Student SET Photo=@photo WHERE StudentNo=@no"; using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.Add("@photo", SqlDbType.VarBinary).Value = photoBytes; cmd.Parameters.Add("@no", SqlDbType.VarChar).Value = studentNo; conn.Open(); cmd.ExecuteNonQuery(); } // 从数据库读出 byte[] 显示到 PictureBox byte[] data = (byte[])reader["Photo"]; using (MemoryStream ms = new MemoryStream(data)) { pictureBox1.Image = Image.FromStream(ms); }File.ReadAllBytes一次性读完文件,适合照片这种小文件。SqlDbType.VarBinary对应数据库的VARBINARY(MAX)。读的时候用MemoryStream包一下再Image.FromStream,注意MemoryStream用完要释放,否则图片文件会被锁定。如果照片字段允许为空,读之前要判断reader["Photo"] != DBNull.Value,否则Image.FromStream会抛异常。
4. 避坑与排查:连接失败、选课超员、图片读不出来的血泪经验
4.1 运行时报「在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误」
现象是程序一启动或者一登录就弹这个错误,堆栈指向SqlConnection.Open()。原因通常是连接字符串里的Data Source写错了,或者 SQL Server 的 TCP/IP 协议没启用。解决步骤:先确认 SQL Server 服务在运行,打开 SQL Server 配置管理器,检查「SQL Server 网络配置」下的 TCP/IP 是否已启用,如果没启用就启用并重启服务。然后把Data Source改成localhost或者127.0.0.1试试,命名实例要加\实例名。如果用的是 SQL 账号登录,确认账号没有被禁用,并且服务器身份验证模式是「SQL Server 和 Windows 身份验证模式」。
4.2 选课成功但已选人数没变,或者人数变了但选课记录没插入
现象是学生选了课,课程列表里已选人数还是 0,或者人数加了但学生自己的选课列表里没有这门课。原因是插入选课记录和更新已选人数这两步没有放在同一个事务里,其中一步失败了。解决方式是在 DAL 里用SqlTransaction把两条 SQL 包起来,任何一步ExecuteNonQuery返回 0 就回滚。代码结构参考 2.3 里的InsertWithTransaction,先BeginTransaction,两条命令都指定Transaction属性,都成功再Commit,捕获异常时Rollback。
4.3 学生照片上传后重新打开程序显示不出来
现象是上传时能看到照片,关掉程序再打开,PictureBox是空的或者报「参数无效」。原因通常是读取时没有判断DBNull,或者MemoryStream被提前释放了。解决方式:读之前先判断reader["Photo"] == DBNull.Value,如果是空就跳过;Image.FromStream之后不要立刻Dispose那个MemoryStream,因为Image对象可能还依赖流里的数据,常见做法是把MemoryStream的引用保持到图片不再使用,或者用new Bitmap(Image.FromStream(ms))复制一份再释放流。
4.4 教师端发布课程后学生端看不到新课程
现象是教师登录后发布了课程,数据库Course表里也有记录,但学生端课程列表刷新不出来。原因可能是学生端窗体在加载时只查了一次数据,没有在每次打开选课窗体时重新查询。解决方式是在选课窗体的Load事件或者「刷新」按钮里重新调CourseBLL.GetAll(),不要用缓存。另一个可能原因是查询条件里加了WHERE Selected < Capacity,如果新课程Selected默认是NULL而不是 0,条件就不成立。建表时给Selected设DEFAULT 0,插入课程时如果不显式赋值,数据库会自动填 0。
4.5 三层架构的引用关系搞反导致编译报错
现象是编译时提示「类型或命名空间不存在」,或者运行时提示「未能加载程序集」。原因是项目之间的引用关系乱了,比如 DAL 引用了 BLL,或者窗体项目直接引用了 DAL 而没引用 BLL。正确的依赖方向是:Stu(主项目)引用 BLL,BLL 引用 DAL 和 Models,DAL 引用 Models。Models 不引用任何其他项目。检查每个类库项目的「引用」节点,把不该有的引用删掉,重新生成。如果用的是 NuGet 包,确认所有项目用的System.Data.SqlClient版本一致。
5. 进阶改造:把选课容量判断做成存储过程,顺带解决并发超选
5.1 为什么把容量判断放到存储过程里更稳
前面 2.3 里的选课逻辑是在 BLL 里先查再插,这在单用户测试时没问题,但如果有两个学生同时选最后一门容量只剩 1 的课,两个人都查到Selected < Capacity成立,然后都插入,结果就超选了。常见做法是把「检查容量 + 插入记录 + 更新人数」整个逻辑放进一个存储过程,利用数据库的行锁来保证原子性。存储过程里用BEGIN TRANSACTION和WITH (UPDLOCK)锁住课程行,判断和更新在同一个事务里完成。
CREATE PROCEDURE SelectCourse @StudentNo VARCHAR(20), @CourseId INT, @Result INT OUTPUT AS BEGIN BEGIN TRANSACTION; -- 锁住课程行,防止并发超选 DECLARE @Capacity INT, @Selected INT; SELECT @Capacity = Capacity, @Selected = Selected FROM Course WITH (UPDLOCK) WHERE CourseId = @CourseId; IF @Selected >= @Capacity BEGIN SET @Result = -1; -- 已满 ROLLBACK TRANSACTION; RETURN; END IF EXISTS (SELECT 1 FROM Enrollment WHERE StudentNo=@StudentNo AND CourseId=@CourseId) BEGIN SET @Result = -2; -- 重复选课 ROLLBACK TRANSACTION; RETURN; END INSERT INTO Enrollment(StudentNo, CourseId) VALUES(@StudentNo, @CourseId); UPDATE Course SET Selected = Selected + 1 WHERE CourseId = @CourseId; SET @Result = 1; -- 成功 COMMIT TRANSACTION; ENDWITH (UPDLOCK)在读取课程行时就加更新锁,其他事务想读同一行会被阻塞,直到当前事务提交或回滚。@Result输出参数返回不同状态码,C# 端调用时用SqlParameter的Direction = ParameterDirection.Output接收,根据返回值弹不同的提示。这样改造之后,并发选课不会超员,重复选课也会被存储过程拦住。
5.2 C# 端调用存储过程的写法
public int SelectCourseByProc(string studentNo, int courseId) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand("SelectCourse", conn)) { cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.AddWithValue("@StudentNo", studentNo); cmd.Parameters.AddWithValue("@CourseId", courseId); SqlParameter result = new SqlParameter("@Result", SqlDbType.Int); result.Direction = ParameterDirection.Output; cmd.Parameters.Add(result); conn.Open(); cmd.ExecuteNonQuery(); return (int)result.Value; } }CommandType.StoredProcedure告诉 ADO.NET 这是存储过程而不是普通 SQL 文本。@Result参数的方向设为Output,执行完ExecuteNonQuery后从result.Value取值。返回 1 表示成功,-1 表示已满,-2 表示重复选课,窗体根据返回值显示对应提示。这种写法比在 BLL 里拼 SQL 更安全,也更容易在数据库层面做并发控制。
5.3 验证改造是否生效的方法
改完之后怎么确认并发问题真的解决了?可以开两个 SSMS 查询窗口,同时执行EXEC SelectCourse传同一个@CourseId但不同@StudentNo,观察是否只有一个返回 1,另一个返回 -1。或者在 C# 里写个简单的多线程测试,起两个Task同时调SelectCourseByProc,看最终Enrollment表里是不是只多了一条记录。我自己的习惯是每次改完涉及人数、库存、余额这类字段的逻辑,都强制用两个会话手动跑一遍并发,确认锁生效了才继续往下写。这个习惯帮我省过好几次上线后才发现超选的后悔药。希望这份拆解能帮到你,把这份资源跑起来、改明白。
本文还有配套的精品资源,点击获取