
校園活動管理系統幾乎是 ASP.NET Core MVC 學習路上繞不開的“標配項目”。無論是課程設計、畢業設計還是公司內部的活動管理小需求它的業務場景都足夠典型有數據的增刪改查、有用戶權限、有狀態流轉、有前后臺頁面。很多人以為這只是一個“簡單的 CRUD”但真正動手做才發現難的不是列表頁和表單頁而是報名這個核心動作背后的業務約束。我的判斷是一個校園活動報名管理系統做得是否“像工程”不看它寫了多少頁面而看它能不能把三個問題處理干凈——名額不能被搶超、同一個人不能重復報名、活動生命周期不能被錯誤狀態污染。如果你正準備用 ASP.NET Core MVC 做這個項目這篇文章會幫你把從建項目、建數據模型到報名并發控制、管理后臺導出的完整鏈路走通。讀完之后你至少能收獲四件事第一一套可以直接運行的關鍵代碼覆蓋活動、報名、管理后臺三個模塊第二報名場景下防重復、防超賣的具體實現思路這是很多教程不會展開講的第三一個可復用的排查清單第四寫課程設計和簡歷項目時可以拿出來講的工程細節。1. 這篇文章真正要解決的問題很多人第一次做后臺管理系統流程是這樣的建一個數據庫建幾個表然后用腳手架生成頁面管理員能發活動、學生能報名。第一天很順利第二天開始出現問題同一個學生連續點了兩次報名數據庫里出現兩條記錄。后臺把活動人數上限設置成 50結果 51 個人報名成功。活動已經結束頁面還能繼續報名。這些問題有一個共同點它們都不是頁面層能解決的而是業務層沒有把規則約束好。如果只是做教學演示這些問題可以忽略但如果要寫成課設、畢設或者作為項目經驗寫進簡歷這些邊界情況恰恰是面試官最關心的問題。所以整篇文章圍繞一條主線展開數據模型如何避免重復報名報名邏輯如何原子更新名額活動狀態如何統一管理管理員后臺如何隔離權限。這條主線就是“業務約束的工程化實現”。2. 系統需求與功能拆解從真實場景來看校園活動報名系統至少包含兩類角色、兩類業務流程。先看角色。學生角色登錄后瀏覽活動、查看詳情、報名/取消報名、查看已報名記錄管理員角色登錄后維護活動信息、查看報名名單、統計參與人數、上下線活動。權限設計不需要一開始做得很復雜但學生和管理員的邊界必須清晰。再看核心對象。業務圍繞兩個實體展開活動Activity和報名記錄Registration。活動有名稱、時間、地點、容量、已報名人數、狀態報名記錄則有學生信息、報名時間、狀態。這里要特別注意不要只把“已報名人數”當成一個查詢結果它必須是一個數據庫字段并且在報名事務中原子更新。后面會詳細說明這個字段為什么不能用內存計算替代。活動狀態按業務流轉可以拆成四種未開始報名、報名中、已滿員、已結束。報名記錄狀態拆成兩種正常報名、已取消。如果學校場景需要加入班主任審核可以再增加一個“審核中”狀態但最小實現先不做復雜審批流程。模塊功能點角色權限活動瀏覽列表、詳情、狀態展示登錄用戶報名管理報名、取消、我的報名學生活動管理新增、編輯、上下線管理員名單管理查看報名名單、導出 CSV管理員這個表格定義了本文所有功能開發的最小范圍。做課程設計或者畢業設計時先把這些功能跑通再根據自己的場景去擴充審核、簽到、消息通知等功能。3. 核心技術選型與運行環境技術棧的選擇原則是不求最新但求最穩。以下組合足夠支撐一個結構清晰、可演示、可擴展的 ASP.NET Core MVC 項目。框架ASP.NET Core MVC。它提供完整的 Model-View-Controller 分層適合這類業務邏輯清晰的后臺管理系統。ORMEF Core。使用 Code First 方式建表省去手寫建表 SQL也能保證實體與數據庫結構同步。數據庫開發環境用 SQLite生產環境建議換成 SQL Server 或 MySQL。SQLite 零配置、開箱即用對課程設計非常友好后面會講到并發場景下 SQL Server 的鎖機制更可靠。前端Bootstrap 5 jQuery通過 Layout 頁面統一引入。認證ASP.NET Core Cookie 認證。相比 IdentityCookie 認證更輕量也更適合在教程中展示登錄、角色授權的基本原理。環境準備方面需要安裝 .NET SDK建議 8.0 或更高具體以實際安裝為準、Visual Studio 2022 或 VS Code以及 EF Core 工具。安裝完成后打開命令行檢查版本dotnet --version dotnet ef --version如果dotnet ef命令不存在說明沒有安裝全局工具執行下面命令安裝dotnet tool install --global dotnet-ef安裝全局工具后可能需要重啟終端才能讓dotnet ef命令生效。4. 數據模型設計與數據庫遷移數據模型是整個系統最該花時間設計的地方。很多問題如果在建表階段就能避免后面不必寫大量防御代碼。4.1 活動實體先在Models目錄下創建活動實體類// 文件路徑Models/Activity.cs using System.ComponentModel.DataAnnotations; namespace CampusActivity.Models { public class Activity { public int Id { get; set; } [Required(ErrorMessage 活動名稱不能為空)] [StringLength(100)] public string Title { get; set; } string.Empty; public string Description { get; set; } string.Empty; [Display(Name 開始時間)] public DateTime StartTime { get; set; } [Display(Name 結束時間)] public DateTime EndTime { get; set; } [Display(Name 活動地點)] public string Location { get; set; } string.Empty; [Display(Name 人數上限)] public int MaxParticipants { get; set; } // 已報名人數必須持久化到數據庫 public int RegisteredCount { get; set; } // 0未開始報名 1報名中 2已滿員 3已結束 public int Status { get; set; } public DateTime CreatedAt { get; set; } DateTime.Now; public ListRegistration Registrations { get; set; } new(); } }這里有一個關鍵設計RegisteredCount不是通過Registrations.Count()實時計算的而是在報名成功時同步更新。原因是后續報名邏輯需要一條原子 UPDATE 語句同時完成“判斷名額未滿”和“名額加一”如果每次都先查詢列表再統計數量在高并發下就會出問題。4.2 報名記錄實體報名記錄要記錄“誰報了哪個活動”并且必須設置唯一索引防止同一學生重復報名同一活動// 文件路徑Models/Registration.cs using System.ComponentModel.DataAnnotations; namespace CampusActivity.Models { public class Registration { public int Id { get; set; } public int ActivityId { get; set; } public Activity? Activity { get; set; } [Display(Name 學號)] public string StudentId { get; set; } string.Empty; [Display(Name 姓名)] public string StudentName { get; set; } string.Empty; [Display(Name 聯系電話)] public string Phone { get; set; } string.Empty; public DateTime RegisterAt { get; set; } public DateTime? CancelAt { get; set; } // 1正常 0已取消 public int Status { get; set; } } }在配置唯一索引時索引字段應該包含(ActivityId, StudentId)同時要處理“已取消的記錄不參與唯一判定”的問題。最穩妥的做法是取消時把 StudentId 改寫為帶后綴的字符串或者使用“軟刪除加唯一過濾索引”。SQLite 和 SQL Server 都支持過濾索引但為了保持代碼可讀性本文先在業務層檢查重復再用唯一索引兜底。4.3 數據庫上下文與遷移創建AppDbContext類并配置唯一索引// 文件路徑Data/AppDbContext.cs using CampusActivity.Models; using Microsoft.EntityFrameworkCore; namespace CampusActivity.Data { public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetActivity Activities SetActivity(); public DbSetRegistration Registrations SetRegistration(); protected override void OnModelCreating(ModelBuilder modelBuilder) { // 唯一索引同一學生不能重復報名同一活動 modelBuilder.EntityRegistration() .HasIndex(r new { r.ActivityId, r.StudentId }) .IsUnique(); } } }然后在Program.cs中注冊 DbContext。以 SQLite 為例// 文件路徑Program.cs using CampusActivity.Data; using Microsoft.EntityFrameworkCore; var builder WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddDbContextAppDbContext(options options.UseSqlite(builder.Configuration.GetConnectionString(DefaultConnection))); var app builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(/Home/Error); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllerRoute( name: default, pattern: {controllerHome}/{actionIndex}/{id?}); app.Run();在appsettings.json中配置連接字符串{ ConnectionStrings: { DefaultConnection: Data Sourceactivity.db }, Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } }, AllowedHosts: * }接下來執行遷移命令生成數據庫dotnet ef migrations add Init dotnet ef database update執行成功后目錄下會出現activity.db文件。如果遷移失敗先檢查dotnet ef工具版本和 .NET SDK 版本是否匹配再查看完整錯誤日志。這一步是后面所有功能的基礎值得花時間確認它沒問題。5. 活動列表與詳情頁實現活動頁面是學生端的入口核心價值在于“狀態展示準確”。一個活動是報名中、已滿員還是已結束必須一眼就能看到。先創建活動控制器// 文件路徑Controllers/ActivityController.cs using CampusActivity.Data; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace CampusActivity.Controllers { public class ActivityController : Controller { private readonly AppDbContext _context; public ActivityController(AppDbContext context) { _context context; } public async TaskIActionResult Index() { var activities await _context.Activities .OrderByDescending(a a.StartTime) .ToListAsync(); return View(activities); } public async TaskIActionResult Detail(int id) { var activity await _context.Activities .Include(a a.Registrations) .FirstOrDefaultAsync(a a.Id id); if (activity null) { return NotFound(); } return View(activity); } } }列表頁視圖需要根據活動狀態顯示不同按鈕。可以封裝一個簡單的狀態判斷方法// 文件路徑Models/ActivityStatusHelper.cs namespace CampusActivity.Models { public static class ActivityStatusHelper { public static string GetStatusText(Activity activity) { if (activity.Status 3 || activity.EndTime DateTime.Now) { return 已結束; } if (activity.Status 1 activity.RegisteredCount activity.MaxParticipants) { return 已滿員; } if (activity.Status 1) { return 報名中; } return activity.Status 0 ? 未開始 : 已結束; } } }視圖中的關鍵代碼是循環展示活動和詳情入口這里不再完整展示頁面 CSS只給出核心判斷邏輯model IEnumerableCampusActivity.Models.Activity foreach (var item in Model) { var statusText CampusActivity.Models.ActivityStatusHelper.GetStatusText(item); var canRegister item.Status 1 item.RegisteredCount item.MaxParticipants; div classcard mb-3 div classcard-body h5 classcard-titleitem.Title/h5 p classcard-text 時間item.StartTime.ToString(yyyy-MM-dd HH:mm) - item.EndTime.ToString(HH:mm)br/ 地點item.Locationbr/ 剩余名額(item.MaxParticipants - item.RegisteredCount) / item.MaxParticipants /p span classbadge (canRegister ? bg-success : bg-secondary)statusText/span a asp-actionDetail asp-route-iditem.Id classbtn btn-primary btn-sm查看詳情/a /div /div }這里容易踩的坑是只判斷Status 1就允許報名卻忘了檢查EndTime。如果活動創建時管理員沒有及時改狀態就會出現“活動時間已經過了頁面還能報名”的尷尬。所以狀態判斷必須結合時間字段不能只依賴一個枚舉字段。6. 報名與取消的核心業務邏輯這是整篇文章最重要的一章。報名功能看起來只是“給表插入一條記錄”但實際涉及三個硬約束名額不能超賣、同一學生不能重復報名、活動狀態必須是報名中。這三個約束遇到并發請求時會產生完全不同的結果。6.1 為什么不能只做查詢再判斷很多教程會這樣寫報名邏輯var activity await _context.Activities.FindAsync(activityId); if (activity null) return (false, 活動不存在); if (activity.Status ! 1) return (false, 活動當前不可報名); if (activity.RegisteredCount activity.MaxParticipants) return (false, 名額已滿); // 檢查是否重復報名 var exists await _context.Registrations.AnyAsync(r r.ActivityId activityId r.StudentId studentId); if (exists) return (false, 你已經報過名了); activity.RegisteredCount; await _context.SaveChangesAsync();這段代碼單獨運行完全沒問題但并發場景下會超賣。原因是RegisteredCount先被讀進內存程序判斷“名額未滿”然后把內存中的值加一再保存回數據庫。假設兩個請求同時讀到RegisteredCount 49兩個進程都認為自己拿下了最后一個名額最終結果就是 51 人報名成功。問題根源在于“判斷”和“更新”不是原子的。解決思路也很簡單把判斷與更新合并為一條原子 SQL。6.2 使用原子 UPDATE 防超賣服務層方法的核心代碼如下// 文件路徑Services/RegistrationService.cs using CampusActivity.Data; using CampusActivity.Models; using Microsoft.EntityFrameworkCore; namespace CampusActivity.Services { public class RegistrationService { private readonly AppDbContext _context; public RegistrationService(AppDbContext context) { _context context; } public async Task(bool Success, string Message) RegisterAsync( int activityId, string studentId, string studentName, string phone) { await using var transaction await _context.Database.BeginTransactionAsync(); try { // 條件 UPDATE名額未滿且狀態為報名中才會執行成功 var rows await _context.Database.ExecuteSqlRawAsync( UPDATE Activities SET RegisteredCount RegisteredCount 1 WHERE Id {0} AND RegisteredCount MaxParticipants AND Status 1, activityId); if (rows 0) { return (false, 活動不存在、已結束或名額已滿); } // 業務層先檢查重復報名提升友好性 var duplicate await _context.Registrations .AnyAsync(r r.ActivityId activityId r.StudentId studentId r.Status 1); if (duplicate) { // 如果重復報名需要回滾已經占用的名額 await _context.Database.ExecuteSqlRawAsync( UPDATE Activities SET RegisteredCount RegisteredCount - 1 WHERE Id {0}, activityId); return (false, 你已經報名過該活動不能重復報名); } _context.Registrations.Add(new Registration { ActivityId activityId, StudentId studentId, StudentName studentName, Phone phone, RegisterAt DateTime.Now, Status 1 }); await _context.SaveChangesAsync(); await transaction.CommitAsync(); return (true, 報名成功); } catch { await transaction.RollbackAsync(); throw; } } } }這里有幾個關鍵點需要說明。第一UPDATE Activities SET RegisteredCount RegisteredCount 1 WHERE ...是一條原子語句數據庫會為匹配的行加鎖如果兩個請求同時進入第二個請求會等待第一個提交后再執行。當第一個請求把名額增加到上限后第二個請求的RegisteredCount MaxParticipants條件不滿足受到影響的行數為 0從而拒絕報名。第二唯一索引作為最終防線。即使業務層的duplicate檢查因為極端并發出現漏判向Registrations表插入重復記錄時數據庫唯一索引仍然會拋異常事務回滾后名額不會丟失。第三業務層的duplicate檢查不是多余的。它能讓用戶在正常場景下得到“你已經報名過”的友好提示而不是在提交時遇到一個看不懂的數據庫錯誤。這是工程化實現與裸寫 SQL 之間的重要區別。6.3 在控制器中調用服務控制器負責讀取用戶身份、做參數綁定然后調用服務層方法// 文件路徑Controllers/ActivityController.cs using CampusActivity.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; public class ActivityController : Controller { private readonly RegistrationService _registrationService; public ActivityController(RegistrationService registrationService) { _registrationService registrationService; } [HttpPost] [Authorize] [ValidateAntiForgeryToken] public async TaskIActionResult Register(int id) { var studentId User.Identity?.Name ?? string.Empty; var studentName User.FindFirst(Name)?.Value ?? string.Empty; var phone User.FindFirst(Phone)?.Value ?? string.Empty; var result await _registrationService.RegisterAsync(id, studentId, studentName, phone); TempData[Message] result.Message; return RedirectToAction(nameof(Detail), new { id }); } }報名按鈕放在詳情頁中并且必須帶上防偽令牌form asp-actionRegister asp-controllerActivity methodpost input typehidden nameid valueModel.Id / button typesubmit classbtn btn-success立即報名/button /form如果視圖中忘記添加Html.AntiForgeryToken()提交時會收到 400 錯誤。這是 ASP.NET Core 默認的防偽保護機制不能因為麻煩而關閉。6.4 取消報名并釋放名額取消報名與報名是對稱操作核心動作是把報名記錄狀態改為已取消同時把活動已報名人數減一。同樣需要放在事務中public async Task(bool Success, string Message) CancelAsync(int activityId, string studentId) { await using var transaction await _context.Database.BeginTransactionAsync(); try { var registration await _context.Registrations .FirstOrDefaultAsync(r r.ActivityId activityId r.StudentId studentId r.Status 1); if (registration null) { return (false, 沒有找到有效報名記錄); } registration.Status 0; registration.CancelAt DateTime.Now; await _context.Database.ExecuteSqlRawAsync( UPDATE Activities SET RegisteredCount RegisteredCount - 1 WHERE Id {0} AND RegisteredCount 0, activityId); await _context.SaveChangesAsync(); await transaction.CommitAsync(); return (true, 取消成功); } catch { await transaction.RollbackAsync(); throw; } }唯一要注意的是UPDATE語句中增加了RegisteredCount 0條件防止數據異常時名額被減成負數。這屬于防御性編程實際場景中不會出現但加上成本很低。7. 管理員后臺與數據導出管理員后臺的職責很清晰管理活動信息、查看報名名單、導出數據。本系統使用 Cookie 認證加角色區分權限。7.1 登錄與角色授權在Program.cs中注冊認證服務using Microsoft.AspNetCore.Authentication.Cookies; builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options { options.LoginPath /Account/Login; options.AccessDeniedPath /Account/Denied; });控制器只需要添加特性即可限制權限// 文件路徑Controllers/AdminController.cs using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; [Authorize(Roles Admin)] public class AdminController : Controller { // 只有 Admin 角色可以訪問 }在登錄邏輯中管理員登錄成功后把角色寫入 Cookie。學生登錄時角色為Student管理員登錄時角色為Admin。7.2 活動管理的核心代碼管理員新增活動的代碼與普通表單提交類似關鍵是狀態設置。管理員提交活動時Status可以先設為0未開始報名到報名開始時再手動改為1。這樣能避免誤操作導致活動一發布就開放報名。7.3 導出報名名單為 CSV導出 CSV 是管理后臺的常見需求。以下代碼將報名名單導出為 CSV 文件下載// 文件路徑Controllers/AdminController.cs using System.Text; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; [Authorize(Roles Admin)] public async TaskIActionResult ExportCsv(int activityId) { var activity await _context.Activities.FindAsync(activityId); if (activity null) { return NotFound(); } var registrations await _context.Registrations .Where(r r.ActivityId activityId r.Status 1) .OrderBy(r r.RegisterAt) .ToListAsync(); var sb new StringBuilder(); sb.AppendLine(序號,學號,姓名,電話,報名時間); int index 1; foreach (var r in registrations) { sb.AppendLine(${index},{r.StudentId},{r.StudentName},{r.Phone},{r.RegisterAt:yyyy-MM-dd HH:mm:ss}); index; } // 添加 UTF-8 BOM避免 Excel 打開中文亂碼 var preamble Encoding.UTF8.GetPreamble(); var body Encoding.UTF8.GetBytes(sb.ToString()); var fileBytes preamble.Concat(body).ToArray(); return File(fileBytes, text/csv; charsetutf-8, $活動報名名單_{activity.Title}.csv); }導出 CSV 這個方法雖然簡單但有兩個細節值得注意一是中文文件名需要瀏覽器兼容建議用 URL 編碼后的文件名二是必須添加 UTF-8 BOM否則 Excel 直接打開 CSV 時中文會亂碼。如果項目中使用 EPPlus 或 NPOI 導出真正的 xlsx 文件效果會更好但 CSV 作為最小實現已經足夠。8. 運行驗證與常見問題排查整個項目開發完成后通過下面命令啟動dotnet run瀏覽器打開控制臺輸出的地址通常是https://localhost:5001或http://localhost:5000。建議按以下順序驗證核心功能注冊或登錄學生賬號創建測試活動并設置人數上限為 1。使用一個學生賬號連續報名兩次第二次應提示“你已經報名過該活動”。再創建一個上限為 1 的活動使用兩個不同學生賬號同時報名確認只有一個學生報名成功。取消報名后確認名額釋放另一個學生可以報名成功。管理員登錄后創建一個活動修改狀態、查看報名名單、導出 CSV。如果過程中出現問題優先查看控制臺日志。下表整理了本項目最常見的六類問題問題現象可能原因排查方式解決方案執行遷移失敗SDK 版本不匹配或未安裝 ef 工具查看完整錯誤日志安裝與 SDK 匹配的dotnet-ef全局工具頁面返回 403未登錄或角色不匹配查看用戶 Cookie 中的角色聲明確認登錄邏輯中寫了角色 Claim并重新登錄表單提交返回 400缺少防偽令牌檢查視圖表單在 form 中添加Html.AntiForgeryToken()報名后名額超賣未使用原子 UPDATE在服務層打斷點觀察并發改為條件 UPDATE 唯一索引同一學生重復報名缺少唯一索引查詢Registrations表記錄增加(ActivityId, StudentId)唯一索引CSV 用 Excel 打開亂碼缺少 UTF-8 BOM用記事本打開文件查看編碼在文件輸出前寫入 BOM對于并發問題如果本地開發環境不容易模擬可以先用 Postman 或 JMeter 對報名接口做壓測觀察最終報名的成功數量是否超過活動上限。這一步能直觀驗證防超賣邏輯是否真的生效。9. 最佳實踐與工程建議項目跑通只是第一步真正能寫進球歷、能在答辯中講清楚的內容往往是一些工程化細節。這里整理幾條建議供你在開發過程中有意識地落實。第一報名核心邏輯必須放在 Service 層 Controller 只負責讀取用戶身份、做參數綁定、調用服務、返回視圖。這樣做的最大好處是如果以后要擴展到 Web API 接口不需要重寫業務代碼。項目里的RegistrationService就是一個可以被 Controller 或 API Controller 同時復用的類。第二不要用字符串拼接 SQL。本文給出的ExecuteSqlRawAsync使用了{0}占位符這是參數化查詢不會引入 SQL 注入風險。如果使用$UPDATE ... {activityId}在用戶輸入可控的場景下就會出大問題。第三管理員后臺的每項操作都要記錄日志至少記錄“誰在什么時間做了什么操作”。這里可以使用ILoggerAdminController輸出到日志也可以在數據庫里建一個操作日志表。對于校園活動管理系統日志表的效果更直觀答辯時也更有說服力。第四所有表單必須啟用防偽令牌。ASP.NET Core 默認對 POST 請求做防偽校驗但這只在表單中包含令牌時才生效。如果視圖用 AJAX 提交需要在請求頭中攜帶令牌。這一步關系到最基本的站點安全不能省略。第五生產環境不要繼續用 SQLite。雖然 SQLite 對課程設計完全夠用但它寫入時是整庫鎖并發報名場景下性能上限很低。推薦生產環境使用 SQL Server 或 MySQL連接字符串的改動很小但并發能力完全不同。第六給熱門查詢加索引。活動列表頁通常按照開始時間倒序展示可以給Activities表的(Status, StartTime)建復合索引提升列表查詢速度。數據量小的時候看不出差別但作為工程習慣值得養成。第七如果活動容量非常大比如全校級活動幾千個名額可以把熱點計數放在 Redis 中報名成功后異步寫入數據庫。但這屬于進階方案本文的最小實現用原子 UPDATE 已經能解決大多數問題。不要在課設階段就引入 Redis避免過度設計。10. 總結與后續學習方向這個項目做成什么程度算“完成度高”不是頁面多不多、動畫炫不炫而是核心業務規則是否穩固。名額不會超賣、重復報名會被攔截、活動狀態不會混亂、權限邊界清晰這些都是比頁面數量更值錢的工程能力。如果你想把項目進一步升級可以按順序做三件事。第一把數據庫從 SQLite 換成 SQL Server重新跑通所有功能這個過程中你會理解不同數據庫在事務和并發上的差異。第二給系統增加 Excel 導出和報名名單的分頁查詢貼近真實后臺管理的使用習慣。第三嘗試把報名接口改造成 RESTful API用 Postman 測試接口在并發場景下的表現。以上練習做完后這個項目的深度已經超過大多數課設和簡歷項目。后續學習方向上值得花時間研究的是 ASP.NET Core Identity、JWT 認證、EF Core 性能優化以及 Repository 模式在項目中的應用。這些內容都能在現有系統上自然延伸不會出現“學完不知道放哪里”的問題。最后提醒一句任何生產環境的上線操作都要先在測試環境驗證涉及數據庫結構的變更先做備份權限配置遵循最小權限原則。這個習慣越早養成后面做真實項目時越省心。