课程库 Excel 导入导出已完成:

支持下载标准导入模板。
导出严格沿用当前关键词、学院、分类、课程性质和状态筛选。
以课程编码为唯一键:存在则更新,不存在则新增。
校验学院编码、课程分类、课程性质、学分学时及考核方式。
整批原子导入,任一行错误则全部不写入,并提示具体行号。
学院管理员仍只能维护本学院的专业课和实践课,无法借 Excel 越权。
界面入口沿用现有课程库工具栏样式。
This commit is contained in:
2026-07-24 18:56:25 +08:00 Unverified
parent 5a7a8d530b
commit d7cf3f9e76
24 changed files with 3710 additions and 32 deletions
@@ -28,6 +28,7 @@ public sealed class CoursesController(
int pageSize = 20,
string? keyword = null,
Guid? collegeId = null,
Guid? categoryId = null,
CourseNature? nature = null,
bool? isEnabled = null,
CancellationToken cancellationToken = default)
@@ -42,6 +43,8 @@ public sealed class CoursesController(
var source = ScopedCourses().AsNoTracking();
if (collegeId.HasValue)
source = source.Where(x => x.CollegeId == collegeId.Value);
if (categoryId.HasValue)
source = source.Where(x => x.CourseCategoryId == categoryId.Value);
if (nature.HasValue)
source = source.Where(x => x.Nature == nature.Value);
if (isEnabled.HasValue)
@@ -69,6 +72,9 @@ public sealed class CoursesController(
x.EnglishName,
x.CollegeId,
CollegeName = x.College!.Name,
x.CourseCategoryId,
CategoryCode = x.CourseCategory != null ? x.CourseCategory.Code : null,
CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null,
x.Credits,
x.TotalHours,
x.LectureHours,
@@ -106,6 +112,7 @@ public sealed class CoursesController(
Name = request.Name.Trim(),
EnglishName = Normalize(request.EnglishName),
CollegeId = request.CollegeId,
CourseCategoryId = request.CourseCategoryId,
Credits = request.Credits,
TotalHours = request.TotalHours,
LectureHours = request.LectureHours,
@@ -137,6 +144,7 @@ public sealed class CoursesController(
entity.Name = request.Name.Trim();
entity.EnglishName = Normalize(request.EnglishName);
entity.CollegeId = request.CollegeId;
entity.CourseCategoryId = request.CourseCategoryId;
entity.Credits = request.Credits;
entity.TotalHours = request.TotalHours;
entity.LectureHours = request.LectureHours;
@@ -167,6 +175,10 @@ public sealed class CoursesController(
if (!CanManage(request.CollegeId, request.Nature)) return Forbid();
if (!await db.Colleges.AnyAsync(x => x.Id == request.CollegeId, cancellationToken))
return ValidationProblem("所选学院不存在。");
if (!await db.CourseCategories.AnyAsync(
x => x.Id == request.CourseCategoryId && x.IsEnabled,
cancellationToken))
return ValidationProblem("所选课程分类不存在或已停用。");
if (request.LectureHours + request.PracticeHours > request.TotalHours)
return ValidationProblem("讲授学时与实践学时之和不能超过总学时。");
return null;
@@ -242,6 +254,7 @@ public sealed record CourseRequest(
[Required, MaxLength(100)] string Name,
[MaxLength(150)] string? EnglishName,
Guid CollegeId,
Guid CourseCategoryId,
[Range(typeof(decimal), "0.1", "99")] decimal Credits,
[Range(1, 1000)] int TotalHours,
[Range(0, 1000)] int LectureHours,