Files
tiku-backend.net/Tiku.Application/Content/ContentV2Rules.cs
xiong 3f5957d744
Some checks failed
ci / release-gate (push) Has been cancelled
feat(content): establish v2 learning delivery foundation
2026-08-06 09:52:18 +08:00

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;
}
}
}