☰
C#学生成绩管理系统:三层架构与数据库设计全解析
2026/10/5 3:02:46 网站建设 项目流程

简介:这是一个基于C#与Access数据库的《学生成绩管理系统》课程作业完整项目,适合正在学习C#、数据库编程或需要完成类似课设的初学者参考。系统涵盖学生、课程、成绩三类核心数据的录入、查询、修改与删除,并实现了Windows窗体界面、ADO.NET数据访问、异常处理与数据验证等关键知识点,能帮助读者快速理解数据库应用项目的整体开发流程。压缩包共90个文件,大小3.35MB,包含31个C#源文件、11个界面资源文件、可直接运行的exe及dll、Access数据库文档(accdb),以及项目解决方案和配置文件,打开即可查看源码并运行体验。目前已有962人学习下载,对希望将理论与动手实践结合、快速上手C#+Access开发的入门者,是一份结构完整、便于对照学习的实用资料。

1. 课程作业里的C#学生成绩管理系统:别把99%时间耗在“能跑”之外

每年交课程设计的日子,总能看到一类 C#学生成绩管理系统 :窗体拖好了、数据库连上了、增删改查能点了,但答辩时老师一问“为什么这么设计”“成绩统计怎么算的”“并发改成绩会不会出错”,就卡壳。这个题目的真实难度不在“把界面做出来”,而在“把系统做成一个能演示、能讲解、能扛住追问的闭环”。用 C# 写学生成绩管理系统,核心价值是让你用最小的成本覆盖三层架构、数据库设计、UI 事件处理和异常排查一整条链路。适合两类人:一是刚学完 C# 和 SQL 基础、要交课程设计的学生,二是想快速搭一套内部培训演示系统的开发新手。这篇笔记按我自己的实现路径来写,从选型到数据库、从录入到导出、从答辩到避坑,每一步都有可复现的操作和参数。

2. 技术选型与数据库设计:先把“作业”做成“能演示的系统”

2.1 WinForms 还是 WPF:选错框架会让答辩多花三倍口水

学生成绩管理系统最常见的载体是 WinForms,其次是 WPF。我做课程作业时选的是 WinForms,理由很简单:拖控件上手快,DataGridView 绑定数据源几行代码就能显示成绩表,老师也认这个界面风格。但要注意,现在的课程答辩越来越喜欢问“你有没有用 MVVM”,WinForms 本身不是为 MVVM 设计的,硬套会很难受。如果你选 WPF,天然支持数据绑定,可以把“学生列表”“成绩列表”“统计结果”拆成独立的 ViewModel,代码会清爽很多,但学习曲线陡一些。

// WinForms 下最简的数据显示:把 DataTable 直接绑到 DataGridView DataTable dt = new DataTable(); dt.Columns.Add("StudentId", typeof(string)); dt.Columns.Add("StudentName", typeof(string)); dt.Columns.Add("Score", typeof(decimal)); dt.Rows.Add("2024001", "张明", 86.5m); dataGridView1.DataSource = dt;

这段代码的逻辑很简单:先建一个内存表结构,再塞几行数据,最后赋给 DataGridView 的 DataSource。关键是 DataTable 的列名和数据库查询出的列名要对上,否则绑定后表格里全是空列。参数说明:StudentId 用 string 是因为学号经常有前导零,用 int 会丢;Score 用 decimal 而不是 double,避免 86.5 显示成 86.499999。

选型建议:如果你对数据绑定不熟,就选 WinForms + DataTable;如果愿意为“可维护性”多写几个类,选 WPF + INotifyPropertyChanged。两种方案都能过答辩,但 WPF 在“架构层面”更耐问。

2.2 数据库选型:SQL Server LocalDB、SQLite、MySQL、Access 怎么挑

C# 学生成绩管理系统可选的数据库有四类,我按“课程作业友好度”排个序。第一名是 SQL Server LocalDB:随 Visual Studio 安装,连接字符串里写(LocalDB)\MSSQLLocalDB,不需要单独装服务,老师拿你的项目打开就能跑,前提是他机器上也装了 VS。第二名是 SQLite:单文件、零配置,用 Microsoft.Data.Sqlite 或 System.Data.SQLite 都行,适合交作业,因为数据库就是一个.db文件,拷走就跑。第三名是 MySQL:功能全、网上教程多,但需要单独装服务,答辩电脑上可能没有,翻车概率高。第四名是 Access:老教程里常见,用 OleDb 连接,但 64 位系统下驱动经常出问题,我不推荐新项目用它。

