成绩提交时按考试一次性计算并存储单科名次、排名百分比、等级、合格状态、总分排名及整场合格状态。
新增 result_statistics、registration_result_summaries 两张统计表,现有数据库首次启动时自动回填。 SQL 查询首先通过 examId 限定范围。 科目成绩只排序一次,并使用 registration/subject 字典组织数据。 汇总接口与成绩明细接口已拆分,明细、成绩录入名单和特征分名单均改为服务端分页。 所有成绩写入口统一限制为:0—满分,且只能是整数或 .5,包括手工录入、批量录入、Excel 导入和复议改分。 前端也增加了相同的即时校验。 修复其他考试卡片显示 undefined 的问题。
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Eis.Infrastructure.Administration;
|
||||
|
||||
namespace Eis.Infrastructure.Tests.Administration;
|
||||
|
||||
public sealed class AdminResultStatisticsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0, 100, true)]
|
||||
[InlineData(10, 100, true)]
|
||||
[InlineData(10.5, 100, true)]
|
||||
[InlineData(10.1, 100, false)]
|
||||
[InlineData(-0.5, 100, false)]
|
||||
[InlineData(100.5, 100, false)]
|
||||
public void IsValidScore_OnlyAcceptsWholeOrHalfPoints(double score, double fullScore, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, AdminResultStatistics.IsValidScore(score, fullScore));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ComputesEachSubjectAndTotalRankingOnceForTheExam()
|
||||
{
|
||||
var exam = Exam("rank_percent", 50);
|
||||
var registrations = new[] { Registration("r1"), Registration("r2"), Registration("r3") };
|
||||
var results = new[]
|
||||
{
|
||||
Result("r1-s1", "r1", "s1", 90), Result("r1-s2", "r1", "s2", 80),
|
||||
Result("r2-s1", "r2", "s1", 90), Result("r2-s2", "r2", "s2", 70),
|
||||
Result("r3-s1", "r3", "s1", 60), Result("r3-s2", "r3", "s2", 70)
|
||||
};
|
||||
|
||||
var calculation = AdminResultStatistics.Calculate(exam, registrations, results, "2026-07-23T00:00:00.000Z");
|
||||
|
||||
Assert.Equal(6, calculation.Metrics.Count);
|
||||
Assert.Equal(1, calculation.Metrics.Single(item => item.ResultId == "r1-s1").Rank);
|
||||
Assert.Equal(1, calculation.Metrics.Single(item => item.ResultId == "r2-s1").Rank);
|
||||
Assert.Equal(3, calculation.Metrics.Single(item => item.ResultId == "r3-s1").Rank);
|
||||
Assert.Equal(2, calculation.Metrics.Single(item => item.ResultId == "r2-s2").Rank);
|
||||
Assert.Equal(2, calculation.Metrics.Single(item => item.ResultId == "r3-s2").Rank);
|
||||
|
||||
var summaries = calculation.Summaries.ToDictionary(item => item.RegistrationId);
|
||||
Assert.Equal(170, summaries["r1"].TotalScore);
|
||||
Assert.Equal(1, summaries["r1"].Rank);
|
||||
Assert.True(summaries["r1"].Qualified);
|
||||
Assert.Equal(2, summaries["r2"].Rank);
|
||||
Assert.True(summaries["r2"].Qualified);
|
||||
Assert.Equal(3, summaries["r3"].Rank);
|
||||
Assert.False(summaries["r3"].Qualified);
|
||||
Assert.All(summaries.Values, item => Assert.Equal(3, item.CohortSize));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_PersistsIncompleteAndDraftStatesWithoutIncludingDraftInPublishedSummary()
|
||||
{
|
||||
var exam = Exam("subject_scores", 0);
|
||||
var registrations = new[] { Registration("r1"), Registration("r2") };
|
||||
var results = new[]
|
||||
{
|
||||
Result("r1-s1", "r1", "s1", 95),
|
||||
Result("r1-s2", "r1", "s2", 88, published: false),
|
||||
Result("r2-s1", "r2", "s1", 80),
|
||||
Result("r2-s2", "r2", "s2", 75)
|
||||
};
|
||||
|
||||
var calculation = AdminResultStatistics.Calculate(exam, registrations, results, "2026-07-23T00:00:00.000Z");
|
||||
var draft = calculation.Metrics.Single(item => item.ResultId == "r1-s2");
|
||||
var summaries = calculation.Summaries.ToDictionary(item => item.RegistrationId);
|
||||
|
||||
Assert.Equal("待发布", draft.Grade);
|
||||
Assert.Equal(2, draft.CohortSize);
|
||||
Assert.False(summaries["r1"].Complete);
|
||||
Assert.Null(summaries["r1"].Qualified);
|
||||
Assert.Equal(1, summaries["r1"].PublishedSubjectCount);
|
||||
Assert.True(summaries["r2"].Complete);
|
||||
}
|
||||
|
||||
private static OperationalExam Exam(string passPolicy, double passValue)
|
||||
{
|
||||
var subjects = new[]
|
||||
{
|
||||
new JsonObject { ["id"] = "s1", ["name"] = "语文", ["fullScore"] = 100, ["passRule"] = "fixed_score", ["passValue"] = 60 },
|
||||
new JsonObject { ["id"] = "s2", ["name"] = "数学", ["fullScore"] = 100, ["passRule"] = "fixed_score", ["passValue"] = 60 }
|
||||
};
|
||||
return new("exam-1", new JsonObject
|
||||
{
|
||||
["id"] = "exam-1",
|
||||
["passPolicy"] = passPolicy,
|
||||
["passValue"] = passValue
|
||||
}, subjects);
|
||||
}
|
||||
|
||||
private static OperationalRegistration Registration(string id) => new(
|
||||
id,
|
||||
$"user-{id}",
|
||||
"exam-1",
|
||||
"approved",
|
||||
"paid",
|
||||
null,
|
||||
"2026-07-23T00:00:00.000Z",
|
||||
new JsonObject
|
||||
{
|
||||
["id"] = id,
|
||||
["subjectIds"] = new JsonArray("s1", "s2")
|
||||
});
|
||||
|
||||
private static AdminWorkflowResult Result(
|
||||
string id,
|
||||
string registrationId,
|
||||
string subjectId,
|
||||
double score,
|
||||
bool published = true) => new(
|
||||
id,
|
||||
registrationId,
|
||||
subjectId,
|
||||
score,
|
||||
published ? "" : "待发布",
|
||||
published,
|
||||
"2026-07-23T00:00:00.000Z",
|
||||
published ? "2026-07-23T00:00:00.000Z" : null);
|
||||
}
|
||||
Reference in New Issue
Block a user