☰
C#学生管理系统实训报告:WinForms+SQL Server六表设计与角色权限实现
2026/10/10 9:54:04 网站建设 项目流程

简介:这是一份C#学生管理系统实训报告,完整记录了湖南生物机电职业技术学院软件专业在《数据库与C#》课程中开发学生信息管理系统的全过程,适合需要完成C#课程设计、毕业设计或系统学习.NET数据库应用开发的读者。报告从项目实训目的出发,划分了学生、教师、管理员三类角色的功能模块:学生可登录并管理个人信息、选修课程、查询成绩和修改密码;教师负责成绩录入修改、课程管理与密码维护;管理员则覆盖班级、成绩、学生、教师及管理员的综合管理。系统总体设计给出了功能模块关系,数据库部分详细描述了student、teacher、course、choice、userinfo、class共6张表的核心字段。整份资源为1个doc文档,大小1.68MB,内容覆盖需求分析、界面设计、代码实现到文件列表的完整文档链,并附有登录界面等运行效果说明。目前已有201人学习下载,可作为实训报告撰写、系统框架搭建和角色权限设计的有力参考。

1. 学生管理系统实训报告:这份 C# + SQL Server 资源能复现到什么程度

先给结论:这份《C#学生管理系统实训报告》不是一份普通文档,它把 2010 级软件专业《数据库与 C#》实训的完整产出——需求分析、数据库设计、功能模块划分、核心代码、界面运行效果——全部打包在了一份 .doc 里。你打开后能看到的不只是“报告”,而是一个可以照着敲、照着改、照着迁移到新项目里的学生信息管理系统雏形。系统分为学生、教师、管理员三种角色,覆盖登录、个人信息、课程管理、成绩查询、班级管理、用户管理等典型 CRUD 场景,数据库侧设计了 6 张核心表,代码侧从 LoginForm.cs 到 MDI 父窗体再到各个子窗体都有对应实现。适合正在做课程设计、需要写实训报告、或者想快速搭一套 WinForms + SQL Server 管理后台的开发者对照参考。我从资源和复现两个角度把它拆开,讲清楚每一块能直接抄什么、改哪里会翻车。

2. 系统功能架构与角色权限:先看清三张面孔再动手

2.1 三种角色的功能边界

系统的权限模型很清晰:学生、教师、管理员各自登录后进入不同的 MDI 父窗体,功能互不越界。学生模块能查看和修改个人信息、管理选修课程、按年度和课程名查成绩、修改个人密码;教师模块能录入和更改所教课程的成绩、维护自己开设的课程信息;管理员模块则拥有最高权限,班级、成绩、学生、教师、管理员本身都能查改增删。

这种设计思路在实训项目里很典型——不追求复杂的 RBAC 权限框架,而是用“角色值 + 登录跳转”硬编码实现。具体到代码里,登录时从 userinfo 表读出 role 字段,判断是 1(学生)、2(教师)还是 3(管理员),然后打开对应的父窗体。这样做的好处是逻辑直接、容易讲清楚,坏处是权限判断散落在各窗体里,后面要加角色就得逐个改。

我看这份报告时最关注的不是功能多不多,而是它是否把“一个系统该有的闭环”走通了。从文件列表来看,它确实做到了:学生能选课、能查成绩,教师能录成绩、能维护课程,管理员能管班级、管账户,数据流在 student、choice、course、score 这几张表之间是能循环起来的,不是那种只有登录和增删改查的“半成品”。

2.2 界面组织:MDI 父窗体与子窗体的配合

报告里的代码大量使用 MDI(多文档界面)模式:学生模块的 MainForm.cs、教师模块的 Formteacher.cs、管理员模块的 Form1.cs 都是父窗体,各自承载不同的子窗体。比如学生父窗体加载时会依次创建 Form3(个人信息)、Form4(修改信息)、Form5(课程管理)、Form6(成绩查询)、Form7(密码修改)五个子窗体,并设置 MdiParent 属性。

这种做法的好处是菜单切换子窗体时不需要重复打开新窗口,点菜单项时先 Close 旧的再 new 一个同类型窗体,保证界面状态是最新的。代价是内存管理要小心——频繁 Close 和重建如果没处理好,会积累资源占用。我在自己的项目里一般会在菜单点击事件里先判断子窗体是否已经存在,存在则 Activate,不存在才创建,比这份代码里的“先 Close 再 new”更稳一些。

