Compare commits
3
Commits
0bc59bd64e
...
08396a6e23
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
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var result = new List<object>();
|
|
||||||
foreach (var setup in setups)
|
|
||||||
{
|
{
|
||||||
var setupTaskIds = await db.EvaluationRecords.AsNoTracking()
|
record.EvaluationSetupId,
|
||||||
.Where(r => r.EvaluationSetupId == setup.Id)
|
SetupName = record.EvaluationSetup!.Name,
|
||||||
.WhereIn(taskIds, r => r.TeachingTaskId)
|
SetupStartDate = record.EvaluationSetup.AcademicTerm!.StartDate,
|
||||||
.Select(r => r.TeachingTaskId)
|
record.TeachingTaskId,
|
||||||
.Distinct()
|
TaskNumber = record.TeachingTask!.TaskNumber,
|
||||||
.ToListAsync(cancellationToken);
|
TaskName = record.TeachingTask.Name,
|
||||||
|
record.TeachingTask.AcademicTermId,
|
||||||
foreach (var taskId in setupTaskIds)
|
CourseCode = record.TeachingTask.Course!.Code,
|
||||||
{
|
CourseName = record.TeachingTask.Course.Name
|
||||||
var task = await db.TeachingTasks.AsNoTracking()
|
|
||||||
.Where(x => x.Id == taskId)
|
|
||||||
.Select(x => new
|
|
||||||
{
|
|
||||||
x.Id,
|
|
||||||
x.TaskNumber,
|
|
||||||
x.Name,
|
|
||||||
x.AcademicTermId,
|
|
||||||
CourseCode = x.Course!.Code,
|
|
||||||
CourseName = x.Course.Name
|
|
||||||
})
|
})
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.Select(group => new
|
||||||
if (task is null) continue;
|
{
|
||||||
|
group.Key,
|
||||||
var records = await db.EvaluationRecords.AsNoTracking()
|
StudentCount = group.Count(),
|
||||||
.Where(r => r.EvaluationSetupId == setup.Id &&
|
AverageTotal = group.Average(record =>
|
||||||
r.TeachingTaskId == taskId)
|
record.Scores.Sum(score => score.Score))
|
||||||
.Include(r => r.Scores)
|
})
|
||||||
|
.OrderByDescending(x => x.Key.SetupStartDate)
|
||||||
|
.ThenBy(x => x.Key.CourseCode)
|
||||||
|
.ThenBy(x => x.Key.TaskNumber)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
if (records.Count == 0) continue;
|
var dimensionSummaries = await (
|
||||||
|
from score in db.EvaluationScores.AsNoTracking()
|
||||||
var dimensionResults = setup.Dimensions.Select(dim =>
|
join record in db.EvaluationRecords.AsNoTracking()
|
||||||
|
on score.EvaluationRecordId equals record.Id
|
||||||
|
join dimension in db.EvaluationDimensions.AsNoTracking()
|
||||||
|
on score.EvaluationDimensionId equals dimension.Id
|
||||||
|
where taskIds.Contains(record.TeachingTaskId) &&
|
||||||
|
record.EvaluationSetup!.Status == EvaluationSetupStatus.Closed
|
||||||
|
group score by new
|
||||||
{
|
{
|
||||||
var scores = records
|
record.EvaluationSetupId,
|
||||||
.SelectMany(r => r.Scores)
|
record.TeachingTaskId,
|
||||||
.Where(s => s.EvaluationDimensionId == dim.Id)
|
dimension.Id,
|
||||||
.Select(s => (double)s.Score)
|
dimension.Name,
|
||||||
.ToList();
|
dimension.MaxScore,
|
||||||
return new
|
dimension.SortOrder
|
||||||
{
|
|
||||||
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,16 +101,22 @@ 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);
|
|
||||||
if (counselorId != default)
|
|
||||||
await NotificationService.SendAsync(db, counselorId, "学生学业预警", $"{si.Name}:{w.Detail}", "/warnings", ct, NotificationCategory.Warning);
|
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)
|
||||||
|
.Select(record => record.StudentId)
|
||||||
|
.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
|
||||||
{
|
{
|
||||||
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);
|
StudentId = item.StudentId,
|
||||||
if (failed >= rule.Threshold)
|
Type = rule.Type,
|
||||||
{
|
TriggerValue = item.Failed,
|
||||||
if (await db.WarningRecords.AnyAsync(x => x.StudentId == s.Id && x.AcademicTermId == termId && x.Type == WarningType.FailedCredits, ct)) continue;
|
Detail = $"不及格课程 {item.Failed} 门,达到预警阈值 {rule.Threshold} 门。",
|
||||||
records.Add(new WarningRecord { StudentId = s.Id, Type = rule.Type, TriggerValue = failed, Detail = $"不及格课程 {failed} 门,达到预警阈值 {rule.Threshold} 门。", AcademicTermId = termId });
|
AcademicTermId = termId
|
||||||
}
|
})
|
||||||
}
|
.ToList();
|
||||||
return records;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
Reference in new issue
Block a user