选型连接方式适合场景风险点
SQL Server LocalDBSqlConnection课程作业、本机演示目标机器需有 Visual Studio / LocalDB 运行时
SQLiteMicrosoft.Data.Sqlite独立文件交付并发写入弱,不适合多用户
MySQLMySqlConnector功能完整、资料多需单独装服务,答辩环境不可控
AccessOleDb老系统兼容64 位驱动坑多,已经边缘化

我一般会选 SQLite 做课程作业,因为它最“省心”。连接字符串只需要Data Source=StudentDB.db,建表语句写在程序启动时执行一次即可,不依赖外部服务。但如果你想练企业级套路,选 SQL Server LocalDB,因为老师大概率会追问“你怎么设计索引、怎么处理并发”,SQLite 不好回答这个问题。

2.3 成绩管理系统的核心表结构与外键约束

不要一上来就建一堆表,学生成绩管理系统最少需要三张表:学生表、课程表、成绩表。成绩表是关联表,外键指向学生和课程。很多作业翻车的点在于:成绩表里只存了“分数”,没存“录入时间”和“录入人”,导致老师问“怎么排查谁改过成绩”时答不上来。建议加两列 CreatedAt 和 CreatedBy,成本很低,但能让系统显得完整。

CREATE TABLE Students ( StudentId NVARCHAR(20) PRIMARY KEY, StudentName NVARCHAR(50) NOT NULL, ClassName NVARCHAR(50) NULL, CreatedAt DATETIME2 DEFAULT SYSUTCDATETIME() ); CREATE TABLE Courses ( CourseId NVARCHAR(20) PRIMARY KEY, CourseName NVARCHAR(50) NOT NULL, Credits DECIMAL(3,1) NOT NULL ); CREATE TABLE Scores ( ScoreId INT IDENTITY PRIMARY KEY, StudentId NVARCHAR(20) NOT NULL, CourseId NVARCHAR(20) NOT NULL, Score DECIMAL(5,2) CHECK (Score >= 0 AND Score <= 100), CreatedAt DATETIME2 DEFAULT SYSUTCDATETIME(), CreatedBy NVARCHAR(50) NULL, CONSTRAINT FK_Scores_Students FOREIGN KEY (StudentId) REFERENCES Students(StudentId), CONSTRAINT FK_Scores_Courses FOREIGN KEY (CourseId) REFERENCES Courses(CourseId), CONSTRAINT UQ_Student_Course UNIQUE (StudentId, CourseId) );

逻辑说明:ScoreId 是自增主键;UQ_Student_Course 保证同一学生对同一门课只能有一条成绩,这是防止重复录入的关键。外键的作用是:你删不掉“已被成绩引用的学生”,这在答辩时可以被解释为“数据完整性约束”。参数说明:Credits 用 DECIMAL(3,1) 是兼容 1.5 学分这种值;Score 用 DECIMAL(5,2) 能存 0.00 到 999.99,但 CHECK 约束把它限制在 0~100。

2.4 三层架构的骨架代码:DAL/BLL/UI 怎么切

课程作业最容易被老师质疑的就是“所有代码都堆在按钮点击事件里”。哪怕只写一个极简三层架构,也能让答辩评价上一个台阶。简单说:DAL 层只做数据库读写,BLL 层做业务校验和规则,UI 层只负责展示和收集输入。下面是一个成绩查询的最小闭环。

// DAL 层:负责查询成绩列表 public class ScoreDal { private readonly string _connStr = "Data Source=StudentDB.db"; public DataTable GetScoresByStudentId(string studentId) { using var conn = new SQLiteConnection(_connStr); conn.Open(); var sql = @"SELECT s.StudentName, c.CourseName, sc.Score FROM Scores sc JOIN Students s ON sc.StudentId = s.StudentId JOIN Courses c ON sc.CourseId = c.CourseId WHERE sc.StudentId = @sid"; using var cmd = new SQLiteCommand(sql, conn); cmd.Parameters.AddWithValue("@sid", studentId); var dt = new DataTable(); dt.Load(cmd.ExecuteReader()); return dt; } }

