课程库 Excel 导入导出已完成:
支持下载标准导入模板。 导出严格沿用当前关键词、学院、分类、课程性质和状态筛选。 以课程编码为唯一键:存在则更新,不存在则新增。 校验学院编码、课程分类、课程性质、学分学时及考核方式。 整批原子导入,任一行错误则全部不写入,并提示具体行号。 学院管理员仍只能维护本学院的专业课和实践课,无法借 Excel 越权。 界面入口沿用现有课程库工具栏样式。
This commit is contained in:
1 parent
5a7a8d530b
commit
d7cf3f9e76
24 files changed
+3710
-32
No files matched your search
@@ -120,6 +120,44 @@ public sealed class BaseDataController(AppDbContext db) : ControllerBase
|
||||
})
|
||||
.ToListAsync(cancellationToken));
|
||||
|
||||
[HttpGet("course-categories")]
|
||||
public async Task<ActionResult<IReadOnlyCollection<CourseCategory>>> GetCourseCategories(
|
||||
CancellationToken cancellationToken) =>
|
||||
await db.CourseCategories.AsNoTracking()
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.ThenBy(x => x.Code)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
[HttpPost("course-categories")]
|
||||
[Authorize(Roles = Administrators)]
|
||||
public async Task<ActionResult<CourseCategory>> CreateCourseCategory(
|
||||
CatalogRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = new CourseCategory
|
||||
{
|
||||
Code = request.Code.Trim(),
|
||||
Name = request.Name.Trim(),
|
||||
SortOrder = request.SortOrder,
|
||||
IsEnabled = request.IsEnabled
|
||||
};
|
||||
return await CreateAsync(entity, nameof(GetCourseCategories), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("course-categories/{id:guid}")]
|
||||
[Authorize(Roles = Administrators)]
|
||||
public async Task<ActionResult<CourseCategory>> UpdateCourseCategory(
|
||||
Guid id,
|
||||
CatalogRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.CourseCategories.FindAsync([id], cancellationToken);
|
||||
if (entity is null) return NotFound();
|
||||
ApplyCatalog(entity, request);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return entity;
|
||||
}
|
||||
|
||||
[HttpPost("majors")]
|
||||
[Authorize(Roles = Administrators)]
|
||||
public async Task<ActionResult<Major>> CreateMajor(
|
||||
@@ -403,6 +441,7 @@ public sealed class BaseDataController(AppDbContext db) : ControllerBase
|
||||
"terms" => await db.AcademicTerms.FindAsync([id], cancellationToken),
|
||||
"buildings" => await db.Buildings.FindAsync([id], cancellationToken),
|
||||
"classrooms" => await db.Classrooms.FindAsync([id], cancellationToken),
|
||||
"course-categories" => await db.CourseCategories.FindAsync([id], cancellationToken),
|
||||
_ => null
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase
|
||||
["classes"] = ["编码", "名称", "所属专业编码", "年级", "辅导员工号", "排序", "状态"],
|
||||
["terms"] = ["编码", "名称", "学年", "学期季", "开始日期", "结束日期", "当前学期", "状态"],
|
||||
["buildings"] = ["编码", "名称", "所属校区编码", "排序", "状态"],
|
||||
["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "设备", "排序", "状态"]
|
||||
["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "设备", "排序", "状态"],
|
||||
["course-categories"] = ["编码", "名称", "排序", "状态"]
|
||||
};
|
||||
|
||||
[HttpGet("{kind}/template")]
|
||||
@@ -90,6 +91,8 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase
|
||||
"terms" => await ImportTermsAsync(rows, errors, cancellationToken),
|
||||
"buildings" => await ImportBuildingsAsync(rows, errors, cancellationToken),
|
||||
"classrooms" => await ImportClassroomsAsync(rows, errors, cancellationToken),
|
||||
"course-categories" => await ImportCourseCategoriesAsync(
|
||||
rows, errors, cancellationToken),
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
@@ -147,6 +150,10 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase
|
||||
.OrderBy(x => x.Code).ToListAsync(cancellationToken))
|
||||
.Select(x => Row(x.Code, x.Name, x.Building!.Code, x.Capacity, x.RoomType,
|
||||
x.Equipment, x.SortOrder, Status(x.IsEnabled))).ToList(),
|
||||
"course-categories" => (await db.CourseCategories.AsNoTracking()
|
||||
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
|
||||
.ToListAsync(cancellationToken))
|
||||
.Select(x => Row(x.Code, x.Name, x.SortOrder, Status(x.IsEnabled))).ToList(),
|
||||
_ => []
|
||||
};
|
||||
|
||||
@@ -471,6 +478,36 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase
|
||||
return new(created, updated, rows.Count);
|
||||
}
|
||||
|
||||
private async Task<ExcelImportResult> ImportCourseCategoriesAsync(
|
||||
IReadOnlyList<ExcelRow> rows,
|
||||
List<string> errors,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await db.CourseCategories.ToDictionaryAsync(
|
||||
x => x.Code,
|
||||
StringComparer.OrdinalIgnoreCase,
|
||||
cancellationToken);
|
||||
var created = 0;
|
||||
var updated = 0;
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var code = Required(row, "编码", errors);
|
||||
var name = Required(row, "名称", errors);
|
||||
if (code is null || name is null) continue;
|
||||
var entity = existing.GetValueOrDefault(code);
|
||||
if (entity is null)
|
||||
{
|
||||
entity = new CourseCategory { Code = code, Name = name };
|
||||
db.CourseCategories.Add(entity);
|
||||
existing[code] = entity;
|
||||
created++;
|
||||
}
|
||||
else updated++;
|
||||
ApplyCatalog(entity, row, name, errors);
|
||||
}
|
||||
return new(created, updated, rows.Count);
|
||||
}
|
||||
|
||||
private ActionResult ImportValidationProblem(IReadOnlyList<string> errors)
|
||||
{
|
||||
foreach (var error in errors.Take(50))
|
||||
@@ -582,6 +619,7 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase
|
||||
"terms" => "学期",
|
||||
"buildings" => "教学楼",
|
||||
"classrooms" => "教室",
|
||||
"course-categories" => "课程分类",
|
||||
_ => "基础数据"
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Loaded 3 of 24 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user