Compare commits
2
Commits
949f389854
...
9b32ee916c
No files matched your search
@@ -61,13 +61,20 @@ public sealed class ClickHouseAnalyticsController(
|
|||||||
GROUP BY attendanceDate ORDER BY attendanceDate
|
GROUP BY attendanceDate ORDER BY attendanceDate
|
||||||
""", cancellationToken);
|
""", cancellationToken);
|
||||||
var grades = await client.QueryAsync($"""
|
var grades = await client.QueryAsync($"""
|
||||||
|
SELECT academicTermId, academicTermName, studentCount,
|
||||||
|
round(weightedScore / nullIf(studentCount, 0), 2) AS averageScore,
|
||||||
|
round(passedCount / nullIf(studentCount, 0), 4) AS passRate
|
||||||
|
FROM
|
||||||
|
(
|
||||||
SELECT academicTermId, any(academicTermName) AS academicTermName,
|
SELECT academicTermId, any(academicTermName) AS academicTermName,
|
||||||
sum(studentCount) AS studentCount,
|
sum(studentCount) AS studentCount,
|
||||||
round(sum(averageScore * studentCount) / nullIf(sum(studentCount), 0), 2) AS averageScore,
|
sum(averageScore * studentCount) AS weightedScore,
|
||||||
round(sum(passedCount) / nullIf(sum(studentCount), 0), 4) AS passRate
|
sum(passedCount) AS passedCount
|
||||||
FROM {options.Database}.gradeStatistics FINAL
|
FROM {options.Database}.gradeStatistics FINAL
|
||||||
WHERE 1 = 1{collegeFilter}
|
WHERE 1 = 1{collegeFilter}
|
||||||
GROUP BY academicTermId ORDER BY academicTermName
|
GROUP BY academicTermId
|
||||||
|
)
|
||||||
|
ORDER BY academicTermName
|
||||||
""", cancellationToken);
|
""", cancellationToken);
|
||||||
|
|
||||||
// Audit data has no college dimension, so it is never exposed to a
|
// Audit data has no college dimension, so it is never exposed to a
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace Jiaowu.Api.Infrastructure.Scheduling;
|
|||||||
|
|
||||||
public sealed class WarningCheckWorker(IServiceScopeFactory scopeFactory, ILogger<WarningCheckWorker> logger) : BackgroundService
|
public sealed class WarningCheckWorker(IServiceScopeFactory scopeFactory, ILogger<WarningCheckWorker> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
|
private static readonly TimeZoneInfo ChinaTimeZone = ResolveChinaTimeZone();
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
while (!ct.IsCancellationRequested)
|
while (!ct.IsCancellationRequested)
|
||||||
@@ -25,22 +27,32 @@ public sealed class WarningCheckWorker(IServiceScopeFactory scopeFactory, ILogge
|
|||||||
|
|
||||||
private static async Task RunAsync(AppDbContext db, CancellationToken ct)
|
private static async Task RunAsync(AppDbContext db, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var nowUtc = DateTime.UtcNow;
|
||||||
var dow = (int)now.DayOfWeek == 0 ? 7 : (int)now.DayOfWeek;
|
var chinaNow = TimeZoneInfo.ConvertTime(
|
||||||
var currentMinute = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0, DateTimeKind.Utc);
|
new DateTimeOffset(nowUtc),
|
||||||
|
ChinaTimeZone);
|
||||||
|
var dow = (int)chinaNow.DayOfWeek == 0 ? 7 : (int)chinaNow.DayOfWeek;
|
||||||
|
var currentMinuteUtc = new DateTime(
|
||||||
|
nowUtc.Year,
|
||||||
|
nowUtc.Month,
|
||||||
|
nowUtc.Day,
|
||||||
|
nowUtc.Hour,
|
||||||
|
nowUtc.Minute,
|
||||||
|
0,
|
||||||
|
DateTimeKind.Utc);
|
||||||
|
|
||||||
var rules = await db.WarningRules
|
var rules = await db.WarningRules
|
||||||
.Where(r => r.IsEnabled && r.AutoCheckEnabled &&
|
.Where(r => r.IsEnabled && r.AutoCheckEnabled &&
|
||||||
r.CheckHour == now.Hour && r.CheckMinute == now.Minute &&
|
r.CheckHour == chinaNow.Hour && r.CheckMinute == chinaNow.Minute &&
|
||||||
(r.CheckDayOfWeek == null || r.CheckDayOfWeek == dow))
|
(r.CheckDayOfWeek == null || r.CheckDayOfWeek == dow))
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
foreach (var rule in rules)
|
foreach (var rule in rules)
|
||||||
{
|
{
|
||||||
if (rule.LastCheckAt.HasValue && rule.LastCheckAt.Value >= currentMinute)
|
if (rule.LastCheckAt.HasValue && rule.LastCheckAt.Value >= currentMinuteUtc)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
rule.LastCheckAt = now;
|
rule.LastCheckAt = nowUtc;
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
var studentIds = await db.Students.Where(x => x.Status == StudentStatus.Active).Select(x => x.Id).ToListAsync(ct);
|
var studentIds = await db.Students.Where(x => x.Status == StudentStatus.Active).Select(x => x.Id).ToListAsync(ct);
|
||||||
@@ -100,4 +112,23 @@ public sealed class WarningCheckWorker(IServiceScopeFactory scopeFactory, ILogge
|
|||||||
var gpa = grades.Average(x => x.GradePoint!.Value);
|
var gpa = grades.Average(x => x.GradePoint!.Value);
|
||||||
return gpa < rule.Threshold ? new WarningRecord { StudentId = sid, Type = WarningType.LowGPA, TriggerValue = Math.Round(gpa, 2), Detail = $"平均绩点 {gpa:F2},低于预警阈值 {rule.Threshold}。", AcademicTermId = termId } : null;
|
return gpa < rule.Threshold ? new WarningRecord { StudentId = sid, Type = WarningType.LowGPA, TriggerValue = Math.Round(gpa, 2), Detail = $"平均绩点 {gpa:F2},低于预警阈值 {rule.Threshold}。", AcademicTermId = termId } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static TimeZoneInfo ResolveChinaTimeZone()
|
||||||
|
{
|
||||||
|
foreach (var id in new[] { "Asia/Shanghai", "China Standard Time" })
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return TimeZoneInfo.FindSystemTimeZoneById(id);
|
||||||
|
}
|
||||||
|
catch (TimeZoneNotFoundException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (InvalidTimeZoneException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("服务器未提供中国标准时间时区定义。");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user