2.3 登录模块代码拆解:密码校验和角色跳转

登录是整套系统的入口,报告里给了完整代码。核心逻辑是:拼接 SQL 查询 userinfo 表,逐行读取用户名、密码、角色、ID,然后用 textBox 输入框的值和数据库里的密码对比,匹配后根据角色值跳转到对应窗体。

string upwd = this.textBox2.Text.Trim(); string b1 = comboBox1.SelectedItem.ToString(); int b = 0; SqlConnection conn = new SqlConnection("server=localhost;database=guanlixitong;uid=sa;pwd=123"); conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select name,pwd,r,id from userinfo where name='" + textBox1.Text.Trim() + "'"; SqlDataReader red = comm.ExecuteReader();

这段代码有几个典型问题值得注意。第一,SQL 是字符串拼接,存在注入风险,输入框里输个单引号就能让查询变形;第二,密码是明文存储和明文比对,没有任何散列处理;第三,连接串写死在代码里,uid=sa、pwd=123,一旦部署到真实环境就是巨大的安全隐患。但对于实训报告来说,这样写反而容易让读者理解“登录验证”的最朴素实现——先查出来,再比对,再跳转。

我建议拿到这份代码后至少做两件事:一是在 where 条件里改用 SqlParameter 参数化查询,二是把连接串挪到 App.config 里用 ConfigurationManager 读取。改完之后,登录模块就从一个“能跑的教学代码”变成了“能扛住基本攻击的可用模块”。

3. 数据库六表设计与关系梳理:照着建库就能跑

3.1 六张表的字段与主外键关系

系统一共设计了 6 张表:student(学生)、teacher(教师)、course(课程)、choice(选课)、userinfo(用户信息)、class(班级)。每张表的字段定义在报告里列得很详细,比如学生表有学号 S_no 主键、姓名、性别、生日、系别、年级;课程表有课程号 C_no 主键、课程名、类型、学分、教师号 T_no 外键、年级。

表间关系可以这样理解:student 和 course 通过 choice 表建立多对多关系,choice 表的主键是 S_no + C_no 联合主键,score 字段记录该学生该课程的成绩;teacher 和 course 是一对多,一个教师可以开多门课;userinfo 表独立存登录账户和角色,不和学生表或教师表做外键约束,通过登录号 Userid 与业务表关联。class 表则只存年级和班级两种信息,用于管理员维护班级数据。

CREATE TABLE student ( S_no CHAR(10) PRIMARY KEY, S_name CHAR(20), S_sex CHAR(2), S_birthday DATETIME, S_department CHAR(10), grade CHAR(10) ); CREATE TABLE choice ( S_no CHAR(10), C_no CHAR(10), score INT, PRIMARY KEY (S_no, C_no), FOREIGN KEY (S_no) REFERENCES student(S_no), FOREIGN KEY (C_no) REFERENCES course(C_no) );

这里有个设计取舍值得说:成绩字段放在 choice 表里,而不是单独建一张 score 表。对于实训规模的数据量来说,这种设计完全够用——选课记录天然携带成绩,查询某学生某课程成绩时只需要一条 SQL 联表就能拿到。但如果未来要支持多次考试、平时分和期末分分开存,就得把成绩拆成独立表了。这是“够用”和“可扩展”的区别,你在复现时可以先按报告来,后续有需求再演进。

3.2 类型选择里的小坑:CHAR 还是 VARCHAR

报告里几乎所有字符串字段都用的是 CHAR(10)、CHAR(20),这在 2010 年前后的教材里很常见。CHAR 定长存储的性能优势在数据量小的时候体现不出来,反而有个明显问题:查询和比对时如果不 Trim 掉尾部空格,明明内容相同的数据也会被认为不同。

代码里很多地方做了 .Trim() 处理,比如登录时c2.Trim() == upwd,这就规避了 CHAR 的尾部空格问题。但如果你自己建库,我更推荐把非主键的字符串字段改成 VARCHAR,比如 S_name VARCHAR(20)、S_department VARCHAR(50),主键和关联字段保持 CHAR 或改成固定长度的业务编码都可以,两者结合既能省空间又能避免一堆 Trim 调用。

-- 推荐调整后的学生表 CREATE TABLE student ( S_no CHAR(10) PRIMARY KEY, S_name VARCHAR(20), S_sex CHAR(2), S_birthday DATETIME, S_department VARCHAR(50), grade VARCHAR(20) );

