Files
tiku-backend.net/Tiku.Application/Learning/QuestionGrading.cs
xiong c497a3ca8d
Some checks failed
ci / release-gate (push) Has been cancelled
清理代码
2026-08-03 12:31:39 +08:00

161 lines
5.6 KiB
C#

using System.Globalization;
using System.Text;
using System.Text.Json;
using Tiku.Domain.Learning;
namespace Tiku.Application.Learning;
public sealed record QuestionGradingInput(
string QuestionType,
int? CorrectOptionIndex,
JsonElement CorrectOptionIndices,
string? CorrectAnswerText,
JsonElement GradingRules,
IReadOnlyCollection<int>? SelectedOptionIndices,
string? AnswerText,
decimal Score);
public sealed record QuestionGradingResult(
AnswerGradingStatus Status,
bool? IsCorrect,
decimal? AwardedScore);
public static class QuestionGrader
{
private static readonly IReadOnlySet<string> SubjectiveTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"short_answer", "reading", "programming"
};
public static QuestionGradingResult Grade(QuestionGradingInput input)
{
var type = input.QuestionType.Trim().ToLowerInvariant();
if (SubjectiveTypes.Contains(type))
return new QuestionGradingResult(AnswerGradingStatus.PendingReview, null, null);
var correct = type switch
{
"choice" => GradeChoice(input),
"multiple_choice" => GradeMultipleChoice(input),
"true_false" => GradeTrueFalse(input),
"fill_blank" => GradeFillBlank(input),
_ => throw new InvalidOperationException($"Unsupported question type '{input.QuestionType}'.")
};
return new QuestionGradingResult(
correct ? AnswerGradingStatus.Correct : AnswerGradingStatus.Incorrect,
correct,
correct ? input.Score : 0);
}
public static bool HasValidAuthoritativeAnswer(
string questionType,
int? correctOptionIndex,
JsonElement correctOptionIndices,
string? answerText)
{
return questionType.Trim().ToLowerInvariant() switch
{
"choice" => correctOptionIndex is >= 0,
"multiple_choice" => ReadIndices(correctOptionIndices).Count > 0,
"true_false" => TryNormalizeBoolean(answerText, out _),
"fill_blank" => !string.IsNullOrWhiteSpace(answerText),
"short_answer" or "reading" or "programming" => true,
_ => false
};
}
private static bool GradeChoice(QuestionGradingInput input)
{
var selected = input.SelectedOptionIndices?.Distinct().ToArray() ?? [];
return input.CorrectOptionIndex.HasValue &&
selected.Length == 1 &&
selected[0] == input.CorrectOptionIndex.Value;
}
private static bool GradeMultipleChoice(QuestionGradingInput input)
{
var expected = ReadIndices(input.CorrectOptionIndices);
var selected = (input.SelectedOptionIndices ?? []).Distinct().Order().ToArray();
return expected.Count > 0 && expected.SequenceEqual(selected);
}
private static bool GradeTrueFalse(QuestionGradingInput input)
{
return TryNormalizeBoolean(input.CorrectAnswerText, out var expected) &&
TryNormalizeBoolean(input.AnswerText, out var actual) &&
expected == actual;
}
private static bool GradeFillBlank(QuestionGradingInput input)
{
var accepted = new HashSet<string>(StringComparer.Ordinal);
AddNormalized(accepted, input.CorrectAnswerText);
if (input.GradingRules.ValueKind == JsonValueKind.Object &&
input.GradingRules.TryGetProperty("acceptedAnswers", out var alternatives) &&
alternatives.ValueKind == JsonValueKind.Array)
foreach (var alternative in alternatives.EnumerateArray())
if (alternative.ValueKind == JsonValueKind.String)
AddNormalized(accepted, alternative.GetString());
return accepted.Count > 0 && accepted.Contains(NormalizeText(input.AnswerText));
}
private static IReadOnlyList<int> ReadIndices(JsonElement value)
{
if (value.ValueKind != JsonValueKind.Array) return [];
return value.EnumerateArray()
.Where(item => item.TryGetInt32(out _))
.Select(item => item.GetInt32())
.Distinct()
.Order()
.ToArray();
}
private static bool TryNormalizeBoolean(string? value, out bool result)
{
switch (NormalizeText(value))
{
case "true" or "1" or "yes" or "正确" or "对":
result = true;
return true;
case "false" or "0" or "no" or "错误" or "错":
result = false;
return true;
default:
result = false;
return false;
}
}
private static void AddNormalized(ISet<string> values, string? value)
{
var normalized = NormalizeText(value);
if (normalized.Length > 0) values.Add(normalized);
}
private static string NormalizeText(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
var normalized = value.Normalize(NormalizationForm.FormKC).Trim().ToLower(CultureInfo.InvariantCulture);
var builder = new StringBuilder(normalized.Length);
var previousWhitespace = false;
foreach (var character in normalized)
if (char.IsWhiteSpace(character))
{
if (!previousWhitespace)
{
builder.Append(' ');
previousWhitespace = true;
}
}
else
{
builder.Append(character);
previousWhitespace = false;
}
return builder.ToString();
}
}