3 Commits
4 changed files with 178 additions and 92 deletions

No files matched your search

@@ -391,81 +391,90 @@ public sealed class EvaluationsController(
if (taskIds.Count == 0) if (taskIds.Count == 0)
return Ok(new { Tasks = Array.Empty<object>() }); return Ok(new { Tasks = Array.Empty<object>() });
var setups = await db.EvaluationSetups.AsNoTracking() var taskSummaries = await db.EvaluationRecords.AsNoTracking()
.Where(x => x.Status == EvaluationSetupStatus.Closed) .WhereIn(taskIds, record => record.TeachingTaskId)
.Include(x => x.Dimensions.OrderBy(d => d.SortOrder)) .Where(record => record.EvaluationSetup!.Status == EvaluationSetupStatus.Closed)
.OrderByDescending(x => x.AcademicTerm!.StartDate) .GroupBy(record => new
{
record.EvaluationSetupId,
SetupName = record.EvaluationSetup!.Name,
SetupStartDate = record.EvaluationSetup.AcademicTerm!.StartDate,
record.TeachingTaskId,
TaskNumber = record.TeachingTask!.TaskNumber,
TaskName = record.TeachingTask.Name,
record.TeachingTask.AcademicTermId,
CourseCode = record.TeachingTask.Course!.Code,
CourseName = record.TeachingTask.Course.Name
})
.Select(group => new
{
group.Key,
StudentCount = group.Count(),
AverageTotal = group.Average(record =>
record.Scores.Sum(score => score.Score))
})
.OrderByDescending(x => x.Key.SetupStartDate)
.ThenBy(x => x.Key.CourseCode)
.ThenBy(x => x.Key.TaskNumber)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var result = new List<object>(); var dimensionSummaries = await (
foreach (var setup in setups) from score in db.EvaluationScores.AsNoTracking()
{ join record in db.EvaluationRecords.AsNoTracking()
var setupTaskIds = await db.EvaluationRecords.AsNoTracking() on score.EvaluationRecordId equals record.Id
.Where(r => r.EvaluationSetupId == setup.Id) join dimension in db.EvaluationDimensions.AsNoTracking()
.WhereIn(taskIds, r => r.TeachingTaskId) on score.EvaluationDimensionId equals dimension.Id
.Select(r => r.TeachingTaskId) where taskIds.Contains(record.TeachingTaskId) &&
.Distinct() record.EvaluationSetup!.Status == EvaluationSetupStatus.Closed
.ToListAsync(cancellationToken); group score by new
foreach (var taskId in setupTaskIds)
{ {
var task = await db.TeachingTasks.AsNoTracking() record.EvaluationSetupId,
.Where(x => x.Id == taskId) record.TeachingTaskId,
.Select(x => new dimension.Id,
{ dimension.Name,
x.Id, dimension.MaxScore,
x.TaskNumber, dimension.SortOrder
x.Name,
x.AcademicTermId,
CourseCode = x.Course!.Code,
CourseName = x.Course.Name
})
.FirstOrDefaultAsync(cancellationToken);
if (task is null) continue;
var records = await db.EvaluationRecords.AsNoTracking()
.Where(r => r.EvaluationSetupId == setup.Id &&
r.TeachingTaskId == taskId)
.Include(r => r.Scores)
.ToListAsync(cancellationToken);
if (records.Count == 0) continue;
var dimensionResults = setup.Dimensions.Select(dim =>
{
var scores = records
.SelectMany(r => r.Scores)
.Where(s => s.EvaluationDimensionId == dim.Id)
.Select(s => (double)s.Score)
.ToList();
return new
{
DimensionId = dim.Id,
DimensionName = dim.Name,
MaxScore = dim.MaxScore,
AverageScore = scores.Count > 0
? Math.Round(scores.Average(), 1) : 0,
TotalScore = scores.Sum(),
Count = scores.Count
};
}).ToList();
var totalAvg = records
.Select(r => (double)r.Scores.Sum(s => s.Score))
.ToList();
result.Add(new
{
SetupId = setup.Id,
setup.Name,
Task = task,
StudentCount = records.Count,
AverageTotal = totalAvg.Count > 0
? Math.Round(totalAvg.Average(), 1) : 0,
Dimensions = dimensionResults
});
} }
} into scoreGroup
select new
{
scoreGroup.Key,
AverageScore = scoreGroup.Average(score => score.Score),
TotalScore = scoreGroup.Sum(score => score.Score),
Count = scoreGroup.Count()
})
.ToListAsync(cancellationToken);
var dimensionsByTask = dimensionSummaries
.GroupBy(item => (item.Key.EvaluationSetupId, item.Key.TeachingTaskId))
.ToDictionary(
group => group.Key,
group => (IReadOnlyCollection<TeacherEvaluationDimensionResult>)group
.OrderBy(item => item.Key.SortOrder)
.Select(item => new TeacherEvaluationDimensionResult(
item.Key.Id,
item.Key.Name,
item.Key.MaxScore,
Math.Round((double)item.AverageScore, 1),
item.TotalScore,
item.Count))
.ToArray());
var result = taskSummaries.Select(item => new TeacherEvaluationTaskResult(
item.Key.EvaluationSetupId,
item.Key.SetupName,
new TeacherEvaluationTask(
item.Key.TeachingTaskId,
item.Key.TaskNumber,
item.Key.TaskName,
item.Key.AcademicTermId,
item.Key.CourseCode,
item.Key.CourseName),
item.StudentCount,
Math.Round((double)item.AverageTotal, 1),
dimensionsByTask.GetValueOrDefault(
(item.Key.EvaluationSetupId, item.Key.TeachingTaskId),
Array.Empty<TeacherEvaluationDimensionResult>())))
.ToArray();
return Ok(new { Tasks = result }); return Ok(new { Tasks = result });
} }
@@ -594,3 +603,27 @@ public sealed record SubmitEvaluationRequest(
public sealed record EvaluationScoreRequest( public sealed record EvaluationScoreRequest(
Guid DimensionId, Guid DimensionId,
int Score); int Score);
public sealed record TeacherEvaluationTaskResult(
Guid SetupId,
string SetupName,
TeacherEvaluationTask Task,
int StudentCount,
double AverageTotal,
IReadOnlyCollection<TeacherEvaluationDimensionResult> Dimensions);
public sealed record TeacherEvaluationTask(
Guid Id,
string TaskNumber,
string Name,
Guid AcademicTermId,
string CourseCode,
string CourseName);
public sealed record TeacherEvaluationDimensionResult(
Guid DimensionId,
string DimensionName,
int MaxScore,
double AverageScore,
decimal TotalScore,
int Count);
@@ -227,14 +227,24 @@ public sealed class OfficialDocumentsController(
[Authorize(Roles = Managers)] [Authorize(Roles = Managers)]
public async Task<ActionResult> DownloadHistory( public async Task<ActionResult> DownloadHistory(
Guid id, Guid id,
CancellationToken cancellationToken) CancellationToken cancellationToken,
int page = 1,
int pageSize = 20)
{ {
if (page < 1 || pageSize is < 1 or > 100)
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
if (!await AccessibleDocuments().AnyAsync(x => x.Id == id, cancellationToken)) if (!await AccessibleDocuments().AnyAsync(x => x.Id == id, cancellationToken))
return NotFound(); return NotFound();
return Ok(await db.OfficialDocumentDownloads.AsNoTracking() var source = db.OfficialDocumentDownloads.AsNoTracking()
.Where(x => x.OfficialDocumentId == id) .Where(x => x.OfficialDocumentId == id)
.OrderByDescending(x => x.CreatedAt) .OrderByDescending(x => x.CreatedAt)
.ThenByDescending(x => x.Id);
var total = await source.CountAsync(cancellationToken);
var items = await source
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(x => new .Select(x => new
{ {
x.Id, x.Id,
@@ -244,7 +254,8 @@ public sealed class OfficialDocumentsController(
x.IpAddress, x.IpAddress,
x.UserAgent x.UserAgent
}) })
.ToListAsync(cancellationToken)); .ToListAsync(cancellationToken);
return Ok(new PagedResult<object>(items, total, page, pageSize));
} }
[HttpPost("{id:guid}/invalidate")] [HttpPost("{id:guid}/invalidate")]
@@ -101,17 +101,23 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc
db.WarningRecords.AddRange(generated); db.WarningRecords.AddRange(generated);
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
var studentsById = studentInfos.ToDictionary(student => student.Id);
var rulesByType = rules.ToDictionary(rule => rule.Type);
var classIds = studentInfos.Select(student => student.ClassId).Distinct().ToArray();
var counselorsByClass = await db.AdministrativeClasses.AsNoTracking()
.WhereIn(classIds, item => item.Id)
.Where(item => item.CounselorUserId != null)
.Select(item => new { item.Id, CounselorUserId = item.CounselorUserId!.Value })
.ToDictionaryAsync(item => item.Id, item => item.CounselorUserId, ct);
foreach (var w in generated) foreach (var w in generated)
{ {
var si = studentInfos.First(s => s.Id == w.StudentId); var si = studentsById[w.StudentId];
var r = rules.First(r => r.Type == w.Type); var r = rulesByType[w.Type];
if (r.NotifyStudent && si.UserId.HasValue) if (r.NotifyStudent && si.UserId.HasValue)
await NotificationService.SendAsync(db, si.UserId.Value, "学业预警", w.Detail, "/warnings", ct, NotificationCategory.Warning); await NotificationService.SendAsync(db, si.UserId.Value, "学业预警", w.Detail, "/warnings", ct, NotificationCategory.Warning);
if (r.NotifyCounselor) if (r.NotifyCounselor && counselorsByClass.TryGetValue(si.ClassId, out var counselorId))
{ {
var counselorId = await db.AdministrativeClasses.Where(c => c.Id == si.ClassId && c.CounselorUserId != null).Select(c => c.CounselorUserId!.Value).FirstOrDefaultAsync(ct); await NotificationService.SendAsync(db, counselorId, "学生学业预警", $"{si.Name}:{w.Detail}", "/warnings", ct, NotificationCategory.Warning);
if (counselorId != default)
await NotificationService.SendAsync(db, counselorId, "学生学业预警", $"{si.Name}:{w.Detail}", "/warnings", ct, NotificationCategory.Warning);
} }
} }
} }
@@ -197,17 +203,31 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc
// ═══════════ Detection logic ═══════════ // ═══════════ Detection logic ═══════════
private async Task<List<WarningRecord>> DetectFailedCredits(WarningRule rule, Guid termId, List<StudentInfo> students, CancellationToken ct) private async Task<List<WarningRecord>> DetectFailedCredits(WarningRule rule, Guid termId, List<StudentInfo> students, CancellationToken ct)
{ {
var records = new List<WarningRecord>(); var studentIds = students.Select(student => student.Id).ToArray();
foreach (var s in students) var existingStudentIds = await db.WarningRecords.AsNoTracking()
{ .Where(record => record.AcademicTermId == termId && record.Type == rule.Type)
var failed = await db.GradeRecords.CountAsync(x => x.StudentId == s.Id && x.GradeSheet!.TeachingTask!.AcademicTermId == termId && x.GradeSheet.Status == GradeSheetStatus.Published && x.TotalScore < 60, ct); .Select(record => record.StudentId)
if (failed >= rule.Threshold) .ToHashSetAsync(ct);
var failedByStudent = await db.GradeRecords.AsNoTracking()
.Where(record => studentIds.Contains(record.StudentId) &&
record.GradeSheet!.TeachingTask!.AcademicTermId == termId &&
record.GradeSheet.Status == GradeSheetStatus.Published &&
record.TotalScore < 60)
.GroupBy(record => record.StudentId)
.Select(group => new { StudentId = group.Key, Failed = group.Count() })
.ToListAsync(ct);
return failedByStudent
.Where(item => item.Failed >= rule.Threshold &&
!existingStudentIds.Contains(item.StudentId))
.Select(item => new WarningRecord
{ {
if (await db.WarningRecords.AnyAsync(x => x.StudentId == s.Id && x.AcademicTermId == termId && x.Type == WarningType.FailedCredits, ct)) continue; StudentId = item.StudentId,
records.Add(new WarningRecord { StudentId = s.Id, Type = rule.Type, TriggerValue = failed, Detail = $"不及格课程 {failed} 门,达到预警阈值 {rule.Threshold} 门。", AcademicTermId = termId }); Type = rule.Type,
} TriggerValue = item.Failed,
} Detail = $"不及格课程 {item.Failed} 门,达到预警阈值 {rule.Threshold} 门。",
return records; AcademicTermId = termId
})
.ToList();
} }
private async Task<List<WarningRecord>> DetectLowGPA(WarningRule rule, Guid termId, List<StudentInfo> students, CancellationToken ct) private async Task<List<WarningRecord>> DetectLowGPA(WarningRule rule, Guid termId, List<StudentInfo> students, CancellationToken ct)
Loaded 3 of 4 files, more files were not shown because too many files have changed in this diff. Show more