逻辑说明:DAL 层只接收参数、返回 DataTable,不关心界面怎么展示;SQL 里用 JOIN 关联三张表,把 ID 换成名字,UI 直接绑定结果即可。参数说明:@sid是 SQLite 的参数占位符,用参数化查询而不是字符串拼接,能从根上防住 SQL 注入。BLL 层会在调用这个 DAL 方法前做校验,比如“学号不能为空”“学号必须是 8 位”,UI 层只负责把用户输入传给 BLL。

3. 从空项目到可交作业:登录、成绩录入与统计的落地实现

3.1 登录模块:MD5 加盐与 Session 持有当前用户

学生成绩管理系统如果只有查询没有登录,答辩时会被问“这系统谁都能改成绩吗”。加一个登录窗口是性价比最高的功能。密码存储不要用明文,也不要只做简单 MD5——现在老师普遍会问“MD5 撞库怎么办”,所以要用“加盐 MD5”或 SHA256。下面以 SHA256 加盐为例。

public static string ComputeHashedPassword(string password, string salt) { using var sha = System.Security.Cryptography.SHA256.Create(); var raw = Encoding.UTF8.GetBytes(password + salt); var bytes = sha.ComputeHash(raw); return Convert.ToHexString(bytes); } // 注册时生成盐 string salt = Guid.NewGuid().ToString("N"); string hashed = ComputeHashedPassword("123456", salt); // 把 hashed 和 salt 都存到 Users 表

逻辑说明:盐(salt)每次注册都随机生成,同一个密码在不同用户下哈希值不同;登录时把用户输入的密码和库里存的盐拼起来再哈希,和库里存的哈希比对。参数说明:Guid.NewGuid().ToString("N") 生成 32 位无横线随机字符串,作为盐足够。这样即使数据库泄露,攻击者也很难通过彩虹表反推出原密码。

登录成功后,用全局静态类持有当前用户信息,比每个窗体单独传参省事。示例:public static class AppSession { public static string UserName { get; set; } },在登录按钮里赋值。要提示一点:这只是课程作业级别的方案,企业系统不会用静态类保存会话状态。

3.2 成绩录入:DataGridView 绑定与批量保存

成绩录入是核心功能,但也是最容易写崩的地方。常见做法是:主窗体放一个 DataGridView,左侧选学生、上方选课程,右边填分数。批量录入时,我建议别一条条 INSERT,而是攒成一个 DataTable 后用事务批量写。网上搜“C# sqlbulkcopy 表变动有影响”会看到很多坑,SqlBulkCopy 对列顺序、列名映射和数据库表结构变化都敏感,一旦目标表加了列,映射就对不上。所以课程作业我用更稳的“事务循环插入”来替代 SqlBulkCopy,数据量几百条时性能差距根本感知不到。

public void BatchInsertScores(List<ScoreEntity> scores) { using var conn = new SQLiteConnection(_connStr); conn.Open(); using var tx = conn.BeginTransaction(); try { var sql = @"INSERT INTO Scores (StudentId, CourseId, Score, CreatedBy) VALUES (@sid, @cid, @score, @createdBy)"; using var cmd = new SQLiteCommand(sql, conn, tx); cmd.Parameters.Add("@sid", DbType.String); cmd.Parameters.Add("@cid", DbType.String); cmd.Parameters.Add("@score", DbType.Decimal); cmd.Parameters.Add("@createdBy", DbType.String); foreach (var item in scores) { cmd.Parameters["@sid"].Value = item.StudentId; cmd.Parameters["@cid"].Value = item.CourseId; cmd.Parameters["@score"].Value = item.Score; cmd.Parameters["@createdBy"].Value = AppSession.UserName; cmd.ExecuteNonQuery(); } tx.Commit(); } catch { tx.Rollback(); throw; } }

逻辑说明:先开启事务,循环执行同一条 SQL,最后一次性提交;任何一条失败都会回滚,不会出现“改了 10 条但只存进去 5 条”的脏数据。参数说明:SQLiteCommand 的 Parameters 先用 Add 声明类型,再在循环里赋值,这比每次 AddWithValue 性能好,也能避免 AddWithValue 把 decimal 推断成字符串的问题。事务在这里的意义不是性能,而是“全成功或全失败”。

3.3 统计与排序:SQL 聚合还是 C# LINQ

