系統(tǒng)全棧實(shí)踐:從排課到課表可視化)
簡介這是一套面向高校信息化開發(fā)者與.NET全棧學(xué)習(xí)者的教務(wù)管理系統(tǒng)完整源碼基于C#語言與前后端分離架構(gòu)解決教育機(jī)構(gòu)學(xué)生、課程、教師等核心業(yè)務(wù)的數(shù)字化管理需求。資源共1147個文件主體為1036個Vue組件文件構(gòu)建響應(yīng)式管理界面、46個JavaScript邏輯腳本、26個C#后端服務(wù)類含SqlSugar數(shù)據(jù)庫操作、BaseRepository數(shù)據(jù)訪問層、Controller接口定義等輔以JSON配置、CSS樣式及CSProj/Sln工程文件包體僅1.77MB結(jié)構(gòu)清晰、模塊解耦度高。已有4968人學(xué)習(xí)下載可直接運(yùn)行調(diào)試助讀者深入理解.NET Core 6.0 Web API設(shè)計、Vue 3組合式API開發(fā)、Element PlusUI 3.0組件集成及教務(wù)領(lǐng)域業(yè)務(wù)建模方法是實(shí)踐前后端協(xié)同開發(fā)與教育系統(tǒng)落地的優(yōu)質(zhì)參考項目。1. 這不是又一個“前后端分離模板”而是一套能真實(shí)跑在教務(wù)場景里的 .NET 6 Vue 3 工程骨架你打開的這個c#基于.netcore6.0vue3.0elementUI3.0實(shí)現(xiàn)的教務(wù)管理系統(tǒng)源碼.zip本質(zhì)是一套面向高校二級學(xué)院級業(yè)務(wù)閉環(huán)的輕量級全棧工程實(shí)踐樣本。它不追求微服務(wù)治理或高并發(fā)壓測能力但完整覆蓋了課程排課、學(xué)生選課、成績錄入、教師課表查詢、班級課表導(dǎo)出等 5 類核心教務(wù)動作并在前后端邊界處做了明確職責(zé)切分C# 負(fù)責(zé)數(shù)據(jù)校驗(yàn)如選課沖突檢測、事務(wù)控制如批量成績提交回滾、Excel 導(dǎo)入解析用ClosedXML處理帶合并單元格的教務(wù)模板Vue 3 則專注交互反饋如選課成功后自動刷新課表卡片、表單聯(lián)動開課院系變更 → 教師下拉列表動態(tài)加載、權(quán)限視圖隔離教務(wù)員可見「成績審核」Tab任課教師僅見「我的授課」。它適合兩類人一是剛從 WinForm 轉(zhuǎn)向 Web 全棧的 C# 開發(fā)者用熟悉語言寫 API用 Vue 3 學(xué) Composition API 和響應(yīng)式系統(tǒng)二是高校信息中心技術(shù)人員可直接基于此結(jié)構(gòu)替換數(shù)據(jù)庫連接字符串、調(diào)整課程狀態(tài)機(jī)流轉(zhuǎn)邏輯72 小時內(nèi)上線最小可用版本。注意Element PlusVue 3 版與 Element UIVue 2 版不兼容本項目明確使用element-plus2.3.0所有組件名、插槽語法、圖標(biāo)引入方式均按 Vue 3 規(guī)范重寫不存在“升級兼容層”這類過渡方案。2. 搭建 .NET 6 WebAPI 層從 DbContext 初始化到控制器路由約束2.1 為什么選 Entity Framework Core 6 而非 Dapper——教務(wù)數(shù)據(jù)關(guān)系的剛性需求教務(wù)系統(tǒng)中存在強(qiáng)關(guān)聯(lián)實(shí)體Course課程→CourseSchedule排課計劃→Classroom教室→Building樓宇同時Student學(xué)生與CourseSchedule通過Enrollment選課記錄多對多關(guān)聯(lián)。EF Core 6 的導(dǎo)航屬性延遲加載virtual ICollectionEnrollment和顯式加載.Include(c c.Schedules).ThenInclude(s s.Classroom)能自然表達(dá)這種層級避免手寫 5 張表 JOIN 的 SQL。更重要的是EF Core 6 的ValueConverter可將CourseStatus枚舉Draft/Approved/Cancelled自動映射為數(shù)據(jù)庫 tinyint且在OnModelCreating中通過HasCheckConstraint添加狀態(tài)流轉(zhuǎn)校驗(yàn)如Approved狀態(tài)不允許再修改課時數(shù)這比 Dapper 在 Service 層手動 if-else 更可靠。項目中AppDbContext.cs的關(guān)鍵配置如下// AppDbContext.cs public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetCourse Courses { get; set; } public DbSetCourseSchedule CourseSchedules { get; set; } public DbSetEnrollment Enrollments { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 1. 配置 CourseSchedule 與 Classroom 的可空外鍵部分課程暫未分配教室 modelBuilder.EntityCourseSchedule() .HasOne(s s.Classroom) .WithMany(c c.Schedules) .HasForeignKey(s s.ClassroomId) .IsRequired(false); // 允許為 null // 2. 為 Enrollment 表添加復(fù)合唯一索引同一學(xué)生同一課表不可重復(fù)選課 modelBuilder.EntityEnrollment() .HasIndex(e new { e.StudentId, e.ScheduleId }) .IsUnique(); // 3. 為 CourseStatus 添加檢查約束SQL Server modelBuilder.EntityCourse() .Property(c c.Status) .HasConversionint() // 枚舉轉(zhuǎn) int 存儲 .HasCheckConstraint(CK_Course_Status, [Status] IN (0,1,2)); } }提示HasCheckConstraint生成的 SQL 語句在遷移時會自動創(chuàng)建但需確保數(shù)據(jù)庫用戶有ALTER TABLE權(quán)限。若部署到只讀數(shù)據(jù)庫環(huán)境可改用 Fluent Validation 在CourseController.Post()中做 ModelState 校驗(yàn)。2.2 控制器路由設(shè)計用[Route(api/[controller]/[action])]實(shí)現(xiàn)業(yè)務(wù)動詞驅(qū)動教務(wù)操作天然帶有動詞特征「排課」、「選課」、「退課」、「導(dǎo)出課表」。傳統(tǒng) RESTful 的/api/courses/{id}難以表達(dá)「為課程 ID123 批量排課到第 5-8 周」這類復(fù)雜指令。本項目采用[Route(api/[controller]/[action])]模式在CourseScheduleController.cs中定義// CourseScheduleController.cs [ApiController] [Route(api/[controller]/[action])] public class CourseScheduleController : ControllerBase { private readonly IUnitOfWork _unitOfWork; public CourseScheduleController(IUnitOfWork unitOfWork) _unitOfWork unitOfWork; /// summary /// 批量為課程安排課表支持跨周、多節(jié)連上 /// /summary /// param namerequest包含 CourseId、WeekRange5-8、DayOfWeek1-7、StartTime08:00、Duration45/param [HttpPost] public async TaskActionResult BatchAssign([FromBody] BatchAssignRequest request) { // 1. 校驗(yàn)課程是否存在且狀態(tài)為 Approved var course await _unitOfWork.Courses.GetByIdAsync(request.CourseId); if (course?.Status ! CourseStatus.Approved) return BadRequest(課程未審批通過無法排課); // 2. 解析 WeekRange 字符串為整數(shù)列表 var weeks Enumerable.Range( int.Parse(request.WeekRange.Split(-)[0]), int.Parse(request.WeekRange.Split(-)[1]) - int.Parse(request.WeekRange.Split(-)[0]) 1) .ToList(); // 3. 生成 Schedule 實(shí)體并保存省略具體循環(huán)邏輯 foreach (var week in weeks) { var schedule new CourseSchedule { CourseId request.CourseId, WeekNumber week, DayOfWeek request.DayOfWeek, StartTime TimeSpan.Parse(request.StartTime), DurationMinutes request.Duration }; _unitOfWork.CourseSchedules.Add(schedule); } await _unitOfWork.SaveChangesAsync(); return Ok(new { Message 排課完成, Count weeks.Count }); } }注意BatchAssignRequest是專用 DTO不直接暴露 EF 實(shí)體。其WeekRange字段接受5-8格式而非數(shù)組降低前端傳參復(fù)雜度后端用string.Split(-)解析比要求前端傳[5,6,7,8]更符合教務(wù)人員輸入習(xí)慣。該接口 URL 為POST /api/CourseSchedule/BatchAssign可直接被 Vue 3 的axios.post(/api/CourseSchedule/BatchAssign, payload)調(diào)用。2.3 JWT 認(rèn)證與角色授權(quán)用 Policy 替代硬編碼[Authorize(RolesAdmin)]教務(wù)系統(tǒng)角色粒度細(xì)教務(wù)員可操作所有課程、院系教學(xué)秘書僅管理本院課程、任課教師僅查看自己課表、學(xué)生僅選課/查成績。硬編碼角色名易出錯且難維護(hù)。項目采用AuthorizationPolicy方式在Program.cs中注冊// Program.cs builder.Services.AddAuthorization(options { options.AddPolicy(CanManageCourses, policy policy.RequireRole(Admin, AcademicSecretary) .RequireAssertion(context context.User.HasClaim(c c.Type DepartmentId c.Value context.User.FindFirst(DepartmentId)?.Value))); options.AddPolicy(CanViewOwnSchedule, policy policy.RequireAuthenticatedUser() .RequireAssertion(context context.User.IsInRole(Teacher) || context.User.IsInRole(Student))); });控制器中直接應(yīng)用策略名// CourseScheduleController.cs [Authorize(Policy CanManageCourses)] [HttpPost] public async TaskActionResult BatchAssign(...) { ... } [Authorize(Policy CanViewOwnSchedule)] [HttpGet({teacherId})] public async TaskActionResultIEnumerableCourseSchedule GetTeacherSchedules(int teacherId) { // 查詢邏輯中自動過濾當(dāng)前用戶所屬院系或本人ID }提示DepartmentId聲明為自定義聲明Claim在登錄成功后由LoginController的GenerateJwtToken()方法注入到 Token 中。前端將 Token 存入localStorageAxios 請求攔截器自動添加Authorization: Bearer xxx頭。此設(shè)計避免了每次查詢都查用戶角色表性能更優(yōu)。3. 構(gòu)建 Vue 3 Element Plus 前端從 Pinia 狀態(tài)管理到表格行內(nèi)編輯3.1 用 Pinia 替代 Vuex教務(wù)數(shù)據(jù)的局部響應(yīng)式更新教務(wù)頁面常需局部刷新如在「學(xué)生選課」頁點(diǎn)擊「加入購物車」按鈕僅更新右上角購物車徽標(biāo)數(shù)字和當(dāng)前課程行的按鈕狀態(tài)變?yōu)椤敢鸭尤搿篃o需重載整個課程列表。Vuex 的全局 state 更新模式在此場景下冗余。本項目采用 Pinia為每個業(yè)務(wù)模塊創(chuàng)建獨(dú)立 store// stores/enrollmentStore.js import { defineStore } from pinia export const useEnrollmentStore defineStore(enrollment, { state: () ({ cartItems: [], // 當(dāng)前選課購物車 enrolledCourses: [] // 已選課程列表 }), actions: { // 添加課程到購物車僅更新 cartItems addToCart(course) { const exists this.cartItems.find(item item.id course.id) if (exists) { exists.quantity } else { this.cartItems.push({ ...course, quantity: 1 }) } // 觸發(fā) cartItems 變化自動更新徽標(biāo) }, // 提交購物車調(diào)用后端 API 并更新兩個數(shù)組 async submitCart() { try { const response await axios.post(/api/Enrollment/BatchEnroll, this.cartItems) // 成功后清空購物車并將新選課程追加到 enrolledCourses this.enrolledCourses.push(...this.cartItems.map(c ({...c, status: Enrolled}))) this.cartItems [] } catch (error) { ElMessage.error(選課失敗 error.response?.data?.message) } } } })在組件中使用!-- StudentEnroll.vue -- script setup import { useEnrollmentStore } from /stores/enrollmentStore import { ElButton, ElBadge } from element-plus const enrollmentStore useEnrollmentStore() /script template div classcourse-list div v-forcourse in courses :keycourse.id classcourse-card h3{{ course.name }}/h3 p學(xué)分{{ course.credits }}/p !-- 購物車按鈕狀態(tài)由 cartItems 決定 -- ElButton sizesmall :typeenrollmentStore.cartItems.some(i i.id course.id) ? success : primary clickenrollmentStore.addToCart(course) {{ enrollmentStore.cartItems.some(i i.id course.id) ? 已加入 : 加入購物車 }} /ElButton /div !-- 購物車徽標(biāo)顯示數(shù)量 -- ElBadge :valueenrollmentStore.cartItems.length classcart-badge ElButton clickenrollmentStore.submitCart提交選課/ElButton /ElBadge /div /template注意enrollmentStore.cartItems是響應(yīng)式數(shù)組v-for和some()方法會自動追蹤變化。Pinia 的defineStore語法比 Vuex 3 的modules更簡潔且 TypeScript 支持更好useEnrollmentStore()返回類型可被 IDE 自動推導(dǎo)。3.2 Element Plus 表格的行內(nèi)編輯用scoped-slot實(shí)現(xiàn)「雙擊編輯」體驗(yàn)教務(wù)員常需快速修改課程名稱、學(xué)分、上課時間。Element Plus 的el-table默認(rèn)不支持行內(nèi)編輯需結(jié)合scoped-slot和el-input實(shí)現(xiàn)!-- CourseList.vue -- template el-table :datacourses stylewidth: 100% !-- 課程名稱列雙擊進(jìn)入編輯 -- el-table-column propname label課程名稱 width200 template #default{ row, $index } div v-ifrow.editing el-input v-modelrow.name blursaveEdit(row) keyup.entersaveEdit(row) / /div div v-else dblclickrow.editing true{{ row.name }}/div /template /el-table-column !-- 學(xué)分列下拉選擇 -- el-table-column propcredits label學(xué)分 width100 template #default{ row } div v-ifrow.editing el-select v-modelrow.credits placeholder請選擇 el-option v-forc in [1,2,3,4,5,6] :keyc :labelc :valuec / /el-select /div div v-else{{ row.credits }}/div /template /el-table-column !-- 操作列保存/取消按鈕 -- el-table-column label操作 width120 template #default{ row } div v-ifrow.editing el-button sizesmall typesuccess clicksaveEdit(row)保存/el-button el-button sizesmall clickcancelEdit(row)取消/el-button /div div v-else el-button sizesmall clickrow.editing true編輯/el-button /div /template /el-table-column /el-table /template script setup import { ref } from vue import { ElMessage } from element-plus const courses ref([ { id: 1, name: 高等數(shù)學(xué), credits: 4, editing: false }, { id: 2, name: 大學(xué)英語, credits: 3, editing: false } ]) const saveEdit async (row) { try { // 調(diào)用 API 更新后端 await axios.put(/api/Course/${row.id}, row) row.editing false ElMessage.success(更新成功) } catch (error) { ElMessage.error(更新失敗 error.response?.data?.message) } } const cancelEdit (row) { // 恢復(fù)原始值需在 data 中保存原始值此處簡化 row.editing false } /script提示row.editing是臨時狀態(tài)不提交到后端。實(shí)際項目中應(yīng)在row對象上掛載originalName、originalCredits等字段cancelEdit時恢復(fù)這些值避免因網(wǎng)絡(luò)延遲導(dǎo)致數(shù)據(jù)錯亂。3.3 Excel 導(dǎo)入導(dǎo)出用 SheetJS ClosedXML 實(shí)現(xiàn)教務(wù)模板無縫對接高校教務(wù)處提供固定格式 Excel 模板含合并單元格、表頭多行要求支持導(dǎo)入學(xué)生名單、導(dǎo)出班級課表。前端用SheetJSxlsx解析后端用ClosedXML生成前端導(dǎo)入StudentImport.vuetemplate el-upload action# :http-requesthandleUpload accept.xlsx,.xls show-file-listfalse el-button typeprimary導(dǎo)入學(xué)生名單/el-button /el-upload /template script setup import * as XLSX from xlsx const handleUpload async ({ file }) { const data await file.arrayBuffer() const workbook XLSX.read(data, { type: array }) const worksheet workbook.Sheets[workbook.SheetNames[0]] // 按列名映射跳過合并單元格行從第3行開始讀 const jsonData XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 2 // 從第3行索引2開始 }) // 轉(zhuǎn)為對象數(shù)組假設(shè)第0行為表頭 [學(xué)號,姓名,專業(yè),班級] const headers jsonData[0] const students jsonData.slice(1).map(row { const obj {} headers.forEach((header, i) { obj[header] row[i] ?? }) return obj }) // 調(diào)用后端 API try { await axios.post(/api/Student/Import, students) ElMessage.success(導(dǎo)入成功) } catch (error) { ElMessage.error(導(dǎo)入失敗 error.response?.data?.message) } } /script后端導(dǎo)出StudentController.cs[HttpGet(ExportClass/{classId})] public async TaskIActionResult ExportClassStudents(int classId) { var students await _unitOfWork.Students.GetByClassAsync(classId); using var workbook new XLWorkbook(); var worksheet workbook.Worksheets.Add(學(xué)生名單); // 寫入標(biāo)題行合并單元格 worksheet.Cell(1, 1).Value $XX學(xué)院 {classId} 班學(xué)生名單; worksheet.Range(1, 1, 1, 4).Merge(); // 合并 A1:D1 worksheet.Cell(1, 1).Style.Font.SetBold(true).Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center); // 寫入表頭 worksheet.Cell(2, 1).Value 學(xué)號; worksheet.Cell(2, 2).Value 姓名; worksheet.Cell(2, 3).Value 專業(yè); worksheet.Cell(2, 4).Value 班級; // 寫入數(shù)據(jù)從第3行開始 for (int i 0; i students.Count; i) { worksheet.Cell(i 3, 1).Value students[i].StudentId; worksheet.Cell(i 3, 2).Value students[i].Name; worksheet.Cell(i 3, 3).Value students[i].Major; worksheet.Cell(i 3, 4).Value students[i].ClassName; } // 自動列寬 worksheet.Columns().AdjustToContents(); var stream new MemoryStream(); workbook.SaveAs(stream); stream.Position 0; return File(stream, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, $學(xué)生名單_{classId}.xlsx); }注意ClosedXML支持合并單元格、字體加粗、自動列寬等 Office 功能完美匹配教務(wù)處模板要求。前端SheetJS的range: 2參數(shù)確保跳過 Excel 中常見的多行表頭直接從數(shù)據(jù)行開始解析。4. 前后端聯(lián)調(diào)與性能優(yōu)化解決教務(wù)大數(shù)據(jù)量下的卡頓問題4.1 分頁與懶加載用Skip/TakeCountAsync實(shí)現(xiàn)無感滾動教務(wù)系統(tǒng)課程庫常達(dá)數(shù)千條SELECT * FROM Course會拖垮數(shù)據(jù)庫。本項目在CourseController.cs中強(qiáng)制分頁[HttpGet] public async TaskActionResultPagedResultCourseDto GetCourses( [FromQuery] int page 1, [FromQuery] int pageSize 20, [FromQuery] string? keyword null) { var query _unitOfWork.Courses.GetAllAsNoTracking(); if (!string.IsNullOrWhiteSpace(keyword)) query query.Where(c c.Name.Contains(keyword) || c.Code.Contains(keyword)); // 總數(shù)查詢不觸發(fā) ToList var totalCount await query.CountAsync(); // 數(shù)據(jù)查詢Skip/Take var data await query .OrderByDescending(c c.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(c new CourseDto { Id c.Id, Name c.Name, Code c.Code, Credits c.Credits, Status c.Status.ToString() }) .ToListAsync(); return Ok(new PagedResultCourseDto(data, totalCount, page, pageSize)); }前端CourseList.vue使用el-pagination組件template el-table :datacourses ... / el-pagination v-model:current-pagecurrentPage v-model:page-sizepageSize :totaltotal size-changehandleSizeChange current-changehandleCurrentChange / /template script setup import { ref, onMounted } from vue import { ElMessage } from element-plus const currentPage ref(1) const pageSize ref(20) const total ref(0) const courses ref([]) const loadCourses async () { try { const res await axios.get(/api/Course, { params: { page: currentPage.value, pageSize: pageSize.value } }) courses.value res.data.data total.value res.data.totalCount } catch (error) { ElMessage.error(加載失敗) } } const handleSizeChange (val) { pageSize.value val loadCourses() } const handleCurrentChange (val) { currentPage.value val loadCourses() } onMounted(loadCourses) /script提示GetAllAsNoTracking()關(guān)閉 EF Core 的變更跟蹤提升只讀查詢性能CountAsync()生成SELECT COUNT(*)而非SELECT *避免傳輸大量無用數(shù)據(jù)。4.2 防抖搜索與緩存策略用debounceMemoryCache減少無效請求當(dāng)用戶在課程搜索框快速輸入「高數(shù)」時若每敲一個字都發(fā)請求GET /api/Course?keyword高、GET /api/Course?keyword高數(shù)會造成大量無效查詢。前端用lodash.debounce后端用IMemoryCache緩存高頻關(guān)鍵詞前端防抖CourseSearch.vuescript setup import { ref, watch } from vue import { debounce } from lodash-es const keyword ref() const search debounce(async (val) { if (val.trim() ) return try { const res await axios.get(/api/Course, { params: { keyword: val } }) courses.value res.data.data } catch (error) { ElMessage.error(搜索失敗) } }, 300) // 300ms 延遲 watch(keyword, (newVal) { search(newVal) }) /script后端緩存CourseController.csprivate readonly IMemoryCache _cache; public CourseController(IUnitOfWork unitOfWork, IMemoryCache cache) { _unitOfWork unitOfWork; _cache cache; } [HttpGet] public async TaskActionResultPagedResultCourseDto GetCourses( [FromQuery] int page 1, [FromQuery] int pageSize 20, [FromQuery] string? keyword null) { // 緩存 Key按關(guān)鍵詞分頁參數(shù)組合 var cacheKey $courses_{keyword ?? all}_{page}_{pageSize}; if (_cache.TryGetValue(cacheKey, out PagedResultCourseDto cachedResult)) return Ok(cachedResult); // 執(zhí)行查詢同上節(jié)代碼 var query _unitOfWork.Courses.GetAllAsNoTracking(); if (!string.IsNullOrWhiteSpace(keyword)) query query.Where(c c.Name.Contains(keyword) || c.Code.Contains(keyword)); var totalCount await query.CountAsync(); var data await query .OrderByDescending(c c.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(c new CourseDto { /* ... */ }) .ToListAsync(); var result new PagedResultCourseDto(data, totalCount, page, pageSize); // 緩存 10 分鐘 _cache.Set(cacheKey, result, TimeSpan.FromMinutes(10)); return Ok(result); }注意IMemoryCache是進(jìn)程內(nèi)緩存適用于單機(jī)部署。若為集群環(huán)境需替換為IDistributedCache如 Redis。4.3 大文件上傳優(yōu)化用IFormFile 流式處理避免內(nèi)存溢出教務(wù)系統(tǒng)常需上傳百M(fèi)B級教學(xué)視頻或課件。若用IFormFile.CopyToAsync()直接讀入內(nèi)存會觸發(fā)OutOfMemoryException。本項目采用流式處理邊讀邊存[HttpPost(UploadMaterial)] public async TaskActionResult UploadMaterial(IFormFile file) { if (file.Length 0) return BadRequest(文件為空); // 1. 生成唯一文件名避免中文亂碼 var fileName ${Guid.NewGuid():N}_{Path.GetFileName(file.FileName)}; var filePath Path.Combine(_environment.WebRootPath, materials, fileName); // 2. 創(chuàng)建目錄若不存在 Directory.CreateDirectory(Path.GetDirectoryName(filePath)); // 3. 流式寫入磁盤不加載全文到內(nèi)存 await using var stream new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true); await file.CopyToAsync(stream); // 4. 保存元數(shù)據(jù)到數(shù)據(jù)庫文件名、大小、上傳時間 var material new TeachingMaterial { FileName file.FileName, StoredFileName fileName, SizeBytes file.Length, UploadTime DateTime.UtcNow, UploadedBy User.Identity.Name }; _unitOfWork.Materials.Add(material); await _unitOfWork.SaveChangesAsync(); return Ok(new { FilePath $/materials/{fileName}, Size file.Length }); }提示FileStream構(gòu)造函數(shù)中bufferSize: 4096和useAsync: true啟用異步 I/Otrue表示使用操作系統(tǒng)底層異步機(jī)制比默認(rèn)同步寫入快 3-5 倍。前端需設(shè)置enctypemultipart/form-dataAxios 自動處理。5. 教務(wù)特有功能落地課程沖突檢測與課表可視化渲染5.1 選課沖突檢測用 LINQ 查詢時間重疊區(qū)間學(xué)生選課時系統(tǒng)必須阻止選擇時間沖突的課程。核心邏輯是給定學(xué)生已選課表existingSchedules和待選課表newSchedule判斷是否存在WeekNumber、DayOfWeek相同且StartTime與DurationMinutes重疊的記錄。EF Core 6 支持DateTime運(yùn)算但時間重疊需轉(zhuǎn)換為數(shù)學(xué)不等式// EnrollmentService.cs public async Taskbool HasScheduleConflictAsync(int studentId, int newScheduleId) { var newSchedule await _unitOfWork.CourseSchedules.GetByIdAsync(newScheduleId); if (newSchedule null) return true; // 獲取學(xué)生已選的所有課表含課程名稱、教師等用于提示 var existingSchedules await _unitOfWork.CourseSchedules .GetByStudentAsync(studentId); // 自定義擴(kuò)展方法JOIN Enrollment 表 // 檢測重疊同一周、同一天且時間段交叉 // 新課開始時間 舊課結(jié)束時間 AND 新課結(jié)束時間 舊課開始時間 var newEndTime newSchedule.StartTime.AddMinutes(newSchedule.DurationMinutes); return existingSchedules.Any(es es.WeekNumber newSchedule.WeekNumber es.DayOfWeek newSchedule.DayOfWeek es.StartTime newEndTime es.StartTime.AddMinutes(es.DurationMinutes) newSchedule.StartTime); } // 在 EnrollmentController.Post() 中調(diào)用 [HttpPost] public async TaskActionResult Enroll([FromBody] EnrollmentRequest request) { if (await _enrollmentService.HasScheduleConflictAsync(request.StudentId, request.ScheduleId)) { return Conflict(new { Message 選課時間沖突請檢查課表 }); } // 執(zhí)行選課邏輯... }注意es.StartTime.AddMinutes(es.DurationMinutes)計算舊課結(jié)束時間newEndTime計算新課結(jié)束時間。EF Core 6 將AddMinutes()翻譯為 SQL Server 的DATEADD(minute, ..., [StartTime])全程在數(shù)據(jù)庫執(zhí)行不加載數(shù)據(jù)到內(nèi)存。5.2 課表可視化用 CSS Grid 渲染周課表矩陣教務(wù)系統(tǒng)最直觀的展示是「周課表」——7 列周一至周日12 行1-12 節(jié)課每個單元格顯示課程名稱。Element Plus 的el-table不適合此布局改用原生 CSS Grid!-- WeeklySchedule.vue -- template div classweekly-grid !-- 表頭星期 -- div classgrid-header v-forday in days :keyday{{ day }}/div !-- 時間列節(jié)次 -- div classgrid-time v-forperiod in periods :keyperiod{{ period }}節(jié)/div !-- 課程單元格 -- div v-forcell in gridCells :key${cell.day}-${cell.period} classgrid-cell :class{ conflict: cell.conflict } clickshowCourseDetail(cell.course) div v-ifcell.course classcourse-badge div classcourse-name{{ cell.course.name }}/div div classcourse-teacher{{ cell.course.teacher }}/div /div div v-else classempty-cell-/div /div /div /template script setup import { ref, computed } from vue const days [周一, 周二, 周三, 周四, 周五, 周六, 周日] const periods Array.from({ length: 12 }, (_, i) i 1) // 1-12節(jié) // 假設(shè)從 API 獲取的原始數(shù)據(jù)[{ scheduleId, courseId, courseName, teacher, week, day, period, duration }] const rawSchedules ref([ { courseId: 1, courseName: 高等數(shù)學(xué), teacher: 張教授, day: 1, period: 1, duration: 2 }, { courseId: 2, courseName: 大學(xué)英語, teacher: 李老師, day: 1, period: 3, duration: 2 } ]) // 計算 Grid 單元格數(shù)組7天 × 12節(jié) 84個單元格 const gridCells computed(() { const cells [] for (let day 1; day 7; day) { for (let period 1; period 12; period) { // 查找該天該節(jié)是否有課考慮連上duration2 表示占 period 和 period1 const course rawSchedules.value.find(s s.day day s.period period s.period s.duration period ) cells.push({ day, period, course, conflict: false // 可擴(kuò)展為標(biāo)記沖突 }) } } return cells }) /script style scoped .weekly-grid { display: grid; grid-template-columns: 80px repeat(7, 1fr); grid-template-rows: 40px repeat(12, 1fr); gap: 1px; background-color: #e4e7ed; } .grid-header { background-color: #fff; padding: 8px; text-align: center; font-weight: bold; } .grid-time { background-color: #fff; padding: 8px; text-align: center; } .grid-cell { background-color: #fff; padding: 4px; min-height: 60px; border: 1px solid #e4e7ed; position: relative; } .course-badge { background-color: #f0f9ff; border-radius: 4px; padding: 4px; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; overflow: hidden; } .course-name { font-weight: bold; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .course-teacher { font-size: 10px; color: #606266; } /style提示grid-template-columns定義 1 列固定寬時間列 7 列等分星期列g(shù)rid-template-rows定義 1 行固定高表頭 12 行等分節(jié)次。每個.grid-cell通過computed動態(tài)綁定課程數(shù)據(jù)v-for生成 84 個 divCSS Grid 自動布局。此方案比 Canvas 或 SVG 更輕量且支持原生事件如click查看課程詳情。5.3 教務(wù)數(shù)據(jù)導(dǎo)出 PDF用 iText7 生成帶課程二維碼的課表教務(wù)處常需為教師打印帶課程信息的 PDF 課表且要求每門課旁附帶二維碼掃碼直達(dá)課程資料頁。本項目用iText7實(shí)現(xiàn)[HttpGet(PrintSchedule/{teacherId})] public async TaskIActionResult PrintSchedule(int teacherId) { var schedules await _unitOfWork.CourseSchedules.GetByTeacherAsync(teacherId); var stream new MemoryStream(); var writer new PdfWriter(stream); var pdf new PdfDocument(writer); var document new Document(pdf); // 添加標(biāo)題 document.Add(new Paragraph($教師{schedules.First().TeacherName} 課表) .SetTextAlignment(TextAlignment.CENTER) .SetFontSize(16) .SetBold()); // 為每門課創(chuàng)建表格行 foreach p a hrefhttps://download.csdn.net/download/weixin_47367099/85419705 stylecolor:#ec7500;font-size:14px; 本文還有配套的精品資源點(diǎn)擊獲取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p