3.3 建库建表的可复现路径

拿到报告后复现数据库的推荐顺序是:先创建数据库,再按依赖关系建表——先建 student、teacher、class 这些基础表,然后建 course(依赖 teacher),最后建 choice(依赖 student 和 course),再单独建 userinfo。userinfo 表不依赖其他表,但它是登录功能的基础,建议在业务表建完后就建。

数据库名在报告代码里是 guanlixitong,拼音风格,和项目时期的教学习惯一致。如果你不需要沿用原名,建议改成 StudentManager 或类似的英文名,避免编码和路径问题。建完表后别忘了把几条测试数据插进去,至少要有学生、教师、管理员各一条账户记录,不然登录功能测不了。

4. 核心模块代码逐段解析:从数据库操作到界面刷新

4.1 学生个人信息显示:SqlDataAdapter 与 DataSet 的组合

学生个人信息子窗体 Form3 的加载逻辑很典型:用 SqlCommand 带参数查询当前学号的学生记录,通过 SqlDataAdapter 填充到 DataSet,再将 DataSet 绑定到 DataGridView 显示,同时把各字段值写入窗体上的文本框。

SqlConnection conn = new SqlConnection("server=localhost;database=guanlixitong;uid=sa;pwd=123"); SqlCommand com = new SqlCommand("Select *from student where S_no=@no", conn); SqlParameter pa = new SqlParameter("@no", Value1); com.Parameters.Add(pa); SqlDataAdapter da = new SqlDataAdapter(); da.SelectCommand = com; DataSet dd = new DataSet(); da.Fill(dd, "student"); dataGridView1.DataSource = dd.Tables[0];

这段代码的价值在于演示了一个完整的“查询 -> 填充 -> 绑定”流程。这里已经用了 SqlParameter,说明作者在部分模块有参数化意识,但登录模块又用了拼接,前后风格不统一,这是实训代码常见的“边写边学”痕迹。你在复现时建议统一全部走参数化路线。

4.2 学生信息修改:SqlCommandBuilder 自动生成更新 SQL

Form4 的修改逻辑有点意思:先查出学生数据填充到 DataSet,用户在文本框里改动字段后,直接修改 DataSet 里的 DataRow,然后调用 SqlCommandBuilder 自动生成 Update 语句,最后da.Update(dd, "student")一次性提交。

DataRow roe = null; foreach (DataRow row in dd.Tables[0].Rows) { if (row["S_no"].ToString().Trim() == Value1) { roe = row; roe["S_name"] = textBox2.Text.ToString().Trim(); roe["S_sex"] = textBox3.Text.ToString().Trim(); roe["S_birthday"] = textBox4.Text.ToString(); roe["S_department"] = textBox5.Text.ToString().Trim(); roe["grade"] = textBox6.Text.ToString().Trim(); } } SqlCommandBuilder bu = new SqlCommandBuilder(da); da.Update(dd, "student");

SqlCommandBuilder 的原理是根据 SelectCommand 自动生成 Insert、Update、Delete 对应的 SQL 命令,前提是 Select 查询必须包含主键字段,并且表结构允许生成这些命令。这里 Select 查了所有列,主键 S_no 也在结果集里,所以能正常工作。但有个限制:如果 Select 语句涉及多表 JOIN,SqlCommandBuilder 通常无法自动生成更新命令,会直接抛异常。所以这种写法只适合单表简单更新。

另一个坑是日期字段的处理。S_birthday 是 datetime 类型,如果文本框里输入了空字符串或者格式不对,da.Update时会因为无法转换为合法日期而报错。我一般会在写入前做 DateTime.TryParse 校验,非法就提示用户,而不是直接放行。

4.3 课程管理:类型化 DataSet 与 TableAdapter 的使用

Form5 的代码和前面几个窗体不太一样,它用了this.courseTableAdapter.Fill(this.guanlixitongDataSet3.course)这种强类型 DataSet 的方式。这说明项目中有一部分是可视化设计器自动生成的,不是纯手写代码。

TableAdapter 方式的好处是设计器会帮你生成连接、命令、参数等一系列封装,代码看起来简洁,坏处是维护时你得知道设计器生成了哪些文件,不然改表结构后经常出现“类型化 DataSet 未同步”的奇怪错误。我复现这类代码时,如果原项目用的是 DataSet 设计器,我通常会把强类型部分换成手写 SqlDataAdapter,一是逻辑更透明,二是跨机器复制项目时不容易出问题。