统计功能是拉开分数差距的地方。最低要求是“按学号查平均分、按课程查最高最低”,进阶可以加“班级排名、不及格人数、分数段分布”。我的习惯是:能用 SQL 聚合的就用 SQL 聚合,不要在 C# 内存里折腾。原因很简单——SQL 的 GROUP BY 是老师必考考点,你在 SQL 里写出来,答辩时能当场讲解;你在 C# 里用 LINQ 写,老师可能会追问“如果数据量大到几十万条,内存方案会怎样”。

-- 按课程统计:平均分、最高分、最低分、不及格人数 SELECT c.CourseName, AVG(sc.Score) AS AvgScore, MAX(sc.Score) AS MaxScore, MIN(sc.Score) AS MinScore, SUM(CASE WHEN sc.Score < 60 THEN 1 ELSE 0 END) AS FailCount FROM Scores sc JOIN Courses c ON sc.CourseId = c.CourseId GROUP BY c.CourseId, c.CourseName ORDER BY AvgScore DESC;

逻辑说明:GROUP BY 之后,AVG/MAX/MIN 是分组聚合,SUM(CASE WHEN…) 是“条件计数”的标准写法。ORDER BY AvgScore DESC 让平均分最高的课程排最前,这在界面上展示更直观。注意 GROUP BY 里必须带上 c.CourseName,否则 SQL Server 会报“列在 SELECT 中无效,因为它既不包含在聚合函数中,也不包含在 GROUP BY 子句中”。

C# 侧如果要做“按班级排名”,我一般会把查询结果放到 List 后,用 OrderByDescending 和 GroupBy 组合。这一页可以提一句 C# list 移除的场景:统计完需要过滤掉某门课成绩时,用scores.RemoveAll(s => s.Score < 0)这种谓词移除,比 for 循环里 Remove 安全得多——后者在遍历中改集合会抛异常。

3.4 用 NPOI 导出 Excel 成绩单,绕开 Interop 的坑

导出 Excel 是每个评委都会点的功能。常见的坑是直接用 Excel Interop:程序里 Excel 进程杀不掉、导出时弹窗卡住、目标机器没装 Office 直接报错。网上搜“C# 读取excel”和“C# interop excel”的教训帖很多,我的方案是直接用 NPOI 这个开源库,它读写 xlsx 文件不依赖本机装 Office。下面是最小导出代码。

using NPOI.XSSF.UserModel; using NPOI.SS.UserModel; var workbook = new XSSFWorkbook(); var sheet = workbook.CreateSheet("成绩单"); // 表头 IRow header = sheet.CreateRow(0); header.CreateCell(0).SetCellValue("学号"); header.CreateCell(1).SetCellValue("姓名"); header.CreateCell(2).SetCellValue("课程"); header.CreateCell(3).SetCellValue("分数"); // 数据行 for (int i = 0; i < dataTable.Rows.Count; i++) { IRow row = sheet.CreateRow(i + 1); row.CreateCell(0).SetCellValue(dataTable.Rows[i]["StudentId"].ToString()); row.CreateCell(1).SetCellValue(dataTable.Rows[i]["StudentName"].ToString()); row.CreateCell(2).SetCellValue(dataTable.Rows[i]["CourseName"].ToString()); row.CreateCell(3).SetCellValue(Convert.ToDouble(dataTable.Rows[i]["Score"])); } using var fs = new FileStream("成绩单.xlsx", FileMode.Create, FileAccess.Write); workbook.Write(fs);

逻辑说明:XSSFWorkbook 对应 .xlsx 格式,HSSFWorkbook 对应 .xls 老格式;课程作业一律用 xlsx,兼容性好。SetCellValue 的入参有各种重载,数字用 double,字符串用 string,不要在数字列里塞字符串,否则 Excel 里会出现绿色三角提示。参数说明:CreateRow(i + 1)是把表头行占掉后从第二行开始写数据。NPOI 的 namespace 是NPOI.XSSF.UserModel和NPOI.SS.UserModel,前者是具体实现,后者是通用接口。

4. 加分项:把课程设计做成“可答辩”的系统

4.1 用例图、ER 图与设计文档怎么画

“学生成绩管理系统用例图”几乎是课程报告里的必备插图。不会画不要紧,用 Visio 或 draw.io 画三分钟就能搞定。用例图只需要画一个学生角色和一个教师角色:学生可以“查询成绩”“查看排名”,教师可以“登录”“录入成绩”“修改成绩”“导出报表”。不要画成十几个用例堆一页,老师要看的是你能分清角色的权限边界。ER 图对应前面建的那三张表,标出主键和外键关系就行。

