教师成绩报告
This commit is contained in:
@@ -0,0 +1,477 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Domain.System;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Caching;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Roles = AnalyticsUsers)]
|
||||
[Route("api/grade-analytics")]
|
||||
public sealed class GradeAnalyticsController(
|
||||
AppDbContext db,
|
||||
ICurrentUserDataScope currentUserDataScope,
|
||||
IAppCache? cache = null) : ControllerBase
|
||||
{
|
||||
private const string AnalyticsUsers =
|
||||
SystemRoles.SuperAdmin + "," +
|
||||
SystemRoles.AcademicAdmin + "," +
|
||||
SystemRoles.CollegeAdmin + "," +
|
||||
SystemRoles.Leader + "," +
|
||||
SystemRoles.Teacher;
|
||||
|
||||
[HttpGet("teaching-classes")]
|
||||
public async Task<ActionResult> GetTeachingClasses(
|
||||
Guid? academicTermId,
|
||||
string? keyword,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 1, 100);
|
||||
keyword = string.IsNullOrWhiteSpace(keyword) ? null : keyword.Trim();
|
||||
|
||||
var source = db.TeachingTaskGradeStatistics.AsNoTracking()
|
||||
.Where(x => VisibleTeachingTasks().Any(task => task.Id == x.TeachingTaskId));
|
||||
if (academicTermId.HasValue)
|
||||
source = source.Where(x => x.AcademicTermId == academicTermId);
|
||||
if (keyword is not null)
|
||||
source = source.Where(x =>
|
||||
x.TeachingTask!.TaskNumber.Contains(keyword) ||
|
||||
x.TeachingTask.Name.Contains(keyword) ||
|
||||
x.TeachingTask.Course!.Code.Contains(keyword) ||
|
||||
x.TeachingTask.Course.Name.Contains(keyword));
|
||||
|
||||
var total = await source.CountAsync(cancellationToken);
|
||||
var items = await source
|
||||
.OrderByDescending(x => x.TeachingTask!.AcademicTerm!.StartDate)
|
||||
.ThenBy(x => x.TeachingTask!.Course!.Code)
|
||||
.ThenBy(x => x.TeachingTask!.TaskNumber)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(x => new
|
||||
{
|
||||
x.GradeSheetId,
|
||||
x.TeachingTaskId,
|
||||
x.TeachingTask!.TaskNumber,
|
||||
TaskName = x.TeachingTask.Name,
|
||||
CourseCode = x.TeachingTask.Course!.Code,
|
||||
CourseName = x.TeachingTask.Course.Name,
|
||||
TermName = x.TeachingTask.AcademicTerm!.Name,
|
||||
x.AcademicTermId,
|
||||
TeacherNames = x.TeachingTask.Teachers
|
||||
.OrderByDescending(item => item.IsPrimary)
|
||||
.Select(item => item.Teacher!.Name),
|
||||
ClassNames = x.TeachingTask.Classes
|
||||
.Select(item => item.AdministrativeClass!.Name),
|
||||
x.StudentCount,
|
||||
x.AverageScore,
|
||||
x.PassRate,
|
||||
x.ExcellentRate,
|
||||
x.CalculatedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(new { Items = items, Total = total, Page = page, PageSize = pageSize });
|
||||
}
|
||||
|
||||
[HttpGet("teaching-classes/{gradeSheetId:guid}")]
|
||||
public async Task<ActionResult> GetTeachingClassAnalysis(
|
||||
Guid gradeSheetId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sheet = await db.GradeSheets.AsNoTracking()
|
||||
.Where(x => x.Id == gradeSheetId &&
|
||||
VisibleTeachingTasks().Any(task => task.Id == x.TeachingTaskId))
|
||||
.Select(x => new AnalysisTarget(
|
||||
x.Id,
|
||||
x.TeachingTaskId,
|
||||
x.TeachingTask!.CourseId,
|
||||
x.TeachingTask.AcademicTermId,
|
||||
x.TeachingTask.Course!.CollegeId,
|
||||
x.TeachingTask.TaskNumber,
|
||||
x.TeachingTask.Name,
|
||||
x.TeachingTask.Course.Code,
|
||||
x.TeachingTask.Course.Name,
|
||||
x.TeachingTask.AcademicTerm!.Name))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (sheet is null) return NotFound();
|
||||
|
||||
var report = await (cache ?? NoOpAppCache.Instance).GetOrCreateAsync(
|
||||
AppCacheKeys.TeachingTaskGradeAnalytics(gradeSheetId),
|
||||
token => BuildReportAsync(sheet, token),
|
||||
AppCacheProfile.Analytics,
|
||||
[AppCacheTags.CourseGradeStatistics],
|
||||
cancellationToken);
|
||||
return Ok(report);
|
||||
}
|
||||
|
||||
[HttpPost("teaching-classes/{gradeSheetId:guid}/refresh")]
|
||||
public async Task<ActionResult> RefreshTeachingClassAnalysis(
|
||||
Guid gradeSheetId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var exists = await db.GradeSheets.AsNoTracking()
|
||||
.AnyAsync(x => x.Id == gradeSheetId &&
|
||||
VisibleTeachingTasks().Any(task => task.Id == x.TeachingTaskId),
|
||||
cancellationToken);
|
||||
if (!exists) return NotFound();
|
||||
|
||||
var job = new CourseGradeStatisticsRefreshJob { GradeSheetId = gradeSheetId };
|
||||
db.CourseGradeStatisticsRefreshJobs.Add(job);
|
||||
db.BackgroundJobOutboxMessages.Add(BackgroundJobOutboxMessage.Create(
|
||||
BackgroundJobKind.CourseGradeStatisticsRefresh,
|
||||
job.Id));
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return Accepted(new { job.Id });
|
||||
}
|
||||
|
||||
private async Task<TeachingClassAnalysisReport> BuildReportAsync(
|
||||
AnalysisTarget target,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var statistic = await db.TeachingTaskGradeStatistics.AsNoTracking()
|
||||
.Where(x => x.GradeSheetId == target.GradeSheetId)
|
||||
.Select(x => new TeachingClassMetrics(
|
||||
x.StudentCount,
|
||||
x.PassedCount,
|
||||
x.ExcellentCount,
|
||||
x.HighestScore,
|
||||
x.AverageScore,
|
||||
x.MedianScore,
|
||||
x.LowestScore,
|
||||
x.StandardDeviation,
|
||||
x.PassRate,
|
||||
x.ExcellentRate,
|
||||
x.CalculatedAt,
|
||||
x.ScoreBands.OrderBy(band => band.SortOrder)
|
||||
.Select(band => new ScoreBand(
|
||||
band.Label,
|
||||
band.LowerBound,
|
||||
band.UpperBound,
|
||||
band.StudentCount))
|
||||
.ToArray()))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (statistic is null)
|
||||
return new TeachingClassAnalysisReport(
|
||||
true,
|
||||
target.GradeSheetId,
|
||||
target.TeachingTaskId,
|
||||
target.TaskNumber,
|
||||
target.TaskName,
|
||||
target.CourseCode,
|
||||
target.CourseName,
|
||||
target.TermName,
|
||||
null,
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null);
|
||||
|
||||
var peerRows = await db.TeachingTaskGradeStatistics.AsNoTracking()
|
||||
.Where(x => x.CourseId == target.CourseId &&
|
||||
x.AcademicTermId == target.AcademicTermId)
|
||||
.OrderByDescending(x => x.AverageScore)
|
||||
.Select(x => new
|
||||
{
|
||||
x.GradeSheetId,
|
||||
x.TeachingTaskId,
|
||||
x.TeachingTask!.TaskNumber,
|
||||
TaskName = x.TeachingTask.Name,
|
||||
TeacherNames = x.TeachingTask.Teachers
|
||||
.OrderByDescending(item => item.IsPrimary)
|
||||
.Select(item => item.Teacher!.Name).ToArray(),
|
||||
ClassNames = x.TeachingTask.Classes
|
||||
.Select(item => item.AdministrativeClass!.Name).ToArray(),
|
||||
x.StudentCount,
|
||||
x.HighestScore,
|
||||
x.AverageScore,
|
||||
x.MedianScore,
|
||||
x.LowestScore,
|
||||
x.StandardDeviation,
|
||||
x.PassRate,
|
||||
x.ExcellentRate
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var peers = peerRows.Select(x => new TeachingClassComparison(
|
||||
x.GradeSheetId,
|
||||
x.TeachingTaskId,
|
||||
x.TaskNumber,
|
||||
x.TaskName,
|
||||
string.Join("、", x.TeacherNames),
|
||||
string.Join("、", x.ClassNames),
|
||||
x.StudentCount,
|
||||
x.HighestScore,
|
||||
x.AverageScore,
|
||||
x.MedianScore,
|
||||
x.LowestScore,
|
||||
x.StandardDeviation,
|
||||
x.PassRate,
|
||||
x.ExcellentRate,
|
||||
x.GradeSheetId == target.GradeSheetId)).ToArray();
|
||||
|
||||
var classProfiles = await db.GradeRecords.AsNoTracking()
|
||||
.Where(x => x.GradeSheetId == target.GradeSheetId)
|
||||
.Select(x => new ClassProfile(
|
||||
x.Student!.AdministrativeClassId,
|
||||
x.Student.AdministrativeClass!.Name,
|
||||
x.Student.AdministrativeClass.MajorId,
|
||||
x.Student.AdministrativeClass.Major!.Name,
|
||||
x.Student.AdministrativeClass.Major.CollegeId,
|
||||
x.Student.AdministrativeClass.Major.College!.Name))
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
var scopeStatistics = await db.CourseGradeStatistics.AsNoTracking()
|
||||
.Where(x => x.CourseId == target.CourseId &&
|
||||
x.AcademicTermId == target.AcademicTermId)
|
||||
.ToListAsync(cancellationToken);
|
||||
var benchmarks = BuildBenchmarks(classProfiles, scopeStatistics);
|
||||
|
||||
var selectedTeacherIds = await db.TeachingTaskTeachers.AsNoTracking()
|
||||
.Where(x => x.TeachingTaskId == target.TeachingTaskId)
|
||||
.Select(x => x.TeacherId)
|
||||
.ToListAsync(cancellationToken);
|
||||
var historicalTaskRows = await db.TeachingTaskGradeStatistics.AsNoTracking()
|
||||
.Where(x => x.CourseId == target.CourseId &&
|
||||
x.TeachingTask!.Teachers.Any(link =>
|
||||
selectedTeacherIds.Contains(link.TeacherId)))
|
||||
.Select(x => new
|
||||
{
|
||||
x.AcademicTermId,
|
||||
TermName = x.TeachingTask!.AcademicTerm!.Name,
|
||||
x.TeachingTask.AcademicTerm.StartDate,
|
||||
x.StudentCount,
|
||||
x.PassedCount,
|
||||
x.ExcellentCount,
|
||||
x.AverageScore
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var courseHistory = await db.CourseGradeStatistics.AsNoTracking()
|
||||
.Where(x => x.CourseId == target.CourseId &&
|
||||
x.Scope == CourseGradeStatisticScope.University)
|
||||
.Select(x => new
|
||||
{
|
||||
x.AcademicTermId,
|
||||
TermName = db.AcademicTerms.Where(term => term.Id == x.AcademicTermId)
|
||||
.Select(term => term.Name).First(),
|
||||
StartDate = db.AcademicTerms.Where(term => term.Id == x.AcademicTermId)
|
||||
.Select(term => term.StartDate).First(),
|
||||
x.StudentCount,
|
||||
x.AverageScore,
|
||||
x.PassRate,
|
||||
ExcellentRate = x.StudentCount == 0 ? 0m :
|
||||
Math.Round((decimal)x.From90To100Count / x.StudentCount * 100m, 2)
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var teacherByTerm = historicalTaskRows
|
||||
.GroupBy(x => new { x.AcademicTermId, x.TermName, x.StartDate })
|
||||
.ToDictionary(group => group.Key.AcademicTermId, group =>
|
||||
{
|
||||
var count = group.Sum(x => x.StudentCount);
|
||||
return new HistoricalSeriesValue(
|
||||
count,
|
||||
count == 0 ? 0m : Math.Round(
|
||||
group.Sum(x => x.AverageScore * x.StudentCount) / count, 1),
|
||||
count == 0 ? 0m : Math.Round(
|
||||
(decimal)group.Sum(x => x.PassedCount) / count * 100m, 2),
|
||||
count == 0 ? 0m : Math.Round(
|
||||
(decimal)group.Sum(x => x.ExcellentCount) / count * 100m, 2));
|
||||
});
|
||||
var history = courseHistory
|
||||
.OrderBy(x => x.StartDate)
|
||||
.Select(x => new HistoricalComparison(
|
||||
x.AcademicTermId,
|
||||
x.TermName,
|
||||
x.StudentCount,
|
||||
x.AverageScore,
|
||||
x.PassRate,
|
||||
x.ExcellentRate,
|
||||
teacherByTerm.GetValueOrDefault(x.AcademicTermId)))
|
||||
.ToArray();
|
||||
|
||||
var university = scopeStatistics.FirstOrDefault(x =>
|
||||
x.Scope == CourseGradeStatisticScope.University);
|
||||
return new TeachingClassAnalysisReport(
|
||||
false,
|
||||
target.GradeSheetId,
|
||||
target.TeachingTaskId,
|
||||
target.TaskNumber,
|
||||
target.TaskName,
|
||||
target.CourseCode,
|
||||
target.CourseName,
|
||||
target.TermName,
|
||||
statistic,
|
||||
peers,
|
||||
benchmarks,
|
||||
history,
|
||||
university is null ? null : new ComparisonDelta(
|
||||
Math.Round(statistic.AverageScore - university.AverageScore, 1),
|
||||
Math.Round(statistic.PassRate - university.PassRate, 2),
|
||||
university.AverageScore,
|
||||
university.PassRate));
|
||||
}
|
||||
|
||||
private static ScopeBenchmark[] BuildBenchmarks(
|
||||
IEnumerable<ClassProfile> classProfiles,
|
||||
IReadOnlyCollection<CourseGradeStatistic> statistics)
|
||||
{
|
||||
var profiles = classProfiles.ToArray();
|
||||
var rows = new List<ScopeBenchmark>();
|
||||
foreach (var profile in profiles)
|
||||
AddBenchmark(rows, statistics, CourseGradeStatisticScope.AdministrativeClass,
|
||||
profile.ClassId, "行政班", profile.ClassName);
|
||||
foreach (var profile in profiles.GroupBy(x => x.MajorId).Select(x => x.First()))
|
||||
AddBenchmark(rows, statistics, CourseGradeStatisticScope.Major,
|
||||
profile.MajorId, "专业", profile.MajorName);
|
||||
foreach (var profile in profiles.GroupBy(x => x.CollegeId).Select(x => x.First()))
|
||||
AddBenchmark(rows, statistics, CourseGradeStatisticScope.College,
|
||||
profile.CollegeId, "学院", profile.CollegeName);
|
||||
AddBenchmark(rows, statistics, CourseGradeStatisticScope.University,
|
||||
null, "全校", "全校同课程");
|
||||
return rows.ToArray();
|
||||
}
|
||||
|
||||
private static void AddBenchmark(
|
||||
ICollection<ScopeBenchmark> target,
|
||||
IEnumerable<CourseGradeStatistic> source,
|
||||
CourseGradeStatisticScope scope,
|
||||
Guid? entityId,
|
||||
string scopeLabel,
|
||||
string name)
|
||||
{
|
||||
var item = source.FirstOrDefault(x =>
|
||||
x.Scope == scope && x.ScopeEntityId == entityId);
|
||||
if (item is null || target.Any(x => x.Scope == scopeLabel && x.Name == name)) return;
|
||||
target.Add(new ScopeBenchmark(
|
||||
scopeLabel,
|
||||
name,
|
||||
item.StudentCount,
|
||||
item.HighestScore,
|
||||
item.AverageScore,
|
||||
item.LowestScore,
|
||||
item.PassRate));
|
||||
}
|
||||
|
||||
private IQueryable<TeachingTask> VisibleTeachingTasks()
|
||||
{
|
||||
var scope = currentUserDataScope.Current;
|
||||
var source = db.TeachingTasks.AsQueryable();
|
||||
if (scope.Scope == DataScope.All) return source;
|
||||
if (scope.Scope == DataScope.College)
|
||||
return source.Where(x => x.Course!.CollegeId == scope.RestrictedCollegeId);
|
||||
if (scope.IsInRole(SystemRoles.Teacher))
|
||||
return source.Where(x =>
|
||||
x.Teachers.Any(link => link.Teacher!.UserId == scope.UserId));
|
||||
return source.Where(_ => false);
|
||||
}
|
||||
|
||||
private sealed record AnalysisTarget(
|
||||
Guid GradeSheetId,
|
||||
Guid TeachingTaskId,
|
||||
Guid CourseId,
|
||||
Guid AcademicTermId,
|
||||
Guid CourseCollegeId,
|
||||
string TaskNumber,
|
||||
string TaskName,
|
||||
string CourseCode,
|
||||
string CourseName,
|
||||
string TermName);
|
||||
|
||||
private sealed record ClassProfile(
|
||||
Guid ClassId,
|
||||
string ClassName,
|
||||
Guid MajorId,
|
||||
string MajorName,
|
||||
Guid CollegeId,
|
||||
string CollegeName);
|
||||
|
||||
public sealed record ScoreBand(
|
||||
string Label,
|
||||
decimal LowerBound,
|
||||
decimal? UpperBound,
|
||||
int StudentCount);
|
||||
|
||||
public sealed record TeachingClassMetrics(
|
||||
int StudentCount,
|
||||
int PassedCount,
|
||||
int ExcellentCount,
|
||||
decimal HighestScore,
|
||||
decimal AverageScore,
|
||||
decimal MedianScore,
|
||||
decimal LowestScore,
|
||||
decimal StandardDeviation,
|
||||
decimal PassRate,
|
||||
decimal ExcellentRate,
|
||||
DateTime CalculatedAt,
|
||||
IReadOnlyList<ScoreBand> ScoreBands);
|
||||
|
||||
public sealed record TeachingClassComparison(
|
||||
Guid GradeSheetId,
|
||||
Guid TeachingTaskId,
|
||||
string TaskNumber,
|
||||
string TaskName,
|
||||
string TeacherNames,
|
||||
string ClassNames,
|
||||
int StudentCount,
|
||||
decimal HighestScore,
|
||||
decimal AverageScore,
|
||||
decimal MedianScore,
|
||||
decimal LowestScore,
|
||||
decimal StandardDeviation,
|
||||
decimal PassRate,
|
||||
decimal ExcellentRate,
|
||||
bool IsSelected);
|
||||
|
||||
public sealed record ScopeBenchmark(
|
||||
string Scope,
|
||||
string Name,
|
||||
int StudentCount,
|
||||
decimal HighestScore,
|
||||
decimal AverageScore,
|
||||
decimal LowestScore,
|
||||
decimal PassRate);
|
||||
|
||||
public sealed record HistoricalSeriesValue(
|
||||
int StudentCount,
|
||||
decimal AverageScore,
|
||||
decimal PassRate,
|
||||
decimal ExcellentRate);
|
||||
|
||||
public sealed record HistoricalComparison(
|
||||
Guid AcademicTermId,
|
||||
string TermName,
|
||||
int CourseStudentCount,
|
||||
decimal CourseAverageScore,
|
||||
decimal CoursePassRate,
|
||||
decimal CourseExcellentRate,
|
||||
HistoricalSeriesValue? Instructor);
|
||||
|
||||
public sealed record ComparisonDelta(
|
||||
decimal AverageScoreDifference,
|
||||
decimal PassRateDifference,
|
||||
decimal UniversityAverageScore,
|
||||
decimal UniversityPassRate);
|
||||
|
||||
public sealed record TeachingClassAnalysisReport(
|
||||
bool IsRefreshing,
|
||||
Guid GradeSheetId,
|
||||
Guid TeachingTaskId,
|
||||
string TaskNumber,
|
||||
string TaskName,
|
||||
string CourseCode,
|
||||
string CourseName,
|
||||
string TermName,
|
||||
TeachingClassMetrics? Summary,
|
||||
IReadOnlyList<TeachingClassComparison> PeerTeachingClasses,
|
||||
IReadOnlyList<ScopeBenchmark> ScopeBenchmarks,
|
||||
IReadOnlyList<HistoricalComparison> History,
|
||||
ComparisonDelta? UniversityDelta);
|
||||
}
|
||||
@@ -79,6 +79,48 @@ public sealed class CourseGradeStatistic : EntityBase
|
||||
public DateTime CalculatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Materialized analysis for one published teaching class. Course/term
|
||||
/// organizational benchmarks stay in <see cref="CourseGradeStatistic"/>;
|
||||
/// this table is the grain used for peer-class and historical comparisons.
|
||||
/// </summary>
|
||||
public sealed class TeachingTaskGradeStatistic : EntityBase
|
||||
{
|
||||
public Guid GradeSheetId { get; set; }
|
||||
public GradeSheet? GradeSheet { get; set; }
|
||||
public Guid TeachingTaskId { get; set; }
|
||||
public TeachingTask? TeachingTask { get; set; }
|
||||
public Guid CourseId { get; set; }
|
||||
public Guid AcademicTermId { get; set; }
|
||||
public int StudentCount { get; set; }
|
||||
public int PassedCount { get; set; }
|
||||
public int ExcellentCount { get; set; }
|
||||
public decimal HighestScore { get; set; }
|
||||
public decimal AverageScore { get; set; }
|
||||
public decimal MedianScore { get; set; }
|
||||
public decimal LowestScore { get; set; }
|
||||
public decimal StandardDeviation { get; set; }
|
||||
public decimal PassRate { get; set; }
|
||||
public decimal ExcellentRate { get; set; }
|
||||
public DateTime CalculatedAt { get; set; }
|
||||
public ICollection<TeachingTaskGradeScoreBand> ScoreBands { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flexible score-band rows are kept separately so future band definitions do
|
||||
/// not require widening the teaching-class summary table.
|
||||
/// </summary>
|
||||
public sealed class TeachingTaskGradeScoreBand : EntityBase
|
||||
{
|
||||
public Guid TeachingTaskGradeStatisticId { get; set; }
|
||||
public TeachingTaskGradeStatistic? TeachingTaskGradeStatistic { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public decimal LowerBound { get; set; }
|
||||
public decimal? UpperBound { get; set; }
|
||||
public int StudentCount { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public enum CourseGradeStatisticScope
|
||||
{
|
||||
AdministrativeClass = 1,
|
||||
|
||||
@@ -185,6 +185,9 @@ public static class AppCacheKeys
|
||||
|
||||
public static string CourseGradeStatistics(Guid gradeSheetId) =>
|
||||
$"grade-statistics:sheet:{gradeSheetId:N}";
|
||||
|
||||
public static string TeachingTaskGradeAnalytics(Guid gradeSheetId) =>
|
||||
$"grade-analytics:sheet:{gradeSheetId:N}:v1";
|
||||
}
|
||||
|
||||
public static class AppCacheTags
|
||||
|
||||
@@ -49,6 +49,8 @@ public sealed class CourseGradeStatisticsRefreshJobProcessor(
|
||||
x.GradeSheet.TeachingTask!.CourseId == target.CourseId &&
|
||||
x.GradeSheet.TeachingTask.AcademicTermId == target.AcademicTermId)
|
||||
.Select(x => new ScoreRow(
|
||||
x.GradeSheetId,
|
||||
x.GradeSheet!.TeachingTaskId,
|
||||
x.TotalScore!.Value,
|
||||
x.Student!.AdministrativeClassId,
|
||||
x.Student.AdministrativeClass!.MajorId,
|
||||
@@ -65,6 +67,16 @@ public sealed class CourseGradeStatisticsRefreshJobProcessor(
|
||||
scores.GroupBy(x => x.CollegeId), rebuilt, target, now);
|
||||
AddUniversityStatistic(scores, rebuilt, target, now);
|
||||
|
||||
var rebuiltTeachingTasks = scores
|
||||
.GroupBy(x => new { x.GradeSheetId, x.TeachingTaskId })
|
||||
.Select(group => CreateTeachingTaskStatistic(
|
||||
group.Key.GradeSheetId,
|
||||
group.Key.TeachingTaskId,
|
||||
group.Select(x => x.Score),
|
||||
target,
|
||||
now))
|
||||
.ToList();
|
||||
|
||||
await db.CourseGradeStatistics
|
||||
.Where(x => x.CourseId == target.CourseId &&
|
||||
x.AcademicTermId == target.AcademicTermId)
|
||||
@@ -72,6 +84,24 @@ public sealed class CourseGradeStatisticsRefreshJobProcessor(
|
||||
if (rebuilt.Count > 0)
|
||||
db.CourseGradeStatistics.AddRange(rebuilt);
|
||||
|
||||
var oldTeachingTaskStatisticIds = await db.TeachingTaskGradeStatistics
|
||||
.Where(x => x.CourseId == target.CourseId &&
|
||||
x.AcademicTermId == target.AcademicTermId)
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
if (oldTeachingTaskStatisticIds.Count > 0)
|
||||
{
|
||||
await db.TeachingTaskGradeScoreBands
|
||||
.Where(x => oldTeachingTaskStatisticIds.Contains(
|
||||
x.TeachingTaskGradeStatisticId))
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
await db.TeachingTaskGradeStatistics
|
||||
.Where(x => oldTeachingTaskStatisticIds.Contains(x.Id))
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
}
|
||||
if (rebuiltTeachingTasks.Count > 0)
|
||||
db.TeachingTaskGradeStatistics.AddRange(rebuiltTeachingTasks);
|
||||
|
||||
job.Status = CourseGradeStatisticsRefreshJobStatus.Succeeded;
|
||||
job.CompletedAt = now;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
@@ -141,6 +171,80 @@ public sealed class CourseGradeStatisticsRefreshJobProcessor(
|
||||
};
|
||||
}
|
||||
|
||||
private sealed record ScoreRow(decimal Score, Guid ClassId, Guid MajorId, Guid CollegeId);
|
||||
private static TeachingTaskGradeStatistic CreateTeachingTaskStatistic(
|
||||
Guid gradeSheetId,
|
||||
Guid teachingTaskId,
|
||||
IEnumerable<decimal> source,
|
||||
StatisticsTarget targetInfo,
|
||||
DateTime calculatedAt)
|
||||
{
|
||||
var scores = source.OrderBy(x => x).ToArray();
|
||||
var passed = scores.Count(x => x >= 60m);
|
||||
var excellent = scores.Count(x => x >= 90m);
|
||||
var average = scores.Average();
|
||||
var middle = scores.Length / 2;
|
||||
var median = scores.Length % 2 == 0
|
||||
? (scores[middle - 1] + scores[middle]) / 2m
|
||||
: scores[middle];
|
||||
var variance = scores.Average(x =>
|
||||
(double)((x - average) * (x - average)));
|
||||
|
||||
var statistic = new TeachingTaskGradeStatistic
|
||||
{
|
||||
GradeSheetId = gradeSheetId,
|
||||
TeachingTaskId = teachingTaskId,
|
||||
CourseId = targetInfo.CourseId,
|
||||
AcademicTermId = targetInfo.AcademicTermId,
|
||||
StudentCount = scores.Length,
|
||||
PassedCount = passed,
|
||||
ExcellentCount = excellent,
|
||||
HighestScore = scores.Max(),
|
||||
AverageScore = Math.Round(average, 1),
|
||||
MedianScore = Math.Round(median, 1),
|
||||
LowestScore = scores.Min(),
|
||||
StandardDeviation = Math.Round((decimal)Math.Sqrt(variance), 2),
|
||||
PassRate = Math.Round((decimal)passed / scores.Length * 100m, 2),
|
||||
ExcellentRate = Math.Round((decimal)excellent / scores.Length * 100m, 2),
|
||||
CalculatedAt = calculatedAt
|
||||
};
|
||||
statistic.ScoreBands =
|
||||
[
|
||||
CreateBand(statistic.Id, "0–59", 0m, 60m,
|
||||
scores.Count(x => x < 60m), 0),
|
||||
CreateBand(statistic.Id, "60–69", 60m, 70m,
|
||||
scores.Count(x => x >= 60m && x < 70m), 1),
|
||||
CreateBand(statistic.Id, "70–79", 70m, 80m,
|
||||
scores.Count(x => x >= 70m && x < 80m), 2),
|
||||
CreateBand(statistic.Id, "80–89", 80m, 90m,
|
||||
scores.Count(x => x >= 80m && x < 90m), 3),
|
||||
CreateBand(statistic.Id, "90–100", 90m, null,
|
||||
scores.Count(x => x >= 90m), 4)
|
||||
];
|
||||
return statistic;
|
||||
}
|
||||
|
||||
private static TeachingTaskGradeScoreBand CreateBand(
|
||||
Guid statisticId,
|
||||
string label,
|
||||
decimal lowerBound,
|
||||
decimal? upperBound,
|
||||
int count,
|
||||
int sortOrder) => new()
|
||||
{
|
||||
TeachingTaskGradeStatisticId = statisticId,
|
||||
Label = label,
|
||||
LowerBound = lowerBound,
|
||||
UpperBound = upperBound,
|
||||
StudentCount = count,
|
||||
SortOrder = sortOrder
|
||||
};
|
||||
|
||||
private sealed record ScoreRow(
|
||||
Guid GradeSheetId,
|
||||
Guid TeachingTaskId,
|
||||
decimal Score,
|
||||
Guid ClassId,
|
||||
Guid MajorId,
|
||||
Guid CollegeId);
|
||||
private sealed record StatisticsTarget(Guid CourseId, Guid AcademicTermId);
|
||||
}
|
||||
|
||||
@@ -68,6 +68,10 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
public DbSet<GradeItemScore> GradeItemScores => Set<GradeItemScore>();
|
||||
public DbSet<CourseGradeStatistic> CourseGradeStatistics =>
|
||||
Set<CourseGradeStatistic>();
|
||||
public DbSet<TeachingTaskGradeStatistic> TeachingTaskGradeStatistics =>
|
||||
Set<TeachingTaskGradeStatistic>();
|
||||
public DbSet<TeachingTaskGradeScoreBand> TeachingTaskGradeScoreBands =>
|
||||
Set<TeachingTaskGradeScoreBand>();
|
||||
public DbSet<CourseGradeStatisticsRefreshJob> CourseGradeStatisticsRefreshJobs =>
|
||||
Set<CourseGradeStatisticsRefreshJob>();
|
||||
public DbSet<OtherExamBatch> OtherExamBatches => Set<OtherExamBatch>();
|
||||
@@ -868,6 +872,46 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
builder.Entity<TeachingTaskGradeStatistic>(entity =>
|
||||
{
|
||||
entity.Property(x => x.HighestScore).HasPrecision(5, 1);
|
||||
entity.Property(x => x.AverageScore).HasPrecision(5, 1);
|
||||
entity.Property(x => x.MedianScore).HasPrecision(5, 1);
|
||||
entity.Property(x => x.LowestScore).HasPrecision(5, 1);
|
||||
entity.Property(x => x.StandardDeviation).HasPrecision(6, 2);
|
||||
entity.Property(x => x.PassRate).HasPrecision(5, 2);
|
||||
entity.Property(x => x.ExcellentRate).HasPrecision(5, 2);
|
||||
entity.HasIndex(x => x.GradeSheetId).IsUnique();
|
||||
entity.HasIndex(x => x.TeachingTaskId).IsUnique();
|
||||
entity.HasIndex(x => new { x.CourseId, x.AcademicTermId });
|
||||
entity.HasOne(x => x.GradeSheet).WithOne()
|
||||
.HasForeignKey<TeachingTaskGradeStatistic>(x => x.GradeSheetId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.TeachingTask).WithOne()
|
||||
.HasForeignKey<TeachingTaskGradeStatistic>(x => x.TeachingTaskId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne<Course>().WithMany().HasForeignKey(x => x.CourseId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne<AcademicTerm>().WithMany().HasForeignKey(x => x.AcademicTermId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
builder.Entity<TeachingTaskGradeScoreBand>(entity =>
|
||||
{
|
||||
entity.Property(x => x.Label).HasMaxLength(30);
|
||||
entity.Property(x => x.LowerBound).HasPrecision(5, 1);
|
||||
entity.Property(x => x.UpperBound).HasPrecision(5, 1);
|
||||
entity.HasIndex(x => new
|
||||
{
|
||||
x.TeachingTaskGradeStatisticId,
|
||||
x.SortOrder
|
||||
}).IsUnique();
|
||||
entity.HasOne(x => x.TeachingTaskGradeStatistic)
|
||||
.WithMany(x => x.ScoreBands)
|
||||
.HasForeignKey(x => x.TeachingTaskGradeStatisticId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<CourseGradeStatisticsRefreshJob>(entity =>
|
||||
{
|
||||
entity.Property(x => x.ErrorMessage).HasMaxLength(2000);
|
||||
|
||||
@@ -88,6 +88,8 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"20260808_46_course_grade_statistics";
|
||||
private const string CourseGradeDistributionMigration =
|
||||
"20260809_47_course_grade_distribution";
|
||||
private const string TeachingTaskGradeAnalyticsMigration =
|
||||
"20260809_48_teaching_task_grade_analytics";
|
||||
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -647,9 +649,25 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
CourseGradeStatisticsMigration,
|
||||
CourseGradeStatisticsStatements,
|
||||
cancellationToken);
|
||||
var courseGradeStatisticColumns = (await db.Database
|
||||
.SqlQueryRaw<string>(
|
||||
"""
|
||||
SELECT name AS "Value"
|
||||
FROM pragma_table_info('CourseGradeStatistics')
|
||||
""")
|
||||
.ToListAsync(cancellationToken))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var missingDistributionStatements = CourseGradeDistributionColumns
|
||||
.Select((column, index) => new { column, index })
|
||||
.Where(x => !courseGradeStatisticColumns.Contains(x.column))
|
||||
.Select(x => CourseGradeDistributionStatements[x.index]);
|
||||
await ApplyMigrationAsync(
|
||||
CourseGradeDistributionMigration,
|
||||
CourseGradeDistributionStatements,
|
||||
missingDistributionStatements,
|
||||
cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
TeachingTaskGradeAnalyticsMigration,
|
||||
TeachingTaskGradeAnalyticsStatements,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
@@ -2241,6 +2259,26 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"""ALTER TABLE "CourseGradeStatistics" ADD COLUMN "From90To100Count" INTEGER NOT NULL DEFAULT 0;"""
|
||||
];
|
||||
|
||||
private static readonly string[] CourseGradeDistributionColumns =
|
||||
[
|
||||
"Below60Count",
|
||||
"From60To69Count",
|
||||
"From70To79Count",
|
||||
"From80To89Count",
|
||||
"From90To100Count"
|
||||
];
|
||||
|
||||
private static readonly string[] TeachingTaskGradeAnalyticsStatements =
|
||||
[
|
||||
"""CREATE TABLE IF NOT EXISTS "TeachingTaskGradeStatistics" ("Id" TEXT NOT NULL CONSTRAINT "PK_TeachingTaskGradeStatistics" PRIMARY KEY, "GradeSheetId" TEXT NOT NULL, "TeachingTaskId" TEXT NOT NULL, "CourseId" TEXT NOT NULL, "AcademicTermId" TEXT NOT NULL, "StudentCount" INTEGER NOT NULL, "PassedCount" INTEGER NOT NULL, "ExcellentCount" INTEGER NOT NULL, "HighestScore" TEXT NOT NULL, "AverageScore" TEXT NOT NULL, "MedianScore" TEXT NOT NULL, "LowestScore" TEXT NOT NULL, "StandardDeviation" TEXT NOT NULL, "PassRate" TEXT NOT NULL, "ExcellentRate" TEXT NOT NULL, "CalculatedAt" TEXT NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_TeachingTaskGradeStatistics_GradeSheets" FOREIGN KEY ("GradeSheetId") REFERENCES "GradeSheets" ("Id") ON DELETE CASCADE, CONSTRAINT "FK_TeachingTaskGradeStatistics_TeachingTasks" FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE CASCADE, CONSTRAINT "FK_TeachingTaskGradeStatistics_Courses" FOREIGN KEY ("CourseId") REFERENCES "Courses" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_TeachingTaskGradeStatistics_AcademicTerms" FOREIGN KEY ("AcademicTermId") REFERENCES "AcademicTerms" ("Id") ON DELETE RESTRICT);""",
|
||||
"""CREATE UNIQUE INDEX IF NOT EXISTS "IX_TeachingTaskGradeStatistics_GradeSheetId" ON "TeachingTaskGradeStatistics" ("GradeSheetId");""",
|
||||
"""CREATE UNIQUE INDEX IF NOT EXISTS "IX_TeachingTaskGradeStatistics_TeachingTaskId" ON "TeachingTaskGradeStatistics" ("TeachingTaskId");""",
|
||||
"""CREATE INDEX IF NOT EXISTS "IX_TeachingTaskGradeStatistics_CourseId_AcademicTermId" ON "TeachingTaskGradeStatistics" ("CourseId", "AcademicTermId");""",
|
||||
"""CREATE INDEX IF NOT EXISTS "IX_TeachingTaskGradeStatistics_AcademicTermId" ON "TeachingTaskGradeStatistics" ("AcademicTermId");""",
|
||||
"""CREATE TABLE IF NOT EXISTS "TeachingTaskGradeScoreBands" ("Id" TEXT NOT NULL CONSTRAINT "PK_TeachingTaskGradeScoreBands" PRIMARY KEY, "TeachingTaskGradeStatisticId" TEXT NOT NULL, "Label" TEXT NOT NULL, "LowerBound" TEXT NOT NULL, "UpperBound" TEXT NULL, "StudentCount" INTEGER NOT NULL, "SortOrder" INTEGER NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_TeachingTaskGradeScoreBands_Statistics" FOREIGN KEY ("TeachingTaskGradeStatisticId") REFERENCES "TeachingTaskGradeStatistics" ("Id") ON DELETE CASCADE);""",
|
||||
"""CREATE UNIQUE INDEX IF NOT EXISTS "IX_TeachingTaskGradeScoreBands_StatisticId_SortOrder" ON "TeachingTaskGradeScoreBands" ("TeachingTaskGradeStatisticId", "SortOrder");"""
|
||||
];
|
||||
|
||||
private static readonly string[] ApprovalTableStatements =
|
||||
[
|
||||
"""CREATE TABLE "CourseExemptions" ("Id" TEXT NOT NULL CONSTRAINT "PK_CourseExemptions" PRIMARY KEY, "StudentId" TEXT NOT NULL, "TeachingTaskId" TEXT NOT NULL, "Reason" TEXT NOT NULL, "Status" INTEGER NOT NULL, "ReviewComment" TEXT NULL, "SubmittedAt" TEXT NOT NULL, "ReviewedAt" TEXT NULL, "ReviewedByUserId" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_CourseExemptions_Students" FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_CourseExemptions_TeachingTasks" FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE RESTRICT);""",
|
||||
|
||||
+6549
File diff suppressed because it is too large
Load Diff
+132
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class TeachingTaskGradeAnalytics : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TeachingTaskGradeStatistics",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
GradeSheetId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
TeachingTaskId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
CourseId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
AcademicTermId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
StudentCount = table.Column<int>(type: "int", nullable: false),
|
||||
PassedCount = table.Column<int>(type: "int", nullable: false),
|
||||
ExcellentCount = table.Column<int>(type: "int", nullable: false),
|
||||
HighestScore = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false),
|
||||
AverageScore = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false),
|
||||
MedianScore = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false),
|
||||
LowestScore = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false),
|
||||
StandardDeviation = table.Column<decimal>(type: "decimal(6,2)", precision: 6, scale: 2, nullable: false),
|
||||
PassRate = table.Column<decimal>(type: "decimal(5,2)", precision: 5, scale: 2, nullable: false),
|
||||
ExcellentRate = table.Column<decimal>(type: "decimal(5,2)", precision: 5, scale: 2, nullable: false),
|
||||
CalculatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TeachingTaskGradeStatistics", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TeachingTaskGradeStatistics_AcademicTerms_AcademicTermId",
|
||||
column: x => x.AcademicTermId,
|
||||
principalTable: "AcademicTerms",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_TeachingTaskGradeStatistics_Courses_CourseId",
|
||||
column: x => x.CourseId,
|
||||
principalTable: "Courses",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_TeachingTaskGradeStatistics_GradeSheets_GradeSheetId",
|
||||
column: x => x.GradeSheetId,
|
||||
principalTable: "GradeSheets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_TeachingTaskGradeStatistics_TeachingTasks_TeachingTaskId",
|
||||
column: x => x.TeachingTaskId,
|
||||
principalTable: "TeachingTasks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TeachingTaskGradeScoreBands",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
TeachingTaskGradeStatisticId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
Label = table.Column<string>(type: "varchar(30)", maxLength: 30, nullable: false),
|
||||
LowerBound = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false),
|
||||
UpperBound = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: true),
|
||||
StudentCount = table.Column<int>(type: "int", nullable: false),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TeachingTaskGradeScoreBands", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TeachingTaskGradeScoreBands_TeachingTaskGradeStatistics_Teac~",
|
||||
column: x => x.TeachingTaskGradeStatisticId,
|
||||
principalTable: "TeachingTaskGradeStatistics",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TeachingTaskGradeScoreBands_TeachingTaskGradeStatisticId_Sor~",
|
||||
table: "TeachingTaskGradeScoreBands",
|
||||
columns: new[] { "TeachingTaskGradeStatisticId", "SortOrder" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TeachingTaskGradeStatistics_AcademicTermId",
|
||||
table: "TeachingTaskGradeStatistics",
|
||||
column: "AcademicTermId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TeachingTaskGradeStatistics_CourseId_AcademicTermId",
|
||||
table: "TeachingTaskGradeStatistics",
|
||||
columns: new[] { "CourseId", "AcademicTermId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TeachingTaskGradeStatistics_GradeSheetId",
|
||||
table: "TeachingTaskGradeStatistics",
|
||||
column: "GradeSheetId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TeachingTaskGradeStatistics_TeachingTaskId",
|
||||
table: "TeachingTaskGradeStatistics",
|
||||
column: "TeachingTaskId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "TeachingTaskGradeScoreBands");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TeachingTaskGradeStatistics");
|
||||
}
|
||||
}
|
||||
}
|
||||
+183
-15
@@ -996,6 +996,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
|
||||
b.Property<int>("Below60Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CalculatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
@@ -1005,6 +1008,18 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("From60To69Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("From70To79Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("From80To89Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("From90To100Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("HighestScore")
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
@@ -1020,21 +1035,6 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Property<int>("PassedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Below60Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("From60To69Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("From70To79Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("From80To89Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("From90To100Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Scope")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -4109,6 +4109,127 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.ToTable("TeachingTaskClasses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeScoreBand", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("varchar(30)");
|
||||
|
||||
b.Property<decimal>("LowerBound")
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("StudentCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("TeachingTaskGradeStatisticId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal?>("UpperBound")
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TeachingTaskGradeStatisticId", "SortOrder")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TeachingTaskGradeScoreBands");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("AcademicTermId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<decimal>("AverageScore")
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
|
||||
b.Property<DateTime>("CalculatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid>("CourseId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("ExcellentCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("ExcellentRate")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.Property<Guid>("GradeSheetId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<decimal>("HighestScore")
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
|
||||
b.Property<decimal>("LowestScore")
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
|
||||
b.Property<decimal>("MedianScore")
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
|
||||
b.Property<decimal>("PassRate")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.Property<int>("PassedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("StandardDeviation")
|
||||
.HasPrecision(6, 2)
|
||||
.HasColumnType("decimal(6,2)");
|
||||
|
||||
b.Property<int>("StudentCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("TeachingTaskId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AcademicTermId");
|
||||
|
||||
b.HasIndex("GradeSheetId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("TeachingTaskId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CourseId", "AcademicTermId");
|
||||
|
||||
b.ToTable("TeachingTaskGradeStatistics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -6050,6 +6171,48 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Navigation("TeachingTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeScoreBand", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", "TeachingTaskGradeStatistic")
|
||||
.WithMany("ScoreBands")
|
||||
.HasForeignKey("TeachingTaskGradeStatisticId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("TeachingTaskGradeStatistic");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AcademicTermId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Course", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CourseId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet")
|
||||
.WithOne()
|
||||
.HasForeignKey("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", "GradeSheetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask")
|
||||
.WithOne()
|
||||
.HasForeignKey("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", "TeachingTaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("GradeSheet");
|
||||
|
||||
b.Navigation("TeachingTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding")
|
||||
@@ -6368,6 +6531,11 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Navigation("Teachers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", b =>
|
||||
{
|
||||
b.Navigation("ScoreBands");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b =>
|
||||
{
|
||||
b.Navigation("AllowedClassrooms");
|
||||
|
||||
Reference in New Issue
Block a user