生管理系統(tǒng)實(shí)訓(xùn):WinForms+SQLite三層架構(gòu)CRUD與報(bào)告撰寫指南)
簡介這份C#學(xué)生管理系統(tǒng)實(shí)訓(xùn)報(bào)告面向軟件專業(yè)學(xué)生及C#.NET初學(xué)者對應(yīng)《數(shù)據(jù)庫與C#》課程實(shí)訓(xùn)場景幫助讀者理解如何用C#結(jié)合SQL Server完成一個(gè)具備學(xué)生、教師、管理員三種角色的信息管理系統(tǒng)。報(bào)告完整呈現(xiàn)了項(xiàng)目實(shí)訓(xùn)目的、系統(tǒng)功能模塊劃分、總體設(shè)計(jì)思路與數(shù)據(jù)庫設(shè)計(jì)涵蓋學(xué)生表、教師表、課程表、選課表、用戶信息表和班級表共6張核心表并給出各表字段類型與說明同時(shí)梳理了登錄、成績管理、課程管理、班級管理等模塊的文件組織與功能邏輯。資源包為1個(gè)doc文檔大小約1.68MB內(nèi)容結(jié)構(gòu)清晰適合作為課程設(shè)計(jì)、實(shí)訓(xùn)報(bào)告撰寫與數(shù)據(jù)庫應(yīng)用開發(fā)的參考范例。目前已有201人學(xué)習(xí)可幫助讀者快速把握C#數(shù)據(jù)庫項(xiàng)目的整體架構(gòu)與文檔編寫方法。1. 一份“C#學(xué)生管理系統(tǒng)實(shí)訓(xùn)報(bào)告.doc.doc”背后到底在練什么如果你手里正躺著一份名為“C#學(xué)生管理系統(tǒng)實(shí)訓(xùn)報(bào)告.doc.doc”的文件先別急著吐槽那個(gè)重復(fù)的.doc后綴——它大概率是某個(gè)同學(xué)從模板復(fù)制出來、改完內(nèi)容忘了改名的產(chǎn)物。這個(gè)標(biāo)題真正指向的不是一份文檔排版任務(wù)而是一套完整的桌面端 CRUD 訓(xùn)練用 C# 加 WinForms 或 WPF把學(xué)生、班級、成績這幾張表管起來再配一份能講清楚設(shè)計(jì)思路的實(shí)訓(xùn)報(bào)告。它適合剛學(xué)完 C# 基礎(chǔ)語法、想找一個(gè)能寫進(jìn)簡歷或交作業(yè)的完整項(xiàng)目練手的人也適合帶實(shí)訓(xùn)課的開發(fā)者拿來當(dāng)參考骨架。核心難點(diǎn)從來不是“怎么連數(shù)據(jù)庫”而是怎么把界面、數(shù)據(jù)訪問、業(yè)務(wù)校驗(yàn)三層拆干凈以及報(bào)告里怎么把“我為什么這么設(shè)計(jì)”講明白。這一篇就按真實(shí)落地順序從環(huán)境搭到報(bào)告結(jié)構(gòu)把能抄的代碼和能避的坑一次講透。2. 先把項(xiàng)目骨架定下來WinForms 還是 WPF數(shù)據(jù)庫選哪個(gè)動(dòng)手之前最怕的就是選型反復(fù)橫跳。我?guī)н^幾輪實(shí)訓(xùn)最常見的翻車現(xiàn)場是第一天用 WinForms 拖控件第二天聽說 WPF 更“高級”就重寫結(jié)果兩天都在搭界面。所以這一章先把選型理由講清楚再給出可直接復(fù)用的三層目錄結(jié)構(gòu)。2.1 界面框架WinForms 夠用WPF 看需求如果你的目標(biāo)是兩周內(nèi)交出一個(gè)能增刪改查、能導(dǎo)出成績的系統(tǒng)WinForms 是更穩(wěn)的選擇。它的拖拽式設(shè)計(jì)器對新手友好DataGridView 綁定 DataTable 幾乎零配置出問題也容易搜到答案。WPF 的優(yōu)勢在樣式和數(shù)據(jù)綁定更靈活但 MVVM 模式對剛接觸 C# 的人是一道額外的坎容易把時(shí)間耗在命令綁定和依賴屬性上。我的建議很直接實(shí)訓(xùn)周期少于三周、以功能完整度為第一優(yōu)先級選 WinForms如果課程明確要求 WPF 或你想順便練 MVVM再上 WPF。兩者在數(shù)據(jù)訪問層和業(yè)務(wù)層的代碼幾乎可以復(fù)用切換成本主要在前端。2.2 數(shù)據(jù)庫SQL Server LocalDB 與 SQLite 的取舍學(xué)生管理系統(tǒng)數(shù)據(jù)量小不需要考慮高并發(fā)。常見做法有兩種方案優(yōu)點(diǎn)缺點(diǎn)適用場景SQL Server LocalDB語法和正式 SQL Server 一致SSMS 可視化需要安裝運(yùn)行時(shí)換機(jī)器可能連不上學(xué)校機(jī)房統(tǒng)一環(huán)境SQLite單文件、零安裝、拷貝即用部分 SQL 語法有差異個(gè)人開發(fā)、需要隨項(xiàng)目打包我一般會(huì)選 SQLite因?yàn)閷?shí)訓(xùn)報(bào)告經(jīng)常要連同數(shù)據(jù)庫文件一起交單文件最省事。下面統(tǒng)一用 SQLite 演示換成 LocalDB 只需改連接字符串和少量 SQL。2.3 三層目錄結(jié)構(gòu)別把 SQL 寫進(jìn)按鈕事件里新手最容易犯的錯(cuò)是在button1_Click里直接拼 SQL。這樣寫出來的代碼改一個(gè)字段要翻遍所有窗體。正確的做法是分三層StudentMgr/ ├── Models/ // 實(shí)體類對應(yīng)數(shù)據(jù)庫表 │ └── Student.cs ├── DAL/ // 數(shù)據(jù)訪問層只負(fù)責(zé)增刪改查 │ └── StudentDal.cs ├── BLL/ // 業(yè)務(wù)層做校驗(yàn)和組合邏輯 │ └── StudentBll.cs └── UI/ // 窗體只負(fù)責(zé)展示和收集輸入 └── MainForm.cs這個(gè)結(jié)構(gòu)的好處是UI 層不出現(xiàn)任何 SQL 字符串DAL 層不出現(xiàn)任何 MessageBox。職責(zé)一分開調(diào)試時(shí)能快速定位問題出在哪一層。2.4 建庫建表的最小 SQL在項(xiàng)目根目錄放一個(gè)init.sql第一次運(yùn)行時(shí)執(zhí)行-- 學(xué)生表學(xué)號做主鍵避免重名 CREATE TABLE Student ( StuNo TEXT PRIMARY KEY, -- 學(xué)號業(yè)務(wù)主鍵 Name TEXT NOT NULL, Gender TEXT CHECK(Gender IN (男,女)), ClassName TEXT, BirthDate TEXT ); -- 成績表用自增 id外鍵關(guān)聯(lián)學(xué)號 CREATE TABLE Score ( Id INTEGER PRIMARY KEY AUTOINCREMENT, StuNo TEXT NOT NULL, Course TEXT NOT NULL, Points REAL CHECK(Points BETWEEN 0 AND 100), FOREIGN KEY (StuNo) REFERENCES Student(StuNo) );邏輯說明學(xué)號用TEXT而不是自增整數(shù)是因?yàn)檎鎸?shí)學(xué)號常帶字母和年份前綴用自增 id 反而要在界面上多維護(hù)一個(gè)字段。CHECK約束把性別和分?jǐn)?shù)范圍卡在數(shù)據(jù)庫層即使業(yè)務(wù)層漏校驗(yàn)臟數(shù)據(jù)也進(jìn)不來。外鍵約束在 SQLite 里默認(rèn)不開啟需要在連接字符串或每次連接后執(zhí)行PRAGMA foreign_keys ON;這一點(diǎn)后面避坑章節(jié)會(huì)再提。3. 數(shù)據(jù)訪問層怎么寫參數(shù)化查詢與實(shí)體映射這一章是整篇的核心代碼量最大。目標(biāo)是把 StudentDal 寫成一個(gè)“換數(shù)據(jù)庫只改一個(gè)文件”的類同時(shí)徹底杜絕 SQL 注入。3.1 連接字符串與數(shù)據(jù)庫文件位置// DAL/DbHelper.cs public static class DbHelper { // 數(shù)據(jù)庫文件放在程序運(yùn)行目錄下方便隨項(xiàng)目拷貝 private static readonly string DbPath Path.Combine(AppDomain.CurrentDomain.BaseDirectory, student.db); public static string ConnStr $Data Source{DbPath};; // 每次取連接時(shí)開啟外鍵約束 public static SQLiteConnection GetConn() { var conn new SQLiteConnection(ConnStr); conn.Open(); using (var cmd new SQLiteCommand(PRAGMA foreign_keys ON;, conn)) { cmd.ExecuteNonQuery(); } return conn; } }參數(shù)說明AppDomain.CurrentDomain.BaseDirectory指向 exe 所在目錄比Environment.CurrentDirectory更可靠因?yàn)楹笳邥?huì)隨“打開文件對話框”等操作改變。把PRAGMA放在GetConn里保證每個(gè)連接都生效不用在每個(gè) DAL 方法里重復(fù)寫。3.2 實(shí)體類與 DataTable 的取舍有人喜歡直接用DataTable綁定 DataGridView省去實(shí)體類。小項(xiàng)目可以但一旦要做業(yè)務(wù)校驗(yàn)和單元測試實(shí)體類更清晰。我一般定義簡單的 POCO// Models/Student.cs public class Student { public string StuNo { get; set; } public string Name { get; set; } public string Gender { get; set; } public string ClassName { get; set; } public DateTime? BirthDate { get; set; } }BirthDate用可空DateTime?因?yàn)殇浫霑r(shí)可能留空用DateTime會(huì)默認(rèn)0001-01-01顯示出來很難看。3.3 增刪改查四個(gè)方法的完整寫法// DAL/StudentDal.cs public class StudentDal { // 查詢支持按姓名或?qū)W號模糊搜索 public ListStudent Query(string keyword ) { var list new ListStudent(); using (var conn DbHelper.GetConn()) using (var cmd conn.CreateCommand()) { // 參數(shù)化絕不拼接字符串 cmd.CommandText SELECT StuNo,Name,Gender,ClassName,BirthDate FROM Student WHERE StuNo LIKE kw OR Name LIKE kw ORDER BY StuNo; cmd.Parameters.AddWithValue(kw, $%{keyword}%); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new Student { StuNo reader.GetString(0), Name reader.GetString(1), Gender reader.IsDBNull(2) ? null : reader.GetString(2), ClassName reader.IsDBNull(3) ? null : reader.GetString(3), BirthDate reader.IsDBNull(4) ? (DateTime?)null : DateTime.Parse(reader.GetString(4)) }); } } } return list; } // 新增返回受影響行數(shù)調(diào)用方據(jù)此判斷成功與否 public int Insert(Student s) { using (var conn DbHelper.GetConn()) using (var cmd conn.CreateCommand()) { cmd.CommandText INSERT INTO Student(StuNo,Name,Gender,ClassName,BirthDate) VALUES(no,name,gender,cls,birth); cmd.Parameters.AddWithValue(no, s.StuNo); cmd.Parameters.AddWithValue(name, s.Name); cmd.Parameters.AddWithValue(gender, (object)s.Gender ?? DBNull.Value); cmd.Parameters.AddWithValue(cls, (object)s.ClassName ?? DBNull.Value); cmd.Parameters.AddWithValue(birth, s.BirthDate?.ToString(yyyy-MM-dd) ?? (object)DBNull.Value); return cmd.ExecuteNonQuery(); } } // 修改以學(xué)號為條件更新其余字段 public int Update(Student s) { using (var conn DbHelper.GetConn()) using (var cmd conn.CreateCommand()) { cmd.CommandText UPDATE Student SET Namename,Gendergender, ClassNamecls,BirthDatebirth WHERE StuNono; cmd.Parameters.AddWithValue(no, s.StuNo); cmd.Parameters.AddWithValue(name, s.Name); cmd.Parameters.AddWithValue(gender, (object)s.Gender ?? DBNull.Value); cmd.Parameters.AddWithValue(cls, (object)s.ClassName ?? DBNull.Value); cmd.Parameters.AddWithValue(birth, s.BirthDate?.ToString(yyyy-MM-dd) ?? (object)DBNull.Value); return cmd.ExecuteNonQuery(); } } // 刪除先刪成績再刪學(xué)生避免外鍵報(bào)錯(cuò) public int Delete(string stuNo) { using (var conn DbHelper.GetConn()) using (var tran conn.BeginTransaction()) { try { using (var cmd conn.CreateCommand()) { cmd.Transaction tran; cmd.CommandText DELETE FROM Score WHERE StuNono; cmd.Parameters.AddWithValue(no, stuNo); cmd.ExecuteNonQuery(); cmd.CommandText DELETE FROM Student WHERE StuNono; cmd.ExecuteNonQuery(); } tran.Commit(); return 1; } catch { tran.Rollback(); throw; } } } }邏輯說明查詢用LIKE kw配合%通配符一個(gè)參數(shù)同時(shí)匹配學(xué)號和姓名界面上只需要一個(gè)搜索框。新增和修改里可空字段統(tǒng)一轉(zhuǎn)成DBNull.Value否則 SQLite 會(huì)把 C# 的null當(dāng)成空字符串處理導(dǎo)致IsDBNull判斷失效。刪除操作放在事務(wù)里先刪子表再刪主表這是外鍵約束下最穩(wěn)妥的順序。參數(shù)說明AddWithValue雖然方便但在某些數(shù)據(jù)庫上會(huì)因類型推斷導(dǎo)致索引失效。SQLite 場景下影響不大如果換成 SQL Server 且字段有索引建議顯式指定SQLiteParameter的DbType。3.4 業(yè)務(wù)層的校驗(yàn)放在哪// BLL/StudentBll.cs public class StudentBll { private readonly StudentDal _dal new StudentDal(); public string Add(Student s) { if (string.IsNullOrWhiteSpace(s.StuNo)) return 學(xué)號不能為空; if (string.IsNullOrWhiteSpace(s.Name)) return 姓名不能為空; if (s.StuNo.Length 20) return 學(xué)號長度不能超過 20; // 查重學(xué)號已存在則拒絕 var exist _dal.Query(s.StuNo) .Any(x x.StuNo s.StuNo); if (exist) return 該學(xué)號已存在; return _dal.Insert(s) 0 ? null : 新增失敗; } }返回string而不是拋異常是為了讓 UI 層直接MessageBox.Show(result)新手更容易理解。如果團(tuán)隊(duì)有統(tǒng)一異常規(guī)范改成拋?zhàn)远x異常也可以但要在 UI 層統(tǒng)一捕獲。4. 界面與數(shù)據(jù)聯(lián)動(dòng)DataGridView 綁定和輸入校驗(yàn)界面層最容易寫出“面條代碼”這一章把綁定、刷新、校驗(yàn)三件事拆開講。4.1 DataGridView 綁定 List 而不是 DataTable// UI/MainForm.cs private readonly StudentBll _bll new StudentBll(); private void RefreshGrid(string keyword ) { // 用 BindingList 才能在增刪后自動(dòng)通知界面刷新 var list _bll.Query(keyword); gridStudents.AutoGenerateColumns true; gridStudents.DataSource new BindingListStudent(list); }邏輯說明直接賦ListT也能顯示但增刪后不會(huì)自動(dòng)刷新需要重新賦值。BindingListT實(shí)現(xiàn)了IBindingList配合DataGridView能獲得更好的編輯體驗(yàn)。AutoGenerateColumns true讓列名自動(dòng)取屬性名如果要做中文表頭就在設(shè)計(jì)器里手動(dòng)加列并設(shè)置DataPropertyName。4.2 新增與修改共用一個(gè)窗體的技巧新手常寫兩個(gè)幾乎一樣的窗體改一個(gè)字段要改兩處。我的做法是給編輯窗體加一個(gè)IsEdit標(biāo)志public partial class StudentEditForm : Form { public Student Result { get; private set; } private readonly bool _isEdit; public StudentEditForm(Student stu null) { InitializeComponent(); _isEdit stu ! null; if (_isEdit) { txtStuNo.Text stu.StuNo; txtStuNo.ReadOnly true; // 學(xué)號是主鍵編輯時(shí)不允許改 txtName.Text stu.Name; // ... 其余字段回填 } Text _isEdit ? 編輯學(xué)生 : 新增學(xué)生; } private void btnOk_Click(object sender, EventArgs e) { // 收集輸入交給業(yè)務(wù)層校驗(yàn) Result new Student { StuNo txtStuNo.Text.Trim(), Name txtName.Text.Trim(), Gender rbMale.Checked ? 男 : 女, ClassName txtClass.Text.Trim(), BirthDate dtpBirth.Checked ? dtpBirth.Value : (DateTime?)null }; DialogResult DialogResult.OK; } }參數(shù)說明txtStuNo.ReadOnly true是關(guān)鍵主鍵一旦允許修改就要處理“改學(xué)號后成績表外鍵怎么辦”的連鎖問題實(shí)訓(xùn)階段直接禁掉最省心。dtpBirth.Checked配合ShowCheckBox true讓用戶可以選擇不填生日。4.3 刪除前必須二次確認(rèn)private void btnDelete_Click(object sender, EventArgs e) { if (gridStudents.CurrentRow?.DataBoundItem is not Student s) return; var ok MessageBox.Show( $確定刪除學(xué)號 {s.StuNo} 的學(xué)生及其成績嗎, 確認(rèn)刪除, MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (ok ! DialogResult.Yes) return; try { _bll.Delete(s.StuNo); RefreshGrid(); } catch (Exception ex) { MessageBox.Show(刪除失敗 ex.Message); } }邏輯說明DataBoundItem拿到的是綁定的實(shí)體對象比從單元格里讀字符串再拼對象更可靠。刪除涉及兩張表用 try-catch 兜住避免外鍵異常直接崩掉程序。5. 避坑與排查那些讓實(shí)訓(xùn)報(bào)告返工的問題這一章按“現(xiàn)象 → 原因 → 解決”記錄我踩過的五個(gè)坑每一個(gè)都真實(shí)消耗過時(shí)間。5.1 現(xiàn)象程序在自己電腦能跑拷到別人機(jī)器報(bào)“找不到 student.db”原因數(shù)據(jù)庫文件路徑寫成了絕對路徑或者文件沒有設(shè)置為“始終復(fù)制”。解決用AppDomain.CurrentDomain.BaseDirectory拼路徑并在解決方案資源管理器里把student.db的“復(fù)制到輸出目錄”設(shè)為“如果較新則復(fù)制”。如果數(shù)據(jù)庫是首次運(yùn)行創(chuàng)建的就在程序啟動(dòng)時(shí)判斷文件是否存在不存在則執(zhí)行建表 SQL。5.2 現(xiàn)象刪除學(xué)生時(shí)報(bào)“FOREIGN KEY constraint failed”原因SQLite 的外鍵約束默認(rèn)關(guān)閉但一旦通過PRAGMA開啟刪除主表記錄時(shí)如果子表還有引用就會(huì)失敗。解決按 3.3 的寫法在事務(wù)里先刪Score再刪Student。另一種做法是建表時(shí)加ON DELETE CASCADE但級聯(lián)刪除對新手來說不夠直觀出問題時(shí)更難排查我傾向于顯式刪除。5.3 現(xiàn)象DataGridView 里修改了單元格數(shù)據(jù)庫卻沒變原因DataGridView默認(rèn)只是把數(shù)據(jù)展示出來編輯后沒有回寫數(shù)據(jù)庫的邏輯。解決要么把單元格設(shè)為只讀所有修改走編輯窗體要么監(jiān)聽CellEndEdit事件在事件里調(diào)用 DAL 的 Update。實(shí)訓(xùn)項(xiàng)目推薦前者邏輯更清晰也避免用戶誤改。5.4 現(xiàn)象搜索框輸入%或_后查詢結(jié)果異常原因這兩個(gè)字符在 SQL 的LIKE里是通配符用戶輸入%會(huì)匹配所有記錄。解決在拼接參數(shù)前做轉(zhuǎn)義把%替換成\%_替換成\_并在 SQL 里加ESCAPE \。代碼片段var safe keyword.Replace(\\, \\\\) .Replace(%, \\%) .Replace(_, \\_); cmd.CommandText ... WHERE StuNo LIKE kw ESCAPE \\ ...; cmd.Parameters.AddWithValue(kw, $%{safe}%);5.5 現(xiàn)象實(shí)訓(xùn)報(bào)告里代碼截圖和正文對不上原因先寫報(bào)告后改代碼或者截圖時(shí)用的是舊版本。解決把報(bào)告里的關(guān)鍵代碼用 Markdown 代碼塊貼出來而不是截圖。代碼塊可以直接從源文件復(fù)制改代碼時(shí)同步改報(bào)告版本一致。如果學(xué)校要求截圖就在最終定稿后統(tǒng)一截一次并在圖注里寫明對應(yīng)的類名和方法名。6. 實(shí)訓(xùn)報(bào)告怎么寫才像做過項(xiàng)目而不是抄的最后一章落到報(bào)告本身。很多人代碼寫得不錯(cuò)報(bào)告卻寫成“功能列表 截圖”讀起來像說明書。一份能拿得出手的實(shí)訓(xùn)報(bào)告核心是講清楚“我遇到了什么問題、怎么權(quán)衡、為什么這么選”。6.1 報(bào)告結(jié)構(gòu)建議不要用“第一章 緒論、第二章 需求分析”這種模板硬套。我一般按這個(gè)順序?qū)戫?xiàng)目目標(biāo)與功能邊界明確說清楚做了哪幾個(gè)模塊沒做哪幾個(gè)為什么不做。技術(shù)選型與理由WinForms 還是 WPF、SQLite 還是 SQL Server各寫兩三條理由。數(shù)據(jù)庫設(shè)計(jì)貼建表 SQL說明主鍵、外鍵、約束的設(shè)計(jì)意圖。關(guān)鍵代碼與設(shè)計(jì)思路挑 DAL 的參數(shù)化查詢、BLL 的校驗(yàn)、UI 的綁定三處展開。遇到的問題與解決直接復(fù)用第 5 章的避坑記錄但要用自己的話重寫。不足與改進(jìn)方向誠實(shí)寫“沒有做分頁”“沒有做權(quán)限”比硬吹功能全更加分。6.2 用一張表把測試用例寫清楚報(bào)告里加一張測試表比大段文字描述更有說服力用例編號操作輸入預(yù)期結(jié)果實(shí)際結(jié)果T01新增學(xué)生學(xué)號 2024001姓名張三列表出現(xiàn)該記錄通過T02新增重復(fù)學(xué)號學(xué)號 2024001提示“該學(xué)號已存在”通過T03刪除有成績的學(xué)生學(xué)號 2024001學(xué)生和成績同時(shí)刪除通過T04搜索特殊字符輸入%不匹配全部記錄通過6.3 一個(gè)讓報(bào)告加分的細(xì)節(jié)把“后悔藥”寫進(jìn)去我在報(bào)告里會(huì)專門留一小節(jié)叫“如果重做我會(huì)怎么改”。比如把連接字符串抽到配置文件而不是硬編碼把 DataGridView 的列寬和格式在CellFormatting事件里統(tǒng)一處理給 BLL 加單元測試而不是全靠手點(diǎn)。這一節(jié)不需要長三四條就夠但能明顯看出你是真的動(dòng)手做過而不是復(fù)制粘貼。6.4 最后說一個(gè)習(xí)慣每次改完代碼先跑一遍新增、修改、刪除、搜索四條主流程再截圖更新報(bào)告。這個(gè)習(xí)慣幫我省掉了無數(shù)次“報(bào)告和代碼對不上”的返工。實(shí)訓(xùn)項(xiàng)目不難難的是把每一步都留下可復(fù)現(xiàn)的記錄。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取