158 lines
5.8 KiB
C#
158 lines
5.8 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Tiku.Domain.Content;
|
|
|
|
namespace Tiku.Application.Content;
|
|
|
|
public sealed record TargetProfileFact(
|
|
Guid TargetDimensionDefinitionId,
|
|
Guid TargetNodeId,
|
|
IReadOnlySet<Guid> AncestorNodeIds);
|
|
|
|
public sealed record PlacementRuleConditionFact(
|
|
Guid TargetDimensionDefinitionId,
|
|
Guid TargetNodeId,
|
|
TargetRuleOperator Operator);
|
|
|
|
public sealed record PlacementRuleGroupFact(
|
|
int GroupOrder,
|
|
IReadOnlyCollection<PlacementRuleConditionFact> Conditions);
|
|
|
|
public static class QuestionApplicabilityEvaluator
|
|
{
|
|
public static bool Matches(
|
|
IReadOnlyCollection<TargetProfileFact> profile,
|
|
IReadOnlyCollection<PlacementRuleGroupFact> groups)
|
|
{
|
|
if (groups.Count == 0) return true;
|
|
var valuesByDimension = profile
|
|
.GroupBy(value => value.TargetDimensionDefinitionId)
|
|
.ToDictionary(group => group.Key, group => group.ToArray());
|
|
return groups.Any(group => group.Conditions.Count > 0 &&
|
|
group.Conditions.All(condition => Matches(valuesByDimension, condition)));
|
|
}
|
|
|
|
private static bool Matches(
|
|
IReadOnlyDictionary<Guid, TargetProfileFact[]> valuesByDimension,
|
|
PlacementRuleConditionFact condition)
|
|
{
|
|
valuesByDimension.TryGetValue(condition.TargetDimensionDefinitionId, out var values);
|
|
values ??= [];
|
|
var positiveMatch = condition.Operator switch
|
|
{
|
|
TargetRuleOperator.Exact or TargetRuleOperator.NotExact =>
|
|
values.Any(value => value.TargetNodeId == condition.TargetNodeId),
|
|
TargetRuleOperator.DescendantOf or TargetRuleOperator.NotDescendantOf =>
|
|
values.Any(value => value.TargetNodeId == condition.TargetNodeId ||
|
|
value.AncestorNodeIds.Contains(condition.TargetNodeId)),
|
|
_ => false
|
|
};
|
|
return condition.Operator is TargetRuleOperator.NotExact or TargetRuleOperator.NotDescendantOf
|
|
? !positiveMatch
|
|
: positiveMatch;
|
|
}
|
|
}
|
|
|
|
public sealed record QuestionDeliveryFingerprintInput(
|
|
string QuestionType,
|
|
string? Content,
|
|
JsonElement Options,
|
|
int? CorrectOptionIndex,
|
|
JsonElement CorrectOptionIndices,
|
|
string? AnswerText,
|
|
JsonElement SubQuestions,
|
|
string? CodeLang,
|
|
string? CodeTemplate);
|
|
|
|
public static class QuestionDeliveryFingerprint
|
|
{
|
|
public static string Compute(QuestionDeliveryFingerprintInput input)
|
|
{
|
|
using var stream = new MemoryStream();
|
|
using (var writer = new Utf8JsonWriter(stream))
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("questionType", NormalizeText(input.QuestionType));
|
|
writer.WriteString("content", NormalizeText(input.Content));
|
|
writer.WritePropertyName("options");
|
|
WriteCanonical(writer, input.Options);
|
|
if (input.CorrectOptionIndex.HasValue)
|
|
writer.WriteNumber("correctOptionIndex", input.CorrectOptionIndex.Value);
|
|
else
|
|
writer.WriteNull("correctOptionIndex");
|
|
writer.WritePropertyName("correctOptionIndices");
|
|
WriteCanonical(writer, input.CorrectOptionIndices);
|
|
writer.WriteString("answerText", NormalizeText(input.AnswerText));
|
|
writer.WritePropertyName("subQuestions");
|
|
WriteCanonical(writer, input.SubQuestions);
|
|
writer.WriteString("codeLang", NormalizeText(input.CodeLang));
|
|
writer.WriteString("codeTemplate", NormalizeText(input.CodeTemplate));
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
return Convert.ToHexString(SHA256.HashData(stream.ToArray())).ToLowerInvariant();
|
|
}
|
|
|
|
public static string NormalizePrompt(string? value) => NormalizeText(value);
|
|
|
|
private static string NormalizeText(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
|
|
var normalized = value.Normalize(NormalizationForm.FormKC).Trim();
|
|
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();
|
|
}
|
|
|
|
private static void WriteCanonical(Utf8JsonWriter writer, JsonElement value)
|
|
{
|
|
switch (value.ValueKind)
|
|
{
|
|
case JsonValueKind.Object:
|
|
writer.WriteStartObject();
|
|
foreach (var property in value.EnumerateObject().OrderBy(property => property.Name, StringComparer.Ordinal))
|
|
{
|
|
writer.WritePropertyName(property.Name);
|
|
WriteCanonical(writer, property.Value);
|
|
}
|
|
writer.WriteEndObject();
|
|
break;
|
|
case JsonValueKind.Array:
|
|
writer.WriteStartArray();
|
|
foreach (var item in value.EnumerateArray()) WriteCanonical(writer, item);
|
|
writer.WriteEndArray();
|
|
break;
|
|
case JsonValueKind.String:
|
|
writer.WriteStringValue(NormalizeText(value.GetString()));
|
|
break;
|
|
case JsonValueKind.Number:
|
|
value.WriteTo(writer);
|
|
break;
|
|
case JsonValueKind.True:
|
|
writer.WriteBooleanValue(true);
|
|
break;
|
|
case JsonValueKind.False:
|
|
writer.WriteBooleanValue(false);
|
|
break;
|
|
default:
|
|
writer.WriteNullValue();
|
|
break;
|
|
}
|
|
}
|
|
}
|