diff --git a/Tiku.Api/Contracts/LearningDtos.cs b/Tiku.Api/Contracts/LearningDtos.cs
index 466275e..3d7b1f5 100644
--- a/Tiku.Api/Contracts/LearningDtos.cs
+++ b/Tiku.Api/Contracts/LearningDtos.cs
@@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using System.Text.Json;
using Tiku.Application.Learning;
-using Tiku.Application.QuestionBanks;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Learning;
@@ -201,16 +200,10 @@ public sealed class SubmitAnswerDto
public sealed class QuestionActionDto
{
///
- /// 题目 ID。
+ /// 已交付给当前学生的会话题目 ID。
///
[Required]
- public Guid QuestionId { get; set; }
-
- ///
- /// 来源。
- ///
- [Required]
- public QuestionSource Source { get; set; } = QuestionSource.Tenant;
+ public Guid SessionQuestionId { get; set; }
///
/// 是否收藏。
@@ -219,7 +212,21 @@ public sealed class QuestionActionDto
public QuestionActionCommand ToCommand()
{
- return new QuestionActionCommand(new QuestionLocator(Source, QuestionId), Favorite);
+ return new QuestionActionCommand(SessionQuestionId, Favorite);
+ }
+}
+
+public sealed class ResolveWrongQuestionDto
+{
+ [Required]
+ public Guid QuestionAssetOwnerTenantId { get; set; }
+
+ [Required]
+ public Guid QuestionAssetId { get; set; }
+
+ public ResolveWrongQuestionCommand ToCommand()
+ {
+ return new ResolveWrongQuestionCommand(QuestionAssetOwnerTenantId, QuestionAssetId);
}
}
diff --git a/Tiku.Api/Controllers/QuestionReviewController.cs b/Tiku.Api/Controllers/QuestionReviewController.cs
index 44d51b2..fc31733 100644
--- a/Tiku.Api/Controllers/QuestionReviewController.cs
+++ b/Tiku.Api/Controllers/QuestionReviewController.cs
@@ -75,7 +75,7 @@ public sealed class QuestionReviewController(
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task> ResolveWrongQuestion(
- QuestionActionDto request,
+ ResolveWrongQuestionDto request,
CancellationToken cancellationToken)
{
return Ok(await service.ResolveWrongQuestionAsync(
diff --git a/Tiku.Application/Learning/LearningActivityModels.cs b/Tiku.Application/Learning/LearningActivityModels.cs
index 39e432f..29aafa6 100644
--- a/Tiku.Application/Learning/LearningActivityModels.cs
+++ b/Tiku.Application/Learning/LearningActivityModels.cs
@@ -1,5 +1,4 @@
using System.Text.Json;
-using Tiku.Application.QuestionBanks;
using Tiku.Domain.Content;
using Tiku.Domain.Learning;
@@ -20,7 +19,11 @@ public sealed record SubmitPracticeSessionCommand(
Guid PracticeSessionId,
string IdempotencyKey);
-public sealed record QuestionActionCommand(QuestionLocator Locator, bool? Favorite);
+public sealed record QuestionActionCommand(Guid SessionQuestionId, bool? Favorite);
+
+public sealed record ResolveWrongQuestionCommand(
+ Guid QuestionAssetOwnerTenantId,
+ Guid QuestionAssetId);
public sealed record WordProgressCommand(
Guid WordId,
@@ -61,8 +64,10 @@ public sealed record LearningLeaderboardResult(
DateTimeOffset GeneratedAt);
public sealed record WrongQuestionReviewPlanItem(
- Guid QuestionReferenceId,
- QuestionLocator Locator,
+ Guid QuestionAssetOwnerTenantId,
+ Guid QuestionAssetId,
+ Guid LastQuestionRevisionId,
+ Guid LastQuestionPlacementId,
int WrongCount,
DateTimeOffset LastWrongAt);
@@ -124,13 +129,17 @@ public sealed record QuestionSolutionItem(
string? Explanation);
public sealed record FavoriteQuestionItem(
- Guid QuestionReferenceId,
- QuestionLocator Locator,
+ Guid QuestionAssetOwnerTenantId,
+ Guid QuestionAssetId,
+ Guid LastQuestionRevisionId,
+ Guid LastQuestionPlacementId,
DateTimeOffset CreatedAt);
public sealed record WrongQuestionItem(
- Guid QuestionReferenceId,
- QuestionLocator Locator,
+ Guid QuestionAssetOwnerTenantId,
+ Guid QuestionAssetId,
+ Guid LastQuestionRevisionId,
+ Guid LastQuestionPlacementId,
int WrongCount,
DateTimeOffset LastWrongAt,
DateTimeOffset? ResolvedAt);
@@ -191,18 +200,15 @@ public sealed record PracticeSessionDetailItem(
public sealed record PracticeSessionQuestionItem(
Guid SessionQuestionId,
- Guid? QuestionReferenceId,
- QuestionLocator? Locator,
- Guid? QuestionId,
- Guid? QuestionAssetId,
- Guid? QuestionRevisionId,
- Guid? QuestionPlacementId,
- Guid? AssessmentPolicyVersionId,
+ Guid QuestionAssetId,
+ Guid QuestionRevisionId,
+ Guid QuestionPlacementId,
+ Guid AssessmentPolicyVersionId,
string Type,
string? TypeLabel,
int? Difficulty,
JsonElement Tags,
- Guid? VersionId,
+ Guid VersionId,
string? Content,
JsonElement Options);
diff --git a/Tiku.Application/Learning/LearningServices.cs b/Tiku.Application/Learning/LearningServices.cs
index 415e36c..18236ba 100644
--- a/Tiku.Application/Learning/LearningServices.cs
+++ b/Tiku.Application/Learning/LearningServices.cs
@@ -25,7 +25,7 @@ public interface IQuestionReviewService
CancellationToken cancellationToken = default);
Task GetWrongQuestionReviewPlanAsync(LearningActor actor, LearningLimitFilter filter,
CancellationToken cancellationToken = default);
- Task ResolveWrongQuestionAsync(LearningActor actor, QuestionActionCommand command,
+ Task ResolveWrongQuestionAsync(LearningActor actor, ResolveWrongQuestionCommand command,
CancellationToken cancellationToken = default);
}
diff --git a/Tiku.Domain/Learning/LearningEntities.cs b/Tiku.Domain/Learning/LearningEntities.cs
index 3e4eb9c..cc36666 100644
--- a/Tiku.Domain/Learning/LearningEntities.cs
+++ b/Tiku.Domain/Learning/LearningEntities.cs
@@ -36,15 +36,12 @@ public sealed class PracticeSession : TenantEntity
public sealed class PracticeSessionQuestion : TenantEntity
{
public Guid PracticeSessionId { get; set; }
- public Guid? ContentReleaseQuestionId { get; set; }
- public Guid? QuestionRevisionId { get; set; }
- public Guid? QuestionPlacementId { get; set; }
- public Guid? AssessmentPolicyVersionId { get; set; }
- public Guid? QuestionAssetId { get; set; }
- public Guid? QuestionReferenceId { get; set; }
+ public Guid ContentReleaseQuestionId { get; set; }
+ public Guid QuestionRevisionId { get; set; }
+ public Guid QuestionPlacementId { get; set; }
+ public Guid AssessmentPolicyVersionId { get; set; }
+ public Guid QuestionAssetId { get; set; }
public Guid QuestionOwnerTenantId { get; set; }
- public Guid? QuestionId { get; set; }
- public Guid? QuestionVersionId { get; set; }
public int Position { get; set; }
public decimal? Score { get; set; }
public string QuestionType { get; set; } = "choice";
@@ -131,27 +128,15 @@ public sealed class LearningOperationIdempotency : Entity, ITenantOwned
public sealed class FavoriteQuestion : ITenantOwned
{
public Guid UserId { get; set; }
- public Guid QuestionReferenceId { get; set; }
- public Guid QuestionOwnerTenantId { get; set; }
- public Guid QuestionId { get; set; }
- public string Source { get; set; } = "imported";
+ public Guid QuestionAssetOwnerTenantId { get; set; }
+ public Guid QuestionAssetId { get; set; }
+ public Guid LastQuestionRevisionId { get; set; }
+ public Guid LastQuestionPlacementId { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public Guid TenantId { get; set; }
}
public sealed class WrongQuestion : ITenantOwned
-{
- public Guid UserId { get; set; }
- public Guid QuestionReferenceId { get; set; }
- public Guid QuestionOwnerTenantId { get; set; }
- public Guid QuestionId { get; set; }
- public int WrongCount { get; set; } = 1;
- public DateTimeOffset LastWrongAt { get; set; } = DateTimeOffset.UtcNow;
- public DateTimeOffset? ResolvedAt { get; set; }
- public Guid TenantId { get; set; }
-}
-
-public sealed class WrongQuestionV2 : ITenantOwned
{
public Guid UserId { get; set; }
public Guid QuestionAssetOwnerTenantId { get; set; }
diff --git a/Tiku.Infrastructure/Learning/Answering/AnsweringService.cs b/Tiku.Infrastructure/Learning/Answering/AnsweringService.cs
index 2e6270d..0cc11ee 100644
--- a/Tiku.Infrastructure/Learning/Answering/AnsweringService.cs
+++ b/Tiku.Infrastructure/Learning/Answering/AnsweringService.cs
@@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Npgsql;
using Tiku.Application.Learning;
-using Tiku.Application.QuestionBanks;
using Tiku.Application.Security;
using Tiku.Domain.Learning;
using Tiku.Infrastructure.Persistence;
@@ -44,10 +43,6 @@ internal sealed class AnsweringService(LearningServiceDependencies dependencies)
"Practice session was not found.");
await strongRevocationService.EnsureVersionAsync(
actor, session.StrongRevocationVersion, cancellationToken);
- if (!sessionQuestion.QuestionRevisionId.HasValue)
- throw new LearningValidationException(
- "practice_session_delivery_unsupported",
- "The practice session uses a retired delivery model.");
var deliveryFacts = ReadV2GradingFacts(sessionQuestion);
var revealedSolution = DefersSolutionUntilSubmission(session.Mode)
? null
diff --git a/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.Helpers.cs b/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.Helpers.cs
index 325889c..13f44cb 100644
--- a/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.Helpers.cs
+++ b/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.Helpers.cs
@@ -9,7 +9,6 @@ using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Learning;
-using Tiku.Domain.QuestionBanks;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.Learning;
@@ -25,15 +24,10 @@ internal abstract partial class LearningActivityServiceBase
.Where(item => item.TenantId == tenantId && item.PracticeSessionId == practiceSessionId)
.OrderBy(item => item.Position)
.ToArrayAsync(cancellationToken);
- if (sessionQuestions.Any(item => !item.QuestionRevisionId.HasValue))
- throw new LearningValidationException(
- "practice_session_delivery_unsupported",
- "The practice session uses a retired delivery model.");
-
var revisions = new List(sessionQuestions.Length);
foreach (var ownerGroup in sessionQuestions.GroupBy(item => item.QuestionOwnerTenantId))
{
- var revisionIds = ownerGroup.Select(item => item.QuestionRevisionId!.Value).Distinct().ToArray();
+ var revisionIds = ownerGroup.Select(item => item.QuestionRevisionId).Distinct().ToArray();
revisions.AddRange(await tenantExecutionScope.ExecuteAsync(
new SystemScopeRequest(
ownerGroup.Key,
@@ -57,9 +51,6 @@ internal abstract partial class LearningActivityServiceBase
version.TenantId == item.QuestionOwnerTenantId && version.Id == item.QuestionRevisionId);
return new PracticeSessionQuestionItem(
item.Id,
- null,
- null,
- null,
item.QuestionAssetId,
revision.Id,
item.QuestionPlacementId,
@@ -106,7 +97,7 @@ internal abstract partial class LearningActivityServiceBase
foreach (var ownerGroup in sessionQuestions.GroupBy(item => item.QuestionOwnerTenantId))
{
var ownerTenantId = ownerGroup.Key;
- var revisionIds = ownerGroup.Select(item => item.QuestionRevisionId!.Value).Distinct().ToArray();
+ var revisionIds = ownerGroup.Select(item => item.QuestionRevisionId).Distinct().ToArray();
revisions.AddRange(await tenantExecutionScope.ExecuteAsync(
new SystemScopeRequest(
ownerTenantId,
@@ -153,11 +144,6 @@ internal abstract partial class LearningActivityServiceBase
if (sessionQuestions.Length == 0)
throw new LearningValidationException("practice_session_empty",
"Practice session has no question snapshot.");
- if (sessionQuestions.Any(item => !item.QuestionRevisionId.HasValue))
- throw new LearningValidationException(
- "practice_session_delivery_unsupported",
- "The practice session uses a retired delivery model.");
-
var answers = await learningPersistence.AnswerRecords
.AsNoTracking()
.Where(answer =>
@@ -193,13 +179,11 @@ internal abstract partial class LearningActivityServiceBase
.Where(question =>
latestAnswers.TryGetValue(question.Id, out var answer) &&
answer.GradingStatus == AnswerGradingStatus.Incorrect)
- .Select(question => question.QuestionReferenceId ?? question.QuestionAssetId)
- .Where(id => id.HasValue)
- .Select(id => id!.Value)
+ .Select(question => question.Id)
.ToArray();
var v2Solutions = isFinal
? await LoadV2ReportSolutionsAsync(
- sessionQuestions.Where(item => item.QuestionRevisionId.HasValue).ToArray(), cancellationToken)
+ sessionQuestions, cancellationToken)
: new Dictionary();
var questionResults = sessionQuestions
.Select(question =>
@@ -209,8 +193,6 @@ internal abstract partial class LearningActivityServiceBase
return new
{
sessionQuestionId = question.Id,
- questionReferenceId = question.QuestionReferenceId,
- questionId = question.QuestionId,
questionAssetId = question.QuestionAssetId,
questionRevisionId = question.QuestionRevisionId,
questionPlacementId = question.QuestionPlacementId,
diff --git a/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.cs b/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.cs
index c57db14..8687736 100644
--- a/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.cs
+++ b/Tiku.Infrastructure/Learning/Foundation/LearningFoundation.cs
@@ -14,8 +14,6 @@ internal sealed record LearningServiceDependencies(
IQuestionBankPersistence QuestionBankPersistence,
IContentAssetPersistence ContentAssetPersistence,
IIdentityPersistence IdentityPersistence,
- IQuestionReferenceService QuestionReferenceService,
- IPublicQuestionAccessPolicy PublicQuestionAccessPolicy,
ILearningAccessService LearningAccessService,
IEffectiveLearningAccessService EffectiveLearningAccessService,
ILearningStrongRevocationService StrongRevocationService,
@@ -31,8 +29,6 @@ internal abstract partial class LearningActivityServiceBase(LearningServiceDepen
protected IContentAssetPersistence contentAssetPersistence { get; } = dependencies.ContentAssetPersistence;
protected IIdentityPersistence identityPersistence { get; } = dependencies.IdentityPersistence;
protected IModulePersistence unitOfWork { get; } = dependencies.LearningPersistence;
- protected IQuestionReferenceService questionReferenceService { get; } = dependencies.QuestionReferenceService;
- protected IPublicQuestionAccessPolicy publicQuestionAccessPolicy { get; } = dependencies.PublicQuestionAccessPolicy;
protected ILearningAccessService learningAccessService { get; } = dependencies.LearningAccessService;
protected IEffectiveLearningAccessService effectiveLearningAccessService { get; } =
dependencies.EffectiveLearningAccessService;
diff --git a/Tiku.Infrastructure/Learning/Outbox/LearningOutboxProcessor.cs b/Tiku.Infrastructure/Learning/Outbox/LearningOutboxProcessor.cs
index c3524fa..ec08504 100644
--- a/Tiku.Infrastructure/Learning/Outbox/LearningOutboxProcessor.cs
+++ b/Tiku.Infrastructure/Learning/Outbox/LearningOutboxProcessor.cs
@@ -176,59 +176,28 @@ internal sealed class LearningOutboxProcessor(
var questions = await learningPersistence.PracticeSessionQuestions.AsNoTracking()
.Where(item => item.TenantId == message.TenantId &&
item.PracticeSessionId == payload.PracticeSessionId &&
- ((item.QuestionReferenceId.HasValue &&
- wrongQuestionIds.Contains(item.QuestionReferenceId.Value)) ||
- (item.QuestionAssetId.HasValue &&
- wrongQuestionIds.Contains(item.QuestionAssetId.Value))))
+ wrongQuestionIds.Contains(item.Id))
.Select(item => new
{
- item.QuestionReferenceId,
item.QuestionOwnerTenantId,
- item.QuestionId,
item.QuestionAssetId,
item.QuestionRevisionId,
item.QuestionPlacementId
})
.ToArrayAsync(cancellationToken);
- var projectedKeys = questions.Select(item => item.QuestionReferenceId ?? item.QuestionAssetId)
- .Where(item => item.HasValue).Select(item => item!.Value).Distinct().ToArray();
- if (projectedKeys.Length != wrongQuestionIds.Distinct().Count())
- throw new InvalidOperationException("One or more wrong-question references could not be projected.");
+ if (questions.Length != wrongQuestionIds.Distinct().Count())
+ throw new InvalidOperationException("One or more V2 wrong-question deliveries could not be projected.");
- var v1 = questions.Where(item => item.QuestionReferenceId.HasValue && item.QuestionId.HasValue)
- .DistinctBy(item => item.QuestionReferenceId).ToArray();
- if (v1.Length > 0)
- {
- var referenceIds = v1.Select(item => item.QuestionReferenceId!.Value).ToArray();
- var ownerTenantIds = v1.Select(item => item.QuestionOwnerTenantId).ToArray();
- var questionIds = v1.Select(item => item.QuestionId!.Value).ToArray();
- await learningPersistence.Database.ExecuteSqlInterpolatedAsync($"""
- INSERT INTO wrong_questions
- (tenant_id, user_id, question_reference_id, question_owner_tenant_id, question_id,
- wrong_count, last_wrong_at, resolved_at)
- SELECT {message.TenantId}, {payload.UserId}, source.question_reference_id,
- source.question_owner_tenant_id, source.question_id, 1, {report.SubmittedAt}, NULL
- FROM unnest({referenceIds}, {ownerTenantIds}, {questionIds})
- AS source(question_reference_id, question_owner_tenant_id, question_id)
- ON CONFLICT (tenant_id, user_id, question_reference_id) DO UPDATE SET
- wrong_count = wrong_questions.wrong_count + 1,
- last_wrong_at = GREATEST(wrong_questions.last_wrong_at, EXCLUDED.last_wrong_at),
- resolved_at = NULL
- """, cancellationToken);
- }
-
- var v2 = questions.Where(item => item.QuestionAssetId.HasValue &&
- item.QuestionRevisionId.HasValue &&
- item.QuestionPlacementId.HasValue)
+ var v2 = questions
.DistinctBy(item => new { item.QuestionOwnerTenantId, item.QuestionAssetId }).ToArray();
if (v2.Length > 0)
{
var ownerTenantIds = v2.Select(item => item.QuestionOwnerTenantId).ToArray();
- var assetIds = v2.Select(item => item.QuestionAssetId!.Value).ToArray();
- var revisionIds = v2.Select(item => item.QuestionRevisionId!.Value).ToArray();
- var placementIds = v2.Select(item => item.QuestionPlacementId!.Value).ToArray();
+ var assetIds = v2.Select(item => item.QuestionAssetId).ToArray();
+ var revisionIds = v2.Select(item => item.QuestionRevisionId).ToArray();
+ var placementIds = v2.Select(item => item.QuestionPlacementId).ToArray();
await learningPersistence.Database.ExecuteSqlInterpolatedAsync($"""
- INSERT INTO wrong_questions_v2
+ INSERT INTO wrong_questions
(tenant_id, user_id, question_asset_owner_tenant_id, question_asset_id,
last_question_revision_id, last_question_placement_id, wrong_count, last_wrong_at, resolved_at)
SELECT {message.TenantId}, {payload.UserId}, source.owner_tenant_id, source.asset_id,
@@ -238,8 +207,8 @@ internal sealed class LearningOutboxProcessor(
ON CONFLICT (tenant_id, user_id, question_asset_owner_tenant_id, question_asset_id) DO UPDATE SET
last_question_revision_id = EXCLUDED.last_question_revision_id,
last_question_placement_id = EXCLUDED.last_question_placement_id,
- wrong_count = wrong_questions_v2.wrong_count + 1,
- last_wrong_at = GREATEST(wrong_questions_v2.last_wrong_at, EXCLUDED.last_wrong_at),
+ wrong_count = wrong_questions.wrong_count + 1,
+ last_wrong_at = GREATEST(wrong_questions.last_wrong_at, EXCLUDED.last_wrong_at),
resolved_at = NULL
""", cancellationToken);
}
diff --git a/Tiku.Infrastructure/Learning/PracticeSessions/PracticeSessionService.cs b/Tiku.Infrastructure/Learning/PracticeSessions/PracticeSessionService.cs
index 78048b9..a4f2a3a 100644
--- a/Tiku.Infrastructure/Learning/PracticeSessions/PracticeSessionService.cs
+++ b/Tiku.Infrastructure/Learning/PracticeSessions/PracticeSessionService.cs
@@ -71,11 +71,6 @@ internal sealed class PracticeSessionService(
item.PracticeSessionId == sessionId &&
revealQuestionIds.Contains(item.Id))
.ToArrayAsync(cancellationToken);
- if (sessionQuestions.Any(item => !item.QuestionRevisionId.HasValue))
- throw new LearningValidationException(
- "practice_session_delivery_unsupported",
- "The practice session uses a retired delivery model.");
-
return await v2PracticeSessionService.LoadSolutionsAsync(sessionQuestions, cancellationToken);
}
diff --git a/Tiku.Infrastructure/Learning/PracticeSessions/V2PracticeSessionService.cs b/Tiku.Infrastructure/Learning/PracticeSessions/V2PracticeSessionService.cs
index 95e3014..0e4ccc8 100644
--- a/Tiku.Infrastructure/Learning/PracticeSessions/V2PracticeSessionService.cs
+++ b/Tiku.Infrastructure/Learning/PracticeSessions/V2PracticeSessionService.cs
@@ -571,7 +571,7 @@ internal sealed class V2PracticeSessionService(LearningServiceDependencies depen
var revisions = new List();
foreach (var ownerGroup in sessionQuestions.GroupBy(item => item.QuestionOwnerTenantId))
{
- var revisionIds = ownerGroup.Select(item => item.QuestionRevisionId!.Value).Distinct().ToArray();
+ var revisionIds = ownerGroup.Select(item => item.QuestionRevisionId).Distinct().ToArray();
revisions.AddRange(await ReadForContentOwnerAsync(
ownerGroup.Key,
"Reveal explicitly locked V2 question solutions",
diff --git a/Tiku.Infrastructure/Learning/QuestionReview/QuestionReviewService.cs b/Tiku.Infrastructure/Learning/QuestionReview/QuestionReviewService.cs
index ccc11b6..ca1c117 100644
--- a/Tiku.Infrastructure/Learning/QuestionReview/QuestionReviewService.cs
+++ b/Tiku.Infrastructure/Learning/QuestionReview/QuestionReviewService.cs
@@ -1,8 +1,6 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Learning;
-using Tiku.Application.QuestionBanks;
-using Tiku.Domain.Content;
using Tiku.Domain.Learning;
namespace Tiku.Infrastructure.Learning;
@@ -23,10 +21,10 @@ internal sealed class QuestionReviewService(LearningServiceDependencies dependen
.OrderByDescending(item => item.CreatedAt)
.Take(ResolveLimit(filter.Limit))
.Select(item => new FavoriteQuestionItem(
- item.QuestionReferenceId,
- new QuestionLocator(
- item.QuestionOwnerTenantId == item.TenantId ? QuestionSource.Tenant : QuestionSource.Platform,
- item.QuestionId),
+ item.QuestionAssetOwnerTenantId,
+ item.QuestionAssetId,
+ item.LastQuestionRevisionId,
+ item.LastQuestionPlacementId,
item.CreatedAt))
.ToArrayAsync(cancellationToken);
@@ -38,15 +36,29 @@ internal sealed class QuestionReviewService(LearningServiceDependencies dependen
QuestionActionCommand command,
CancellationToken cancellationToken = default)
{
- var reference = await questionReferenceService.ResolveAsync(
- actor.TenantId,
- actor.UserId,
- command.Locator,
- cancellationToken);
+ var delivery = await (
+ from sessionQuestion in learningPersistence.PracticeSessionQuestions.AsNoTracking()
+ join session in learningPersistence.PracticeSessions.AsNoTracking()
+ on new { sessionQuestion.TenantId, Id = sessionQuestion.PracticeSessionId }
+ equals new { session.TenantId, session.Id }
+ where sessionQuestion.TenantId == actor.TenantId &&
+ session.UserId == actor.UserId &&
+ sessionQuestion.Id == command.SessionQuestionId
+ select new
+ {
+ QuestionAssetOwnerTenantId = sessionQuestion.QuestionOwnerTenantId,
+ sessionQuestion.QuestionAssetId,
+ sessionQuestion.QuestionRevisionId,
+ sessionQuestion.QuestionPlacementId
+ })
+ .SingleOrDefaultAsync(cancellationToken);
+ if (delivery is null)
+ throw new LearningResourceNotFoundException(
+ "practice_question_not_found", "The delivered practice question was not found.");
var favorite = command.Favorite ?? true;
var item = await learningPersistence.FavoriteQuestions.FindAsync(
- [actor.TenantId, actor.UserId, reference.Id],
+ [actor.TenantId, actor.UserId, delivery.QuestionAssetOwnerTenantId, delivery.QuestionAssetId],
cancellationToken);
if (favorite)
@@ -56,12 +68,17 @@ internal sealed class QuestionReviewService(LearningServiceDependencies dependen
{
TenantId = actor.TenantId,
UserId = actor.UserId,
- QuestionReferenceId = reference.Id,
- QuestionOwnerTenantId = reference.QuestionOwnerTenantId,
- QuestionId = reference.QuestionId,
- Source = reference.Source.ToString().ToLowerInvariant(),
+ QuestionAssetOwnerTenantId = delivery.QuestionAssetOwnerTenantId,
+ QuestionAssetId = delivery.QuestionAssetId,
+ LastQuestionRevisionId = delivery.QuestionRevisionId,
+ LastQuestionPlacementId = delivery.QuestionPlacementId,
CreatedAt = DateTimeOffset.UtcNow
});
+ else
+ {
+ item.LastQuestionRevisionId = delivery.QuestionRevisionId;
+ item.LastQuestionPlacementId = delivery.QuestionPlacementId;
+ }
}
else if (item is not null)
{
@@ -90,10 +107,10 @@ internal sealed class QuestionReviewService(LearningServiceDependencies dependen
.OrderByDescending(item => item.LastWrongAt)
.Take(ResolveLimit(filter.Limit))
.Select(item => new WrongQuestionItem(
- item.QuestionReferenceId,
- new QuestionLocator(
- item.QuestionOwnerTenantId == item.TenantId ? QuestionSource.Tenant : QuestionSource.Platform,
- item.QuestionId),
+ item.QuestionAssetOwnerTenantId,
+ item.QuestionAssetId,
+ item.LastQuestionRevisionId,
+ item.LastQuestionPlacementId,
item.WrongCount,
item.LastWrongAt,
item.ResolvedAt))
@@ -104,16 +121,11 @@ internal sealed class QuestionReviewService(LearningServiceDependencies dependen
public async Task ResolveWrongQuestionAsync(
LearningActor actor,
- QuestionActionCommand command,
+ ResolveWrongQuestionCommand command,
CancellationToken cancellationToken = default)
{
- var reference = await questionReferenceService.ResolveAsync(
- actor.TenantId,
- actor.UserId,
- command.Locator,
- cancellationToken);
var item = await learningPersistence.WrongQuestions.FindAsync(
- [actor.TenantId, actor.UserId, reference.Id],
+ [actor.TenantId, actor.UserId, command.QuestionAssetOwnerTenantId, command.QuestionAssetId],
cancellationToken);
if (item is null)
@@ -138,10 +150,10 @@ internal sealed class QuestionReviewService(LearningServiceDependencies dependen
.ThenBy(item => item.LastWrongAt)
.Take(ResolveLimit(filter.Limit))
.Select(item => new WrongQuestionReviewPlanItem(
- item.QuestionReferenceId,
- new QuestionLocator(
- item.QuestionOwnerTenantId == item.TenantId ? QuestionSource.Tenant : QuestionSource.Platform,
- item.QuestionId),
+ item.QuestionAssetOwnerTenantId,
+ item.QuestionAssetId,
+ item.LastQuestionRevisionId,
+ item.LastQuestionPlacementId,
item.WrongCount,
item.LastWrongAt))
.ToArrayAsync(cancellationToken);
diff --git a/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs b/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs
index e9fe543..e819a27 100644
--- a/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs
+++ b/Tiku.Infrastructure/Persistence/Configurations/LearningConfigurations.cs
@@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Tiku.Domain.Content;
using Tiku.Domain.Identity;
using Tiku.Domain.Learning;
-using Tiku.Domain.QuestionBanks;
using Tiku.Domain.Tenancy;
namespace Tiku.Infrastructure.Persistence.Configurations;
@@ -70,7 +69,22 @@ internal sealed class PracticeSessionQuestionConfiguration : IEntityTypeConfigur
entity.TenantId,
entity.PracticeSessionId,
entity.ContentReleaseQuestionId
- }).IsUnique().HasFilter("content_release_question_id is not null");
+ }).IsUnique();
+ builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.ContentReleaseQuestionId })
+ .HasDatabaseName("ix_psq_release_question");
+ builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.QuestionAssetId })
+ .HasDatabaseName("ix_psq_question_asset");
+ builder.HasIndex(entity => new
+ {
+ entity.QuestionOwnerTenantId,
+ entity.QuestionAssetId,
+ entity.QuestionRevisionId
+ })
+ .HasDatabaseName("ix_psq_question_revision");
+ builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.QuestionPlacementId })
+ .HasDatabaseName("ix_psq_question_placement");
+ builder.HasIndex(entity => new { entity.QuestionOwnerTenantId, entity.AssessmentPolicyVersionId })
+ .HasDatabaseName("ix_psq_assessment_policy");
builder.Property(entity => entity.Score).HasPrecision(8, 2);
builder.Property(entity => entity.QuestionType).HasMaxLength(50).HasDefaultValue("choice");
builder.Property(entity => entity.TypeLabelSnapshot).HasMaxLength(100);
@@ -82,28 +96,46 @@ internal sealed class PracticeSessionQuestionConfiguration : IEntityTypeConfigur
.HasForeignKey(entity => new { entity.TenantId, entity.PracticeSessionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
- builder.HasOne().WithMany()
- .HasForeignKey(entity => new
- {
- entity.TenantId,
- entity.QuestionOwnerTenantId,
- entity.QuestionId
- })
- .HasPrincipalKey(entity => new
- {
- entity.TenantId,
- entity.QuestionOwnerTenantId,
- entity.QuestionId
- })
- .OnDelete(DeleteBehavior.Restrict);
- builder.HasOne().WithMany()
+ builder.HasOne().WithMany()
.HasForeignKey(entity => new
{
TenantId = entity.QuestionOwnerTenantId,
- entity.QuestionId,
- Id = entity.QuestionVersionId
+ Id = entity.ContentReleaseQuestionId
})
- .HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionId, entity.Id })
+ .HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
+ .OnDelete(DeleteBehavior.Restrict);
+ builder.HasOne().WithMany()
+ .HasForeignKey(entity => new
+ {
+ TenantId = entity.QuestionOwnerTenantId,
+ Id = entity.QuestionAssetId
+ })
+ .HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
+ .OnDelete(DeleteBehavior.Restrict);
+ builder.HasOne().WithMany()
+ .HasForeignKey(entity => new
+ {
+ TenantId = entity.QuestionOwnerTenantId,
+ entity.QuestionAssetId,
+ Id = entity.QuestionRevisionId
+ })
+ .HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id })
+ .OnDelete(DeleteBehavior.Restrict);
+ builder.HasOne().WithMany()
+ .HasForeignKey(entity => new
+ {
+ TenantId = entity.QuestionOwnerTenantId,
+ Id = entity.QuestionPlacementId
+ })
+ .HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
+ .OnDelete(DeleteBehavior.Restrict);
+ builder.HasOne().WithMany()
+ .HasForeignKey(entity => new
+ {
+ TenantId = entity.QuestionOwnerTenantId,
+ Id = entity.AssessmentPolicyVersionId
+ })
+ .HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -263,9 +295,15 @@ internal sealed class FavoriteQuestionConfiguration : IEntityTypeConfiguration builder)
{
builder.ToTable("favorite_questions");
- builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionReferenceId });
- builder.Property(entity => entity.Source).HasMaxLength(50);
+ builder.HasKey(entity => new
+ {
+ entity.TenantId,
+ entity.UserId,
+ entity.QuestionAssetOwnerTenantId,
+ entity.QuestionAssetId
+ });
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
+ builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.CreatedAt });
builder.HasOne().WithMany()
.HasForeignKey(entity => entity.TenantId)
@@ -273,12 +311,34 @@ internal sealed class FavoriteQuestionConfiguration : IEntityTypeConfiguration().WithMany()
.HasForeignKey(entity => entity.UserId)
.OnDelete(DeleteBehavior.Cascade);
- builder.HasOne().WithMany()
- .HasForeignKey(entity => new { entity.TenantId, entity.QuestionReferenceId })
+ ConfigureQuestionAssetEvidence(builder);
+ }
+
+ private static void ConfigureQuestionAssetEvidence(EntityTypeBuilder builder)
+ {
+ builder.HasOne().WithMany()
+ .HasForeignKey(entity => new
+ {
+ TenantId = entity.QuestionAssetOwnerTenantId,
+ Id = entity.QuestionAssetId
+ })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
- .OnDelete(DeleteBehavior.Cascade);
- builder.HasOne().WithMany()
- .HasForeignKey(entity => new { entity.QuestionOwnerTenantId, entity.QuestionId })
+ .OnDelete(DeleteBehavior.Restrict);
+ builder.HasOne().WithMany()
+ .HasForeignKey(entity => new
+ {
+ TenantId = entity.QuestionAssetOwnerTenantId,
+ entity.QuestionAssetId,
+ Id = entity.LastQuestionRevisionId
+ })
+ .HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id })
+ .OnDelete(DeleteBehavior.Restrict);
+ builder.HasOne().WithMany()
+ .HasForeignKey(entity => new
+ {
+ TenantId = entity.QuestionAssetOwnerTenantId,
+ Id = entity.LastQuestionPlacementId
+ })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
}
@@ -289,32 +349,6 @@ internal sealed class WrongQuestionConfiguration : IEntityTypeConfiguration builder)
{
builder.ToTable("wrong_questions");
- builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionReferenceId });
- builder.Property(entity => entity.WrongCount).HasDefaultValue(1);
- builder.Property(entity => entity.LastWrongAt).HasDefaultValueSql("now()");
-
- builder.HasOne().WithMany()
- .HasForeignKey(entity => entity.TenantId)
- .OnDelete(DeleteBehavior.Cascade);
- builder.HasOne().WithMany()
- .HasForeignKey(entity => entity.UserId)
- .OnDelete(DeleteBehavior.Cascade);
- builder.HasOne().WithMany()
- .HasForeignKey(entity => new { entity.TenantId, entity.QuestionReferenceId })
- .HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
- .OnDelete(DeleteBehavior.Cascade);
- builder.HasOne().WithMany()
- .HasForeignKey(entity => new { entity.QuestionOwnerTenantId, entity.QuestionId })
- .HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
- .OnDelete(DeleteBehavior.Restrict);
- }
-}
-
-internal sealed class WrongQuestionV2Configuration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder builder)
- {
- builder.ToTable("wrong_questions_v2");
builder.HasKey(entity => new
{
entity.TenantId,
@@ -325,9 +359,12 @@ internal sealed class WrongQuestionV2Configuration : IEntityTypeConfiguration entity.WrongCount).HasDefaultValue(1);
builder.Property(entity => entity.LastWrongAt).HasDefaultValueSql("now()");
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.LastWrongAt });
- builder.HasOne().WithMany().HasForeignKey(entity => entity.TenantId)
+
+ builder.HasOne().WithMany()
+ .HasForeignKey(entity => entity.TenantId)
.OnDelete(DeleteBehavior.Cascade);
- builder.HasOne().WithMany().HasForeignKey(entity => entity.UserId)
+ builder.HasOne().WithMany()
+ .HasForeignKey(entity => entity.UserId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne().WithMany()
.HasForeignKey(entity => new
@@ -346,6 +383,14 @@ internal sealed class WrongQuestionV2Configuration : IEntityTypeConfiguration new { entity.TenantId, entity.QuestionAssetId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
+ builder.HasOne().WithMany()
+ .HasForeignKey(entity => new
+ {
+ TenantId = entity.QuestionAssetOwnerTenantId,
+ Id = entity.LastQuestionPlacementId
+ })
+ .HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
+ .OnDelete(DeleteBehavior.Restrict);
}
}
diff --git a/Tiku.Infrastructure/Persistence/Migrations/20260806015826_MigrateQuestionReviewV2.Designer.cs b/Tiku.Infrastructure/Persistence/Migrations/20260806015826_MigrateQuestionReviewV2.Designer.cs
new file mode 100644
index 0000000..f2ff9f7
--- /dev/null
+++ b/Tiku.Infrastructure/Persistence/Migrations/20260806015826_MigrateQuestionReviewV2.Designer.cs
@@ -0,0 +1,26515 @@
+//
+using System;
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using Tiku.Infrastructure.Persistence;
+
+#nullable disable
+
+namespace Tiku.Infrastructure.Persistence.Migrations
+{
+ [DbContext(typeof(TikuDbContext))]
+ [Migration("20260806015826_MigrateQuestionReviewV2")]
+ partial class MigrateQuestionReviewV2
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext");
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "ltree");
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm");
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text")
+ .HasColumnName("claim_type");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text")
+ .HasColumnName("claim_value");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_user_claims");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_user_claims_user_id");
+
+ b.ToTable("user_claims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("text")
+ .HasColumnName("login_provider");
+
+ b.Property("ProviderKey")
+ .HasColumnType("text")
+ .HasColumnName("provider_key");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text")
+ .HasColumnName("provider_display_name");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("LoginProvider", "ProviderKey")
+ .HasName("pk_user_logins");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_user_logins_user_id");
+
+ b.ToTable("user_logins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("LoginProvider")
+ .HasColumnType("text")
+ .HasColumnName("login_provider");
+
+ b.Property("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("UserId", "LoginProvider", "Name")
+ .HasName("pk_user_tokens");
+
+ b.ToTable("user_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Catalog.Category", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("CategoryType")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("category_type");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean")
+ .HasColumnName("is_active");
+
+ b.Property("LegacyId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)")
+ .HasColumnName("name");
+
+ b.Property("NodeId")
+ .HasColumnType("uuid")
+ .HasColumnName("node_id");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer")
+ .HasColumnName("sort_order");
+
+ b.Property("SubjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("subject_id");
+
+ b.Property("SvipQuestionLimit")
+ .HasColumnType("integer")
+ .HasColumnName("svip_question_limit");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id")
+ .HasName("pk_categories");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_categories_tenant_id_id");
+
+ b.HasIndex("TenantId", "LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_categories_tenant_id_legacy_id");
+
+ b.HasIndex("TenantId", "NodeId")
+ .HasDatabaseName("ix_categories_tenant_id_node_id");
+
+ b.HasIndex("TenantId", "SubjectId")
+ .HasDatabaseName("ix_categories_tenant_id_subject_id");
+
+ b.ToTable("categories", (string)null);
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Catalog.Major", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean")
+ .HasColumnName("is_active");
+
+ b.Property("LegacyId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)")
+ .HasColumnName("name");
+
+ b.Property("RegionId")
+ .HasColumnType("uuid")
+ .HasColumnName("region_id");
+
+ b.Property("SchoolId")
+ .HasColumnType("uuid")
+ .HasColumnName("school_id");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer")
+ .HasColumnName("sort_order");
+
+ b.Property("StudyTips")
+ .HasColumnType("text")
+ .HasColumnName("study_tips");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id")
+ .HasName("pk_majors");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_majors_tenant_id_id");
+
+ b.HasIndex("TenantId", "LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_majors_tenant_id_legacy_id");
+
+ b.HasIndex("TenantId", "RegionId")
+ .HasDatabaseName("ix_majors_tenant_id_region_id");
+
+ b.HasIndex("TenantId", "SchoolId")
+ .HasDatabaseName("ix_majors_tenant_id_school_id");
+
+ b.ToTable("majors", (string)null);
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Catalog.ModuleNode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean")
+ .HasColumnName("is_active");
+
+ b.Property("LegacyId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_id");
+
+ b.Property("LegacyModuleId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_module_id");
+
+ b.Property("LegacyParentId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_parent_id");
+
+ b.Property("Metadata")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("jsonb")
+ .HasColumnName("metadata")
+ .HasDefaultValueSql("'{}'::jsonb");
+
+ b.Property("ModuleId")
+ .HasColumnType("uuid")
+ .HasColumnName("module_id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)")
+ .HasColumnName("name");
+
+ b.Property("ParentId")
+ .HasColumnType("uuid")
+ .HasColumnName("parent_id");
+
+ b.Property("Path")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)")
+ .HasColumnName("path");
+
+ b.Property("RegionId")
+ .HasColumnType("uuid")
+ .HasColumnName("region_id");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer")
+ .HasColumnName("sort_order");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("type");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id")
+ .HasName("pk_module_nodes");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_module_nodes_tenant_id_id");
+
+ b.HasIndex("TenantId", "LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_module_nodes_tenant_id_legacy_id");
+
+ b.HasIndex("TenantId", "ModuleId")
+ .HasDatabaseName("ix_module_nodes_tenant_id_module_id");
+
+ b.HasIndex("TenantId", "ParentId")
+ .HasDatabaseName("ix_module_nodes_tenant_id_parent_id");
+
+ b.HasIndex("TenantId", "RegionId")
+ .HasDatabaseName("ix_module_nodes_tenant_id_region_id");
+
+ b.ToTable("module_nodes", (string)null);
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Catalog.Region", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("Code")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("code");
+
+ b.Property("Config")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("jsonb")
+ .HasColumnName("config")
+ .HasDefaultValueSql("'{}'::jsonb");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("FullName")
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)")
+ .HasColumnName("full_name");
+
+ b.Property("Icon")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)")
+ .HasColumnName("icon");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean")
+ .HasColumnName("is_active");
+
+ b.Property("IsHot")
+ .HasColumnType("boolean")
+ .HasColumnName("is_hot");
+
+ b.Property("LegacyId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("name");
+
+ b.Property("Pinyin")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)")
+ .HasColumnName("pinyin");
+
+ b.Property("ShortName")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("short_name");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer")
+ .HasColumnName("sort_order");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id")
+ .HasName("pk_regions");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_regions_tenant_id_id");
+
+ b.HasIndex("TenantId", "LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_regions_tenant_id_legacy_id");
+
+ b.ToTable("regions", (string)null);
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Catalog.RegionModule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("Color")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("color");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property("Icon")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)")
+ .HasColumnName("icon");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean")
+ .HasColumnName("is_active");
+
+ b.Property("IsPrimarySchoolModule")
+ .HasColumnType("boolean")
+ .HasColumnName("is_primary_school_module");
+
+ b.Property("LegacyId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("name");
+
+ b.Property("RegionId")
+ .HasColumnType("uuid")
+ .HasColumnName("region_id");
+
+ b.Property("Route")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)")
+ .HasColumnName("route");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer")
+ .HasColumnName("sort_order");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("TextColor")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("text_color");
+
+ b.Property("Type")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("type");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id")
+ .HasName("pk_region_modules");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_region_modules_tenant_id_id");
+
+ b.HasIndex("TenantId", "LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_region_modules_tenant_id_legacy_id");
+
+ b.HasIndex("TenantId", "RegionId")
+ .HasDatabaseName("ix_region_modules_tenant_id_region_id");
+
+ b.ToTable("region_modules", (string)null);
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Catalog.School", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("LegacyId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_id");
+
+ b.Property("Metadata")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("jsonb")
+ .HasColumnName("metadata")
+ .HasDefaultValueSql("'{}'::jsonb");
+
+ b.Property("ModuleId")
+ .HasColumnType("uuid")
+ .HasColumnName("module_id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)")
+ .HasColumnName("name");
+
+ b.Property("ProfessionalExamDate")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)")
+ .HasColumnName("professional_exam_date");
+
+ b.Property("RegionId")
+ .HasColumnType("uuid")
+ .HasColumnName("region_id");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id")
+ .HasName("pk_schools");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_schools_tenant_id_id");
+
+ b.HasIndex("TenantId", "LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_schools_tenant_id_legacy_id");
+
+ b.HasIndex("TenantId", "ModuleId")
+ .HasDatabaseName("ix_schools_tenant_id_module_id");
+
+ b.HasIndex("TenantId", "RegionId")
+ .HasDatabaseName("ix_schools_tenant_id_region_id");
+
+ b.ToTable("schools", (string)null);
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Catalog.ScorelineField", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)")
+ .HasColumnName("description");
+
+ b.Property("FieldKey")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("field_key");
+
+ b.Property("FieldName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("field_name");
+
+ b.Property("FieldType")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("field_type");
+
+ b.Property("IsFilter")
+ .HasColumnType("boolean")
+ .HasColumnName("is_filter");
+
+ b.Property("IsRequired")
+ .HasColumnType("boolean")
+ .HasColumnName("is_required");
+
+ b.Property("IsTrend")
+ .HasColumnType("boolean")
+ .HasColumnName("is_trend");
+
+ b.Property("IsVisible")
+ .HasColumnType("boolean")
+ .HasColumnName("is_visible");
+
+ b.Property("LegacyId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_id");
+
+ b.Property("Options")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("jsonb")
+ .HasColumnName("options")
+ .HasDefaultValueSql("'[]'::jsonb");
+
+ b.Property("Placeholder")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("placeholder");
+
+ b.Property("RegionId")
+ .HasColumnType("uuid")
+ .HasColumnName("region_id");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer")
+ .HasColumnName("sort_order");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("Unit")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("unit");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id")
+ .HasName("pk_scoreline_fields");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_scoreline_fields_tenant_id_id");
+
+ b.HasIndex("TenantId", "LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_scoreline_fields_tenant_id_legacy_id");
+
+ b.HasIndex("TenantId", "RegionId", "FieldKey")
+ .IsUnique()
+ .HasDatabaseName("ix_scoreline_fields_tenant_id_region_id_field_key");
+
+ b.HasIndex("TenantId", "RegionId", "IsFilter", "SortOrder")
+ .HasDatabaseName("ix_scoreline_fields_tenant_id_region_id_is_filter_sort_order");
+
+ b.ToTable("scoreline_fields", (string)null);
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Catalog.ScorelineRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("FieldValues")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("jsonb")
+ .HasColumnName("field_values")
+ .HasDefaultValueSql("'{}'::jsonb");
+
+ b.Property("LegacyId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_id");
+
+ b.Property("MajorId")
+ .HasColumnType("uuid")
+ .HasColumnName("major_id");
+
+ b.Property("MajorName")
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)")
+ .HasColumnName("major_name");
+
+ b.Property("RegionId")
+ .HasColumnType("uuid")
+ .HasColumnName("region_id");
+
+ b.Property("SchoolId")
+ .HasColumnType("uuid")
+ .HasColumnName("school_id");
+
+ b.Property("SchoolName")
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)")
+ .HasColumnName("school_name");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("Year")
+ .HasColumnType("integer")
+ .HasColumnName("year");
+
+ b.HasKey("Id")
+ .HasName("pk_scoreline_records");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_scoreline_records_tenant_id_id");
+
+ b.HasIndex("TenantId", "LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_scoreline_records_tenant_id_legacy_id");
+
+ b.HasIndex("TenantId", "MajorId")
+ .HasDatabaseName("ix_scoreline_records_tenant_id_major_id");
+
+ b.HasIndex("TenantId", "SchoolId")
+ .HasDatabaseName("ix_scoreline_records_tenant_id_school_id");
+
+ b.HasIndex("TenantId", "Year")
+ .HasDatabaseName("ix_scoreline_records_tenant_id_year");
+
+ b.HasIndex("TenantId", "RegionId", "SchoolId", "MajorId", "Year")
+ .HasDatabaseName("ix_scoreline_records_tenant_id_region_id_school_id_major_id_ye~");
+
+ b.HasIndex("TenantId", "Year", "SchoolName", "MajorName", "Id")
+ .IsDescending(false, true, false, false, false)
+ .HasDatabaseName("ix_scoreline_records_tenant_id_year_school_name_major_name_id");
+
+ b.ToTable("scoreline_records", (string)null);
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Catalog.Subject", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property("Icon")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)")
+ .HasColumnName("icon");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean")
+ .HasColumnName("is_active");
+
+ b.Property("LegacyId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_id");
+
+ b.Property("MajorId")
+ .HasColumnType("uuid")
+ .HasColumnName("major_id");
+
+ b.Property("MajorLegacyIds")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("jsonb")
+ .HasColumnName("major_legacy_ids")
+ .HasDefaultValueSql("'[]'::jsonb");
+
+ b.Property("ModuleId")
+ .HasColumnType("uuid")
+ .HasColumnName("module_id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)")
+ .HasColumnName("name");
+
+ b.Property("NodeId")
+ .HasColumnType("uuid")
+ .HasColumnName("node_id");
+
+ b.Property("RegionId")
+ .HasColumnType("uuid")
+ .HasColumnName("region_id");
+
+ b.Property("SchoolId")
+ .HasColumnType("uuid")
+ .HasColumnName("school_id");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer")
+ .HasColumnName("sort_order");
+
+ b.Property("Stats")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("jsonb")
+ .HasColumnName("stats")
+ .HasDefaultValueSql("'{}'::jsonb");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("Type")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("type");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id")
+ .HasName("pk_subjects");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_subjects_tenant_id_id");
+
+ b.HasIndex("TenantId", "LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_subjects_tenant_id_legacy_id");
+
+ b.HasIndex("TenantId", "MajorId")
+ .HasDatabaseName("ix_subjects_tenant_id_major_id");
+
+ b.HasIndex("TenantId", "ModuleId")
+ .HasDatabaseName("ix_subjects_tenant_id_module_id");
+
+ b.HasIndex("TenantId", "NodeId")
+ .HasDatabaseName("ix_subjects_tenant_id_node_id");
+
+ b.HasIndex("TenantId", "RegionId")
+ .HasDatabaseName("ix_subjects_tenant_id_region_id");
+
+ b.HasIndex("TenantId", "SchoolId")
+ .HasDatabaseName("ix_subjects_tenant_id_school_id");
+
+ b.ToTable("subjects", (string)null);
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Commerce.ActivationCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("AgentUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("agent_user_id");
+
+ b.Property("BatchId")
+ .HasColumnType("uuid")
+ .HasColumnName("batch_id");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("citext")
+ .HasColumnName("code");
+
+ b.Property("CouponCode")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("coupon_code");
+
+ b.Property("CouponRedemptionId")
+ .HasColumnType("uuid")
+ .HasColumnName("coupon_redemption_id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("Days")
+ .HasColumnType("integer")
+ .HasColumnName("days");
+
+ b.Property("IsUsed")
+ .HasColumnType("boolean")
+ .HasColumnName("is_used");
+
+ b.Property("LegacyId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_id");
+
+ b.Property("Remark")
+ .HasColumnType("text")
+ .HasColumnName("remark");
+
+ b.Property("SaleType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("sale_type");
+
+ b.Property("SoldTo")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("sold_to");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("UnitPriceCents")
+ .HasColumnType("integer")
+ .HasColumnName("unit_price_cents");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("UsedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("used_at");
+
+ b.Property("UsedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("used_by");
+
+ b.Property("UsedRegionId")
+ .HasColumnType("uuid")
+ .HasColumnName("used_region_id");
+
+ b.HasKey("Id")
+ .HasName("pk_activation_codes");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_activation_codes_tenant_id_id");
+
+ b.HasIndex("AgentUserId")
+ .HasDatabaseName("ix_activation_codes_agent_user_id");
+
+ b.HasIndex("UsedBy")
+ .HasDatabaseName("ix_activation_codes_used_by");
+
+ b.HasIndex("TenantId", "BatchId")
+ .HasDatabaseName("ix_activation_codes_tenant_id_batch_id");
+
+ b.HasIndex("TenantId", "Code")
+ .IsUnique()
+ .HasDatabaseName("ix_activation_codes_tenant_id_code");
+
+ b.HasIndex("TenantId", "CouponRedemptionId")
+ .HasDatabaseName("ix_activation_codes_tenant_id_coupon_redemption_id");
+
+ b.HasIndex("TenantId", "LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_activation_codes_tenant_id_legacy_id");
+
+ b.HasIndex("TenantId", "UsedRegionId")
+ .HasDatabaseName("ix_activation_codes_tenant_id_used_region_id");
+
+ b.ToTable("activation_codes", (string)null);
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Commerce.CodeBatch", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("CampaignName")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("campaign_name");
+
+ b.Property("Channel")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("channel");
+
+ b.Property("CommissionRate")
+ .HasPrecision(6, 4)
+ .HasColumnType("numeric(6,4)")
+ .HasColumnName("commission_rate");
+
+ b.Property("CostPriceCents")
+ .HasColumnType("integer")
+ .HasColumnName("cost_price_cents");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property("Days")
+ .HasColumnType("integer")
+ .HasColumnName("days");
+
+ b.Property("DefaultUnitPriceCents")
+ .HasColumnType("integer")
+ .HasColumnName("default_unit_price_cents");
+
+ b.Property("IssuedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("issued_at");
+
+ b.Property("LegacyId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_id");
+
+ b.Property("LegacyRegionId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("legacy_region_id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("name");
+
+ b.Property("RegionId")
+ .HasColumnType("uuid")
+ .HasColumnName("region_id");
+
+ b.Property("Remark")
+ .HasColumnType("text")
+ .HasColumnName("remark");
+
+ b.Property("SaleType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("sale_type");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("TotalCount")
+ .HasColumnType("integer")
+ .HasColumnName("total_count");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id")
+ .HasName("pk_code_batches");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_code_batches_tenant_id_id");
+
+ b.HasIndex("TenantId", "LegacyId")
+ .IsUnique()
+ .HasDatabaseName("ix_code_batches_tenant_id_legacy_id");
+
+ b.ToTable("code_batches", null, t =>
+ {
+ t.HasCheckConstraint("ck_code_batches_amounts", "default_unit_price_cents >= 0 and cost_price_cents >= 0");
+
+ t.HasCheckConstraint("ck_code_batches_counts", "total_count >= 0");
+ });
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Commerce.CommerceAdjustmentVoucher", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("AmountCents")
+ .HasColumnType("integer")
+ .HasColumnName("amount_cents");
+
+ b.Property("BatchId")
+ .HasColumnType("uuid")
+ .HasColumnName("batch_id");
+
+ b.Property("ClosedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("closed_at");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property("Currency")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)")
+ .HasColumnName("currency");
+
+ b.Property("Direction")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("direction");
+
+ b.Property("IssueId")
+ .HasColumnType("uuid")
+ .HasColumnName("issue_id");
+
+ b.Property("ItemId")
+ .HasColumnType("uuid")
+ .HasColumnName("item_id");
+
+ b.Property("Metadata")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("jsonb")
+ .HasColumnName("metadata")
+ .HasDefaultValueSql("'{}'::jsonb");
+
+ b.Property("OrderId")
+ .HasColumnType("uuid")
+ .HasColumnName("order_id");
+
+ b.Property("PaymentId")
+ .HasColumnType("uuid")
+ .HasColumnName("payment_id");
+
+ b.Property("ProofAssetKey")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)")
+ .HasColumnName("proof_asset_key");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)")
+ .HasColumnName("reason");
+
+ b.Property("RefundRequestId")
+ .HasColumnType("uuid")
+ .HasColumnName("refund_request_id");
+
+ b.Property("ReviewedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("reviewed_at");
+
+ b.Property("ReviewedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("reviewed_by");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("status");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("VoucherNo")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("voucher_no");
+
+ b.HasKey("Id")
+ .HasName("pk_commerce_adjustment_vouchers");
+
+ b.HasAlternateKey("TenantId", "Id")
+ .HasName("ak_commerce_adjustment_vouchers_tenant_id_id");
+
+ b.HasIndex("CreatedBy")
+ .HasDatabaseName("ix_commerce_adjustment_vouchers_created_by");
+
+ b.HasIndex("ReviewedBy")
+ .HasDatabaseName("ix_commerce_adjustment_vouchers_reviewed_by");
+
+ b.HasIndex("TenantId", "BatchId")
+ .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_batch_id");
+
+ b.HasIndex("TenantId", "ItemId")
+ .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_item_id");
+
+ b.HasIndex("TenantId", "OrderId")
+ .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_order_id");
+
+ b.HasIndex("TenantId", "PaymentId")
+ .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_payment_id");
+
+ b.HasIndex("TenantId", "RefundRequestId")
+ .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_refund_request_id");
+
+ b.HasIndex("TenantId", "VoucherNo")
+ .IsUnique()
+ .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_voucher_no");
+
+ b.HasIndex("TenantId", "IssueId", "CreatedAt")
+ .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_issue_id_created_at");
+
+ b.HasIndex("TenantId", "Status", "CreatedAt")
+ .HasDatabaseName("ix_commerce_adjustment_vouchers_tenant_id_status_created_at");
+
+ b.ToTable("commerce_adjustment_vouchers", null, t =>
+ {
+ t.HasCheckConstraint("ck_commerce_adjustment_vouchers_amount", "amount_cents > 0");
+ });
+ });
+
+ modelBuilder.Entity("Tiku.Domain.Commerce.CommerceAdjustmentVoucherEvent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("ActorUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("actor_user_id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property