设计文档里建议包含这么几段:功能需求、非功能需求、数据库设计、模块划分、核心算法说明。非功能需求写“系统响应时间小于 1 秒”“支持并发 20 用户”这种可验证的描述,别写“界面美观”这种主观词。答辩时如果你能主动说“这里我用事务保证数据一致性”,比等老师问要加分很多。

4.2 DataGridView 性能优化:2000 行数据别让界面卡成 PPT

很多同学做完成绩查询后,一加载全部成绩就卡顿。网上搜“C# 控件多致winfrom卡”能搜到一堆讨论,核心原因通常是:DataGridView 默认开启了 AutoSizeColumnsMode 和视觉样式增强,行一多,每次重绘都做大量计算。解决方法很简单,手动关掉几个属性就能明显变流畅。

this.dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None; this.dataGridView1.RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.DisableResizing; this.dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect; this.dataGridView1.ReadOnly = true;

参数说明:AutoSizeColumnsMode.None 告诉控件不要每次数据变化都重新计算列宽;DisableResizing 是禁用行头自动调整;ReadOnly 设置成绩列表默认只读,避免用户误改单元格后不知道怎么保存。再配合虚拟模式(VirtualMode)可以扛上万行,但对课程作业来说,关掉 AutoSize 就够了。

4.3 状态栏与进度条:耗时操作的 UI 反馈

成绩导入或导出时,界面如果没有反馈,用户会以为程序死了。用 BackgroundWorker 或 async/await 把耗时操作放到后台线程,同时更新状态栏和进度条,这是个很加分的细节。搜“C# winform如何更新状态栏与进度条”能找到很多方案,核心规则只有一条:不要在后台线程直接改控件属性,必须通过 Invoke 或者用 Task 的上下文切回 UI 线程。

