配置保存在独立数据库表 CourseGradeStatisticsRefreshSettings,不是修改 appsettings.json。 后台每 10 秒读取配置,仅到期扫描;成绩录入、导入、审批时不再立即创建统计任务。 扫描发现过期数据后,仍通过持久任务、Outbox 和 RabbitMQ 执行;Redis统计缓存由处理任务统一刷新。
177 lines
6.0 KiB
C#
177 lines
6.0 KiB
C#
using Jiaowu.Api.Domain.Academic;
|
|
using Jiaowu.Api.Domain.System;
|
|
using Jiaowu.Api.Infrastructure.Grades;
|
|
using Jiaowu.Api.Infrastructure.Persistence;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace Jiaowu.Api.Tests;
|
|
|
|
public sealed class CourseGradeStatisticsRefreshSchedulerTests
|
|
{
|
|
[Fact]
|
|
public async Task EnqueueDueAsync_UsesPersistentScheduleAndHonorsInterval()
|
|
{
|
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
|
await connection.OpenAsync();
|
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
.UseSqlite(connection)
|
|
.Options;
|
|
await using var db = new AppDbContext(options);
|
|
await db.Database.EnsureCreatedAsync();
|
|
var scheduler = new CourseGradeStatisticsRefreshScheduler(
|
|
db,
|
|
NullLogger<CourseGradeStatisticsRefreshScheduler>.Instance);
|
|
var now = new DateTime(2026, 8, 9, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
await scheduler.EnqueueDueAsync(now, CancellationToken.None);
|
|
var setting = Assert.Single(await db.CourseGradeStatisticsRefreshSettings.ToListAsync());
|
|
Assert.True(setting.IsEnabled);
|
|
Assert.Equal(now, setting.LastRunAt);
|
|
|
|
await scheduler.EnqueueDueAsync(now.AddMinutes(1), CancellationToken.None);
|
|
Assert.Equal(now, setting.LastRunAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnqueueStaleAsync_GroupsByCourseTerm_AndSkipsActiveTarget()
|
|
{
|
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
|
await connection.OpenAsync();
|
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
.UseSqlite(connection)
|
|
.Options;
|
|
await using var db = new AppDbContext(options);
|
|
await db.Database.EnsureCreatedAsync();
|
|
|
|
var college = new College { Code = "SCHEDULE", Name = "定时任务学院" };
|
|
var major = new Major
|
|
{
|
|
Code = "SCHEDULE-M",
|
|
Name = "定时任务专业",
|
|
College = college,
|
|
DegreeType = "本科"
|
|
};
|
|
var administrativeClass = new AdministrativeClass
|
|
{
|
|
Code = "SCHEDULE-C",
|
|
Name = "定时任务一班",
|
|
Major = major,
|
|
Grade = 2026
|
|
};
|
|
var student = new Student
|
|
{
|
|
StudentNumber = "SCHEDULE-001",
|
|
Name = "定时任务学生",
|
|
AdministrativeClass = administrativeClass,
|
|
EnrollmentYear = 2026,
|
|
EnrollmentDate = new DateOnly(2026, 9, 1)
|
|
};
|
|
var course = new Course
|
|
{
|
|
Code = "SCHEDULE-COURSE",
|
|
Name = "定时任务课程",
|
|
College = college,
|
|
Credits = 2,
|
|
TotalHours = 32,
|
|
LectureHours = 32
|
|
};
|
|
var term = new AcademicTerm
|
|
{
|
|
Code = "2026-A",
|
|
Name = "2026-2027 学年第一学期",
|
|
AcademicYear = "2026-2027",
|
|
Season = TermSeason.Autumn,
|
|
StartDate = new DateOnly(2026, 9, 1),
|
|
EndDate = new DateOnly(2027, 1, 15)
|
|
};
|
|
var firstTask = new TeachingTask
|
|
{
|
|
TaskNumber = "SCHEDULE-T1",
|
|
Name = "定时任务教学班一",
|
|
Course = course,
|
|
AcademicTerm = term,
|
|
Capacity = 30
|
|
};
|
|
var secondTask = new TeachingTask
|
|
{
|
|
TaskNumber = "SCHEDULE-T2",
|
|
Name = "定时任务教学班二",
|
|
Course = course,
|
|
AcademicTerm = term,
|
|
Capacity = 30
|
|
};
|
|
var firstSheet = PublishedSheet(firstTask, student, 80m);
|
|
var secondSheet = PublishedSheet(secondTask, student, 90m);
|
|
db.GradeSheets.AddRange(firstSheet, secondSheet);
|
|
await db.SaveChangesAsync();
|
|
|
|
var scheduler = new CourseGradeStatisticsRefreshScheduler(
|
|
db,
|
|
NullLogger<CourseGradeStatisticsRefreshScheduler>.Instance);
|
|
|
|
var queued = await scheduler.EnqueueStaleAsync(100, CancellationToken.None);
|
|
var queuedAgain = await scheduler.EnqueueStaleAsync(100, CancellationToken.None);
|
|
|
|
Assert.Equal(1, queued);
|
|
Assert.Equal(0, queuedAgain);
|
|
var job = Assert.Single(await db.CourseGradeStatisticsRefreshJobs.ToListAsync());
|
|
var outbox = Assert.Single(await db.BackgroundJobOutboxMessages.ToListAsync());
|
|
Assert.Equal(BackgroundJobKind.CourseGradeStatisticsRefresh, outbox.JobKind);
|
|
|
|
job.Status = CourseGradeStatisticsRefreshJobStatus.Succeeded;
|
|
db.TeachingTaskGradeStatistics.AddRange(
|
|
FreshStatistic(firstSheet, course, term),
|
|
FreshStatistic(secondSheet, course, term));
|
|
await db.SaveChangesAsync();
|
|
|
|
var queuedAfterFreshStatistics = await scheduler.EnqueueStaleAsync(
|
|
100,
|
|
CancellationToken.None);
|
|
|
|
Assert.Equal(0, queuedAfterFreshStatistics);
|
|
Assert.Single(await db.CourseGradeStatisticsRefreshJobs.ToListAsync());
|
|
}
|
|
|
|
private static GradeSheet PublishedSheet(
|
|
TeachingTask task,
|
|
Student student,
|
|
decimal score)
|
|
{
|
|
var sheet = new GradeSheet
|
|
{
|
|
TeachingTask = task,
|
|
Status = GradeSheetStatus.Published,
|
|
PublishedAt = DateTime.UtcNow
|
|
};
|
|
sheet.Records.Add(new GradeRecord
|
|
{
|
|
GradeSheet = sheet,
|
|
Student = student,
|
|
TotalScore = score,
|
|
GradePoint = 3m
|
|
});
|
|
return sheet;
|
|
}
|
|
|
|
private static TeachingTaskGradeStatistic FreshStatistic(
|
|
GradeSheet sheet,
|
|
Course course,
|
|
AcademicTerm term) => new()
|
|
{
|
|
GradeSheetId = sheet.Id,
|
|
TeachingTaskId = sheet.TeachingTaskId,
|
|
CourseId = course.Id,
|
|
AcademicTermId = term.Id,
|
|
StudentCount = 1,
|
|
PassedCount = 1,
|
|
HighestScore = 80m,
|
|
AverageScore = 80m,
|
|
MedianScore = 80m,
|
|
LowestScore = 80m,
|
|
PassRate = 100m,
|
|
CalculatedAt = DateTime.UtcNow.AddMinutes(1)
|
|
};
|
|
}
|