预演
This commit is contained in:
1 parent
727a429abf
commit
33ae991e86
16 files changed
+7117
-8
No files matched your search
@@ -84,6 +84,17 @@ public sealed class CoursesController(
|
||||
x.Nature,
|
||||
x.AssessmentMethod,
|
||||
x.Description,
|
||||
PrerequisiteCourseIds = x.Prerequisites
|
||||
.OrderBy(item => item.PrerequisiteCourse!.Code)
|
||||
.Select(item => item.PrerequisiteCourseId),
|
||||
Prerequisites = x.Prerequisites
|
||||
.OrderBy(item => item.PrerequisiteCourse!.Code)
|
||||
.Select(item => new
|
||||
{
|
||||
item.PrerequisiteCourseId,
|
||||
item.PrerequisiteCourse!.Code,
|
||||
item.PrerequisiteCourse.Name
|
||||
}),
|
||||
x.IsEnabled,
|
||||
x.SortOrder,
|
||||
x.CreatedAt,
|
||||
@@ -146,9 +157,6 @@ public sealed class CoursesController(
|
||||
CourseRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var validation = await ValidateAsync(request, cancellationToken);
|
||||
if (validation is not null) return validation;
|
||||
|
||||
var entity = new Course
|
||||
{
|
||||
Code = request.Code.Trim(),
|
||||
@@ -166,6 +174,16 @@ public sealed class CoursesController(
|
||||
IsEnabled = request.IsEnabled,
|
||||
SortOrder = request.SortOrder
|
||||
};
|
||||
var validation = await ValidateAsync(entity.Id, request, cancellationToken);
|
||||
if (validation is not null) return validation;
|
||||
|
||||
entity.Prerequisites = NormalizePrerequisiteIds(request)
|
||||
.Select(prerequisiteId => new CoursePrerequisite
|
||||
{
|
||||
CourseId = entity.Id,
|
||||
PrerequisiteCourseId = prerequisiteId
|
||||
})
|
||||
.ToList();
|
||||
db.Courses.Add(entity);
|
||||
return await SaveAsync(entity.Id, true, cancellationToken);
|
||||
}
|
||||
@@ -177,10 +195,12 @@ public sealed class CoursesController(
|
||||
CourseRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.Courses.FindAsync([id], cancellationToken);
|
||||
var entity = await db.Courses
|
||||
.Include(x => x.Prerequisites)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (entity is null) return NotFound();
|
||||
if (!CanManage(entity.CollegeId, entity.Nature)) return Forbid();
|
||||
var validation = await ValidateAsync(request, cancellationToken);
|
||||
var validation = await ValidateAsync(id, request, cancellationToken);
|
||||
if (validation is not null) return validation;
|
||||
|
||||
entity.Code = request.Code.Trim();
|
||||
@@ -197,6 +217,19 @@ public sealed class CoursesController(
|
||||
entity.Description = Normalize(request.Description);
|
||||
entity.IsEnabled = request.IsEnabled;
|
||||
entity.SortOrder = request.SortOrder;
|
||||
var prerequisiteIds = NormalizePrerequisiteIds(request).ToHashSet();
|
||||
db.CoursePrerequisites.RemoveRange(
|
||||
entity.Prerequisites.Where(x =>
|
||||
!prerequisiteIds.Contains(x.PrerequisiteCourseId)));
|
||||
foreach (var prerequisiteId in prerequisiteIds.Except(
|
||||
entity.Prerequisites.Select(x => x.PrerequisiteCourseId)))
|
||||
{
|
||||
entity.Prerequisites.Add(new CoursePrerequisite
|
||||
{
|
||||
CourseId = entity.Id,
|
||||
PrerequisiteCourseId = prerequisiteId
|
||||
});
|
||||
}
|
||||
return await SaveAsync(entity.Id, false, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -212,6 +245,7 @@ public sealed class CoursesController(
|
||||
}
|
||||
|
||||
private async Task<ActionResult?> ValidateAsync(
|
||||
Guid courseId,
|
||||
CourseRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -224,9 +258,62 @@ public sealed class CoursesController(
|
||||
return ValidationProblem("所选课程分类不存在或已停用。");
|
||||
if (request.LectureHours + request.PracticeHours > request.TotalHours)
|
||||
return ValidationProblem("讲授学时与实践学时之和不能超过总学时。");
|
||||
|
||||
var prerequisiteIds = NormalizePrerequisiteIds(request).ToArray();
|
||||
if (prerequisiteIds.Contains(courseId))
|
||||
return ValidationProblem("课程不能把自身设置为先修课程。");
|
||||
var accessiblePrerequisiteCount = await ScopedCourses()
|
||||
.CountAsync(x =>
|
||||
prerequisiteIds.Contains(x.Id) &&
|
||||
x.IsEnabled,
|
||||
cancellationToken);
|
||||
if (accessiblePrerequisiteCount != prerequisiteIds.Length)
|
||||
return ValidationProblem("包含不存在、已停用或不在当前数据范围内的先修课程。");
|
||||
if (await CreatesPrerequisiteCycleAsync(
|
||||
courseId,
|
||||
prerequisiteIds,
|
||||
cancellationToken))
|
||||
return ValidationProblem("先修关系不能形成循环依赖。");
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<bool> CreatesPrerequisiteCycleAsync(
|
||||
Guid courseId,
|
||||
IReadOnlyCollection<Guid> prerequisiteIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (prerequisiteIds.Count == 0) return false;
|
||||
|
||||
var edges = await db.CoursePrerequisites.AsNoTracking()
|
||||
.Where(x => x.CourseId != courseId)
|
||||
.Select(x => new { x.CourseId, x.PrerequisiteCourseId })
|
||||
.ToListAsync(cancellationToken);
|
||||
var prerequisitesByCourse = edges
|
||||
.GroupBy(x => x.CourseId)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.Select(x => x.PrerequisiteCourseId).ToArray());
|
||||
|
||||
foreach (var prerequisiteId in prerequisiteIds)
|
||||
{
|
||||
var pending = new Stack<Guid>();
|
||||
var visited = new HashSet<Guid>();
|
||||
pending.Push(prerequisiteId);
|
||||
while (pending.TryPop(out var candidate))
|
||||
{
|
||||
if (candidate == courseId) return true;
|
||||
if (!visited.Add(candidate) ||
|
||||
!prerequisitesByCourse.TryGetValue(candidate, out var next))
|
||||
continue;
|
||||
foreach (var item in next) pending.Push(item);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<Guid> NormalizePrerequisiteIds(CourseRequest request) =>
|
||||
(request.PrerequisiteCourseIds ?? []).Distinct();
|
||||
|
||||
private IQueryable<Course> ScopedCourses()
|
||||
{
|
||||
var scope = currentUserDataScope.Current;
|
||||
@@ -307,4 +394,5 @@ public sealed record CourseRequest(
|
||||
AssessmentMethod AssessmentMethod,
|
||||
[MaxLength(1000)] string? Description,
|
||||
bool IsEnabled,
|
||||
int SortOrder);
|
||||
int SortOrder,
|
||||
IReadOnlyCollection<Guid>? PrerequisiteCourseIds = null);
|
||||
Reference in new issue
Block a user