private async void btnExport_Click(object sender, EventArgs e) { btnExport.Enabled = false; progressBar1.Value = 0; statusStrip1.Items["toolStripStatus"].Text = "正在导出..."; await Task.Run(() => DoExport()); // 耗时操作放后台 progressBar1.Value = 100; statusStrip1.Items["toolStripStatus"].Text = "导出完成"; btnExport.Enabled = true; }

逻辑说明:async void 用于事件处理器是允许的;await Task.Run 把 DoExport 放到线程池,导出完成后自动回到 UI 线程继续更新控件,WindowsFormsSynchronizationContext 会帮我们处理好跨线程切换,所以不需要手动 Invoke。参数说明:进度条在这里是装饰性的,真实统计粒度可以把 DoExport 拆成多段,每完成一段更新一次 Value。关键教训是:千万别把 Task.Run 用在同一个方法里又去等待自己,会死锁。

5. 常见问题排查与避坑:5 个让课程作业返工的实际案例

5.1 现象:打开项目提示“未能加载文件或程序集”

答辩现场最容易遇到的翻车场景:老师电脑上双击你的 .sln,编译报错“未能加载文件或程序集 System.Data.SQLite”或“Microsoft.Data.Sqlite”。原因基本只有一个:你用 NuGet 装的 SQLite 包是“按本机架构”解析的,32 位和 64 位不匹配。解决方法是:在 NuGet 包管理器里重新装 SQLite 的 “bundle” 版本,它会自动带对应架构的原生 DLL;或者把项目平台目标改成 AnyCPU,并在项目属性里勾选“首选 32 位”的复选框。我在交作业前都习惯把项目换一台机器重新拉下来编译一遍。

5.2 现象:DataGridView 编辑后保存不了数据

自己填了分数,按保存按钮没反应,数据库里还是空的。原因是 DataGridView 默认在单元格失去焦点时才提交修改,如果保存按钮直接读取 DataGridView 的数据源,当前正在编辑的单元格还没写入。解决:在保存按钮的 Click 事件第一行调用this.dataGridView1.EndEdit(),强制结束编辑状态;也可以用BindingSource.EndEdit()双保险。另一个隐藏坑:如果 DataGridView 的数据源是 DataTable,修改后需要DataTable.AcceptChanges()吗?不需要——AcceptChanges 只是清除行状态标记,保存时用 SELECT 重新查一次数据库看有没有写进去,别靠内存状态判断。

5.3 现象:插入成绩报错“列名或所提供值的数目与表定义不匹配”

写完 INSERT 语句后一执行就报这个错,常见于你给 Scores 表新增了 CreatedBy 列,但 INSERT 语句没更新;或者反过来,表没有 CreatedBy 列,SQL 里却写了。解决:先查表结构,再对 SQL 语句。在 SQLite 里可以用PRAGMA table_info(Scores);看列清单;在 SQL Server 里用SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='Scores';。这是一个很低级但非常消耗时间的坑,养成“改表就改 SQL”的习惯能省很多事。

5.4 现象:导出的 Excel 文件被占用,第二次导出失败

用 Interop 导出时,Excel 进程没释放,导致下一个用户打不开文件或被锁定。这也是我改用 NPOI 的直接原因。如果你因为某些原因必须用 Interop,记住收尾三行:workbook.Close(); excel.Quit(); Marshal.ReleaseComObject(excel);,并在任务管理器里确认 EXCEL.EXE 进程确实结束。NPOI 方案没有这个隐患,因为它是直接操作文件流,用完 Dispose 就释放了。

5.5 现象:输入用户名时输入' OR '1'='1直接登录成功

这是最严重的作业污点:SQL 注入。如果在登录查询里用了字符串拼接,比如"SELECT * FROM Users WHERE Name='" + username + "'",那输入' OR '1'='1就会绕过密码校验。解决方式就是全面改用参数化查询,前面登录和成绩查询的代码示例都用了@sid这类参数,把拼接 SQL 的习惯彻底戒掉。答辩时如果老师问“你怎么防注入”,你答“全程参数化查询”就是标准答案。我见过有同学在 PPT 里贴了拼接 SQL 的代码,被老师当场指出,整个评分直接掉了档,这点一定要自检。

6. 答辩前 15 分钟:用手工冒烟清单验证功能完整性,别让演示现场翻车

离答辩还有半小时,与其焦虑不如跑一遍冒烟清单。我会按业务主线走一遍:启动程序 → 登录(分别测正确密码和错误密码)→ 添加一个学生 → 给这个学生录两门课成绩 → 修改其中一个成绩 → 查询统计结果确认平均分有变化 → 导出 Excel 并打开确认内容 → 关掉程序重开,登录刚才的账号,确认数据还在。这一条流程能覆盖 90% 的常见故障:连接字符串写错、事务没提交、外键约束冲突、导出路径无权限。

还要做一个反向验证:故意输入非法分数(比如 101 分、负分),看系统是否拦截。老师最喜欢操作这一步,因为很多学生只在 UI 上做了控件校验,但绕过 UI 直接调用 BLL 层就能塞进非法数据。我一般会在 BLL 层加这道判断,UI 层只是第一步拦截。

if (score < 0 || score > 100) { throw new ArgumentOutOfRangeException(nameof(score), "成绩必须在0到100之间"); }

这句话写在小节里的原因是:它体现“分层架构的业务规则校验”,是最容易在答辩现场讲的 30 秒。参数说明:nameof(score) 是 C# 6 的语法,抛异常时自动带上参数名,排查时能直接定位到是哪个参数出了问题。

另外一个小习惯:把 SQLite 数据库文件放到程序运行目录(比如AppDomain.CurrentDomain.BaseDirectory),不要写死D:\xxx\StudentDB.db这种绝对路径。否则答辩换个电脑,程序就找不到数据库,这属于“在自己的机器上跑通了,换台机器就废”的典型。要判断路径是否可靠,可以打印Environment.CurrentDirectory和AppDomain.CurrentDomain.BaseDirectory,把数据库文件的定位逻辑放到一个单独的 DbPathProvider 类里,别散落各处。

最后说一个我的血泪教训:有个学期我把数据库文件放在了bin\Debug下,交作业时用 Release 模式重新生成了一次项目,结果数据库文件被覆盖,所有测试数据全没了。后来我固定把数据库放在一个独立的Data目录,并在程序启动时检查文件是否存在,不存在就自动建表和插入种子数据。这样哪怕文件丢了,程序也能重新生成,不至于当场空白。希望帮到你——按这套思路做完,你的 C# 学生成绩管理系统就不仅是一个能跑的作业,更是一个你能讲清楚、能经得起追问的完整项目。

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

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

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

立即咨询