// 手写替代方案:查询当前学生的选课及对应课程名 SqlConnection conn = new SqlConnection("server=localhost;database=guanlixitong;uid=sa;pwd=123"); SqlCommand com = new SqlCommand( "SELECT c.C_no, c.C_name, c.C_type, c.C_score FROM course c " + "INNER JOIN choice ch ON c.C_no = ch.C_no WHERE ch.S_no = @no", conn); com.Parameters.AddWithValue("@no", Value1); SqlDataAdapter da = new SqlDataAdapter(com); DataTable dt = new DataTable(); da.Fill(dt); dataGridView1.DataSource = dt;

这段替代方案在原报告基础上做了扩展:它把 course 和 choice 做了 INNER JOIN,学生看到的就不只是选课记录的编号,而是课程的具体信息。原报告里学生课程管理只查看 choice 表里的课程号,信息不全;实际产品里基本都会 join 课程表,让界面显示课程名和学分。

4.4 MDI 子窗体切换:内存与状态的取舍

学生父窗体加载时同时创建 5 个子窗体,这个设计在“功能展示”层面没问题,但有个资源隐患:所有子窗体在启动时就全部加载,即使学生从头到尾只用成绩查询,其他四个窗体的查询和绑定也全部执行了一遍。

做法改成“首次点击菜单时才创建子窗体”是更优解。对应代码就是把父窗体 Load 事件里的创建逻辑移除,在每个菜单点击事件里判断if (chil3 == null || chil3.IsDisposed) { chil3 = new Form3(Value1); },然后 Show。这样启动速度快,内存占用低,也不会有隐藏窗体干扰数据状态。

5. 避坑与排查:实训代码翻车的五个典型位置

5.1 登录界面点击后闪退无提示

现象:输入正确的用户名密码,点击登录按钮后程序直接退出,没有报错信息。

原因:代码里catch (SqlException)只捕获了 SQL 异常,如果是连接失败,比如 SQL Server 服务没启动、连接串里的数据库名不存在,确实会走这个分支显示“程序出错,请重新启动”。但如果异常发生在其他位置,比如 comboBox1.SelectedItem 为 null、表里查不到数据导致 red.Read() 直接跳过,异常类型不是 SqlException,程序就会崩掉。

解决:把 catch 改成两个——一个捕获 SqlException 显示数据库错误,一个捕获 Exception 打印具体堆栈。调试期间最好用 MessageBox.Show(ex.Message) 把真实错误露出来,定位到原因后再收口。

5.2 SQL 拼接导致登录名带单引号时报错

现象:用户名输入admin' or '1'='1,登录窗直接报 SQL 语法错误,或者更糟——直接登录成功。

原因:登录模块用的是字符串拼接 SQL,单引号被当成 SQL 语法的一部分解析。如果后台代码还有“查不到记录就让他登录”的错误逻辑,注入就能绕过密码。

解决:所有用户输入都走 SqlParameter。改法和前面的参数化一致,把where name='"+ textBox1.Text.Trim() + "'"换成where name=@name,再用 Parameters.AddWithValue 传值。改完后再用注入语句测一次,不会再报错就说明挡住了。

5.3 CHAR 类型的尾部空格导致密码比对失败

现象:userinfo 表里存的是admin(CHAR(20)),数据库自动补齐空格到 20 位,界面输入的密码admin却没有尾部空格,Trim 不彻底就比对失败。

原因:代码里是c2.Trim() == upwd,已经做了 Trim,理论上不会出问题。但如果你在自己复现时没写 Trim,直接拿数据库读出来的值和输入框的值做==,就会因为尾随空格一直登录失败。

解决:数据库读出值做.Trim(),输入框值也做.Trim(),两边都清理再比对。更彻底的做法是把非主键字段改成 VARCHAR,从根上消灭填充空格的问题。

5.4 SqlCommandBuilder 更新时提示“无法自动生成命令”

现象:学生信息修改窗体点保存,报错说 SqlCommandBuilder 需要主键列或者无法生成 Update 命令。

原因:Select 语句查的不是单表,或者结果集不包含主键列。比如你把 Select 改成了同时查 student 和 class 两个表的内容,SqlCommandBuilder 就不知道怎么生成 Update。

解决:保证 Select 查询只针对单表且包含主键。多表信息展示用 JOIN 查没问题,但写入操作要回到单表更新,或者手写 Update 语句指定要改的字段。

5.5 DataGridView 绑定后列头为英文表名或字段名

现象:查询学生信息列表时,表格列头显示的是 S_no、S_name 这种英文字段名,界面不够友好。

原因:DataGridView 直接绑定了 DataTable,列名就是数据库字段名,没有做列映射。

解决:在窗体设计器里手动配置 DataGridView 列,或者绑定后写代码把列名替换为中文标题:

dataGridView1.Columns["S_no"].HeaderText = "学号"; dataGridView1.Columns["S_name"].HeaderText = "姓名"; dataGridView1.Columns["S_sex"].HeaderText = "性别";

这种做法不改变数据源结构,只是改显示名称,是最快捷的界面优化方式。

6. 让系统从“能跑”到“能演示”:角色体验与数据扩展技巧

6.1 构造一套完整的数据演示链路

实训项目演示时最怕的是“有功能没数据”,评委点开成绩查询却是空表,观感很差。我拿到这套系统后第一步就是造数据:至少创建 5 个学生、3 个教师、6 门课程,然后让每个学生选 3~5 门课并给每门课录个成绩。

造数据有个次序问题:先插基础表(student、teacher、course、class),再插 choice,中间补 userinfo。userinfo 中的用户名和密码要和学生表、教师表能对应上,方便演示时用不同角色登录。我一般会让学生账户的登录号就是学号 S_no,教师账户的登录号就是 T_no,这样从数据上就能看出账户归属。

-- 造一条学生账户和基本信息 INSERT INTO student (S_no, S_name, S_sex, S_birthday, S_department, grade) VALUES ('20250001', '张三', '男', '2000-01-01', '计算机系', '2025'); INSERT INTO userinfo (Userid, Username, passwd, role) VALUES ('20250001', '张三', '123456', '1');

学生端看到的成绩查询,实际查的是 choice 表里的 score 字段,所以一定要在造完选课记录后给每门课填写分数,否则学生端成绩模块一片空白,和“有成绩管理功能”的汇报矛盾。

6.2 把界面从教学样机调成演示级

原系统的界面风格受限于 2010 年代的 WinForms 默认样式,放到现在偏朴素。如果只是实训演示,不追求界面美观也可以,但要让评审看到你的用心,我建议做三件小事:给 DataGridView 开启自动列宽并加斑马纹、把登录窗体的按钮做默认和取消绑定(回车默认登录、ESC 清空输入)、给 MDI 父窗体加背景色区分功能区。

这三件事都不需要改业务逻辑,纯界面层调整,半小时内完成,但对整体观感提升明显。我在复现类似实训项目时,常把“登录页 + 主界面 + 一个查询列表”这三个页面的视觉调好,评审看到的就是“这个学生不只是会抄代码,还懂产品”。

6.3 密码明文存储的后患与最低限度修复

报告里的 userinfo 表 passwd 字段存的是明文,这在实训环境里能跑,但如果你把这套系统作为面试作品或者接手的旧系统改造项目,明文密码就是硬伤。最低限度的修复是加一层哈希再存,不改表结构只改逻辑。

// 使用 SHA256 对密码做简单哈希,避免明文落库 using System.Security.Cryptography; using System.Text; static string HashPassword(string raw) { using (SHA256 sha = SHA256.Create()) { byte[] bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(raw)); StringBuilder sb = new StringBuilder(); foreach (byte b in bytes) sb.Append(b.ToString("x2")); return sb.ToString(); } }

这个方案只看代码量,改动很小,但安全收益立竿见影——数据库泄露也不会直接暴露所有账户的密码。我在处理这类教学代码时会先问自己一个问题:这份代码是只用来交作业,还是可能会被放到真实环境里跑。如果是后者,密码哈希、参数化查询、连接串外置这三步必须做。从那以后我每次接手类似的项目,都会先检查这几个点再动其它功能,顺序是:连接串先外置、所有 SQL 先参数化、密码先做哈希,一套流程走完,心里才有底。希望这份拆解能帮你把这份实训报告真正用起来,不管是交作业还是做二次开发,都能少走几段弯路。

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

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

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

立即咨询