feat: migrate direct tenant content and learning endpoints

This commit is contained in:
xiong
2026-07-26 17:57:03 +08:00
parent 1d7e09b1f0
commit 91a4162908
18 changed files with 4079 additions and 0 deletions

View File

@@ -36,6 +36,37 @@ public sealed record AssetUploadConfirmCommand(
long? FileSizeBytes,
string? ChecksumSha256);
public sealed record UpsertAssetCommand(
Guid? AssetId,
Guid? RegionId,
Guid? SubjectId,
Guid? CategoryId,
Guid? ContentNodeId,
string? LegacyId,
string? AssetKey,
string? Title,
string? Category,
string? Description,
string? FileName,
string? CdnUrl,
bool? IsPublic,
string? AssetType,
string? Visibility,
string? Status,
string? Provider,
string? Bucket,
string? ObjectKey,
string? MimeType,
long? FileSizeBytes,
string? ChecksumSha256,
string? PreviewUrl,
string? PreviewObjectKey,
int? Order,
JsonElement AccessRules,
JsonElement Metadata);
public sealed record AssetAccessSignCommand(Guid AssetId, int? ExpiresInSeconds);
public sealed record AssetManagementFilter(
Guid? RegionId = null,
Guid? SubjectId = null,
@@ -54,6 +85,8 @@ public sealed record ImportJobFilter(
string? SourceFormat = null,
int? Limit = null);
public sealed record AssetEventFilter(Guid? AssetId = null, Guid? UserId = null, int? Limit = null);
public sealed record ContentAssetManagementItem(
Guid Id,
string? LegacyId,
@@ -96,6 +129,39 @@ public sealed record AssetUploadConfirmResult(
ContentAssetManagementItem Item,
ObjectStorageMetadata Metadata);
public sealed record AssetManagementSignedAccessResult(
ContentAssetManagementItem Item,
ObjectStorageSignedUrl Url);
public sealed record ContentAssetAccessEventItem(
Guid Id,
Guid? AssetId,
Guid? UserId,
AssetAccessActorRole ActorRole,
AssetAccessType AccessType,
string? Visibility,
string? AssetType,
string? StorageProvider,
AssetAccessDisposition? Disposition,
int? ExpiresInSeconds,
string? SignatureMode,
AssetAccessResult Result,
string? DenyCode,
string? IpAddress,
string? UserAgent,
JsonElement Metadata,
DateTimeOffset CreatedAt);
public sealed record ContentAssetSecurityScanEventItem(
Guid Id,
Guid? AssetId,
string Provider,
AssetSecurityScanStatus ScanStatus,
AssetSecurityRiskLevel RiskLevel,
string[] IssueCodes,
JsonElement Details,
DateTimeOffset CreatedAt);
public sealed record ContentImportJobItem(
Guid Id,
Guid? TargetRegionId,

View File

@@ -1,4 +1,5 @@
using Tiku.Application.Catalog;
using Tiku.Application.Content;
namespace Tiku.Application.Assets;
@@ -9,6 +10,11 @@ public interface IAssetManagementService
AssetManagementFilter filter,
CancellationToken cancellationToken = default);
Task<ContentManagementResult<ContentAssetManagementItem>> UpsertAssetAsync(
AssetManagementActor actor,
UpsertAssetCommand command,
CancellationToken cancellationToken = default);
Task<AssetUploadSignResult> SignUploadAsync(
AssetManagementActor actor,
AssetUploadSignCommand command,
@@ -19,6 +25,26 @@ public interface IAssetManagementService
AssetUploadConfirmCommand command,
CancellationToken cancellationToken = default);
Task<AssetManagementSignedAccessResult> SignDownloadAsync(
AssetManagementActor actor,
AssetAccessSignCommand command,
CancellationToken cancellationToken = default);
Task<AssetManagementSignedAccessResult> SignPreviewAsync(
AssetManagementActor actor,
AssetAccessSignCommand command,
CancellationToken cancellationToken = default);
Task<CatalogList<ContentAssetAccessEventItem>> GetAccessEventsAsync(
AssetManagementActor actor,
AssetEventFilter filter,
CancellationToken cancellationToken = default);
Task<CatalogList<ContentAssetSecurityScanEventItem>> GetSecurityScanEventsAsync(
AssetManagementActor actor,
AssetEventFilter filter,
CancellationToken cancellationToken = default);
Task<CatalogList<ContentImportJobItem>> GetImportJobsAsync(
AssetManagementActor actor,
ImportJobFilter filter,

View File

@@ -0,0 +1,320 @@
using System.Text.Json;
using Tiku.Application.Assets;
using Tiku.Application.Catalog;
using Tiku.Domain.Catalog;
using Tiku.Domain.Content;
using Tiku.Domain.Learning;
using Tiku.Domain.Operations;
using Tiku.Domain.QuestionBanks;
namespace Tiku.Application.Content;
public sealed record DirectContentActor(Guid TenantId, Guid UserId);
public sealed record AdminLimitFilter(
Guid? RegionId = null,
Guid? EntryId = null,
Guid? ContentNodeId = null,
Guid? ParentId = null,
Guid? SubjectId = null,
Guid? ChapterId = null,
Guid? UnitId = null,
Guid? SchoolId = null,
Guid? MajorId = null,
Guid? QuestionId = null,
string? Status = null,
string? Keyword = null,
int? Year = null,
int? Limit = null);
public sealed record QuestionWriteCommand(
Guid? QuestionId,
Guid? QuestionBankId,
Guid? SubjectId,
Guid? CategoryId,
Guid? NodeId,
Guid? EntryId,
Guid? ContentNodeId,
Guid? PrimaryCollectionId,
string? LegacyId,
string? Type,
string? TypeLabel,
int? Difficulty,
JsonElement Tags,
string? Content,
JsonElement Options,
int? CorrectOptionIndex,
JsonElement CorrectOptionIndices,
string? AnswerText,
string? Explanation,
JsonElement SubQuestions,
string? CodeLang,
string? CodeTemplate,
string? MediaUrl,
string? Status,
JsonElement ExamMarkers,
string? SourceHash,
bool CreateVersion);
public sealed record QuestionManagementItem(
Guid Id,
Guid? VersionId,
Guid? QuestionBankId,
Guid? SubjectId,
Guid? CategoryId,
Guid? NodeId,
Guid? EntryId,
Guid? ContentNodeId,
Guid? PrimaryCollectionId,
string? LegacyId,
string Type,
string? TypeLabel,
int? Difficulty,
JsonElement Tags,
string? Content,
JsonElement Options,
int? CorrectOptionIndex,
JsonElement CorrectOptionIndices,
string? AnswerText,
string? Explanation,
JsonElement SubQuestions,
string? CodeLang,
string? CodeTemplate,
string? MediaUrl,
bool HasVideoExplanation,
QuestionStatus Status);
public sealed record VocabularyUnitCommand(
Guid? Id,
Guid? RegionId,
Guid? EntryId,
Guid? ContentNodeId,
string? LegacyId,
string Name,
string? Description,
int? WordCount,
int? Order,
bool? IsActive,
JsonElement Metadata);
public sealed record VocabularyWordCommand(
Guid? Id,
Guid? UnitId,
Guid? EntryId,
Guid? ContentNodeId,
string? LegacyId,
string Word,
string? Phonetic,
string? Meaning,
string? Example,
string? ExampleTranslation,
int? Difficulty,
JsonElement Tags,
int? Order,
bool? IsActive,
JsonElement Metadata);
public sealed record HandbookSubjectCommand(
Guid? Id,
Guid? RegionId,
Guid? SchoolId,
Guid? MajorId,
Guid? EntryId,
Guid? ContentNodeId,
string? LegacyId,
string Name,
string? Type,
string? Icon,
string? Color,
string? Description,
int? Order,
bool? IsActive,
JsonElement Metadata);
public sealed record HandbookChapterCommand(
Guid? Id,
Guid? SubjectId,
Guid? EntryId,
Guid? ContentNodeId,
string? LegacyId,
string Name,
string? Description,
int? Order,
bool? IsActive,
JsonElement Metadata);
public sealed record HandbookEntryCommand(
Guid? Id,
Guid? ChapterId,
Guid? EntryId,
Guid? ContentNodeId,
string? LegacyId,
string Title,
string? Summary,
string? Content,
JsonElement Tags,
int? Order,
bool? IsActive,
JsonElement Metadata);
public sealed record SchoolCommand(
Guid? Id,
Guid? RegionId,
string? LegacyId,
string Name,
string? ProfessionalExamDate,
JsonElement Metadata);
public sealed record MajorCommand(
Guid? Id,
Guid? RegionId,
Guid? SchoolId,
string? LegacyId,
string Name,
string? Description,
string? StudyTips,
int? Order,
bool? IsActive);
public sealed record VideoExplanationCommand(
Guid? Id,
Guid? SubjectId,
string? LegacyId,
string Title,
string? Description,
string? VideoUrl,
string? ThumbnailUrl,
int? DurationSeconds,
JsonElement KnowledgeTags,
bool? IsGeneral,
int? Difficulty,
int? Order,
bool? IsActive,
JsonElement Metadata);
public sealed record QuestionVideoCommand(
Guid QuestionId,
Guid VideoId,
string? LegacyId,
string? VideoType,
int? Order,
JsonElement Metadata);
public sealed record OperationContentCommand(
Guid? Id,
Guid? RegionId,
Guid? SchoolId,
string? LegacyId,
string? Title,
string? Subtitle,
string? Content,
string? Question,
string? Answer,
string? Link,
string? ButtonText,
string? ButtonLink,
string? BackgroundColor,
string? BorderColor,
string? ExamName,
DateTimeOffset? ExamAt,
string? ExamType,
string? Description,
int? Order,
bool? IsActive,
JsonElement Metadata);
public sealed record SimpleImportCommand(
string ImportType,
string? SourceFormat,
string? SourceName,
Guid? RegionId,
Guid? EntryId,
Guid? ContentNodeId,
Guid? SubjectId,
Guid? CategoryId,
Guid? QuestionBankId,
Guid? CollectionId,
IReadOnlyCollection<JsonElement> Items,
bool DryRun);
public sealed record SimpleImportResult(
ContentImportJobItem Job,
IReadOnlyCollection<ContentImportItemModel> Items,
IReadOnlyCollection<ContentImportIssueModel> Issues);
public sealed record ImportPostCheckResult(Guid JobId, string Status, JsonElement Counts, IReadOnlyCollection<ContentImportIssueModel> Issues);
public sealed record VideoManagementItem(
Guid Id,
Guid? SubjectId,
string? LegacyId,
string Title,
string? Description,
string? VideoUrl,
string? ThumbnailUrl,
int? DurationSeconds,
JsonElement KnowledgeTags,
bool IsGeneral,
int? Difficulty,
int Order,
bool IsActive,
JsonElement Metadata);
public sealed record QuestionVideoManagementItem(
Guid Id,
Guid? QuestionId,
Guid? VideoId,
string? LegacyId,
QuestionVideoType VideoType,
int Order,
JsonElement Metadata);
public sealed record OperationContentItem(
Guid Id,
string Kind,
Guid? RegionId,
Guid? SchoolId,
string? LegacyId,
string? Title,
string? Content,
string? Question,
string? Answer,
DateTimeOffset? ExamAt,
string? ExamType,
int Order,
bool IsActive,
JsonElement Metadata);
public sealed record ScorelineTrendItem(int Year, int SchoolCount, int MajorCount);
public interface IDirectContentService
{
Task<ContentManagementResult<QuestionManagementItem>> CreateQuestionAsync(DirectContentActor actor, QuestionWriteCommand command, CancellationToken cancellationToken = default);
Task<ContentManagementResult<QuestionManagementItem>> UpdateQuestionAsync(DirectContentActor actor, QuestionWriteCommand command, CancellationToken cancellationToken = default);
Task<CatalogList<VocabularyUnit>> GetVocabularyUnitsAsync(DirectContentActor actor, AdminLimitFilter filter, CancellationToken cancellationToken = default);
Task<ContentManagementResult<VocabularyUnit>> UpsertVocabularyUnitAsync(DirectContentActor actor, VocabularyUnitCommand command, CancellationToken cancellationToken = default);
Task<CatalogList<VocabularyWord>> GetVocabularyWordsAsync(DirectContentActor actor, AdminLimitFilter filter, CancellationToken cancellationToken = default);
Task<ContentManagementResult<VocabularyWord>> UpsertVocabularyWordAsync(DirectContentActor actor, VocabularyWordCommand command, CancellationToken cancellationToken = default);
Task<CatalogList<HandbookSubject>> GetHandbookSubjectsAsync(DirectContentActor actor, AdminLimitFilter filter, CancellationToken cancellationToken = default);
Task<ContentManagementResult<HandbookSubject>> UpsertHandbookSubjectAsync(DirectContentActor actor, HandbookSubjectCommand command, CancellationToken cancellationToken = default);
Task<CatalogList<HandbookChapter>> GetHandbookChaptersAsync(DirectContentActor actor, AdminLimitFilter filter, CancellationToken cancellationToken = default);
Task<ContentManagementResult<HandbookChapter>> UpsertHandbookChapterAsync(DirectContentActor actor, HandbookChapterCommand command, CancellationToken cancellationToken = default);
Task<CatalogList<HandbookEntry>> GetHandbookEntriesAsync(DirectContentActor actor, AdminLimitFilter filter, CancellationToken cancellationToken = default);
Task<ContentManagementResult<HandbookEntry>> UpsertHandbookEntryAsync(DirectContentActor actor, HandbookEntryCommand command, CancellationToken cancellationToken = default);
Task<CatalogList<School>> GetSchoolsAsync(DirectContentActor actor, AdminLimitFilter filter, CancellationToken cancellationToken = default);
Task<ContentManagementResult<School>> UpsertSchoolAsync(DirectContentActor actor, SchoolCommand command, CancellationToken cancellationToken = default);
Task<CatalogList<Major>> GetMajorsAsync(DirectContentActor actor, AdminLimitFilter filter, CancellationToken cancellationToken = default);
Task<ContentManagementResult<Major>> UpsertMajorAsync(DirectContentActor actor, MajorCommand command, CancellationToken cancellationToken = default);
Task<CatalogList<int>> GetScorelineYearsAsync(DirectContentActor actor, AdminLimitFilter filter, CancellationToken cancellationToken = default);
Task<CatalogList<ScorelineTrendItem>> GetScorelineTrendAsync(DirectContentActor actor, AdminLimitFilter filter, CancellationToken cancellationToken = default);
Task<CatalogList<VideoManagementItem>> GetVideosAsync(DirectContentActor actor, AdminLimitFilter filter, CancellationToken cancellationToken = default);
Task<ContentManagementResult<VideoManagementItem>> UpsertVideoAsync(DirectContentActor actor, VideoExplanationCommand command, CancellationToken cancellationToken = default);
Task<ContentManagementResult<QuestionVideoManagementItem>> BindQuestionVideoAsync(DirectContentActor actor, QuestionVideoCommand command, CancellationToken cancellationToken = default);
Task<CatalogList<OperationContentItem>> GetOperationContentAsync(DirectContentActor actor, string kind, AdminLimitFilter filter, CancellationToken cancellationToken = default);
Task<ContentManagementResult<OperationContentItem>> UpsertOperationContentAsync(DirectContentActor actor, string kind, OperationContentCommand command, CancellationToken cancellationToken = default);
Task<SimpleImportResult> PreviewImportAsync(DirectContentActor actor, SimpleImportCommand command, CancellationToken cancellationToken = default);
Task<SimpleImportResult> ExecuteImportAsync(DirectContentActor actor, SimpleImportCommand command, CancellationToken cancellationToken = default);
Task<CatalogList<ContentImportIssueModel>> GetImportIssuesAsync(DirectContentActor actor, Guid jobId, CancellationToken cancellationToken = default);
Task<ImportPostCheckResult> RunImportPostCheckAsync(DirectContentActor actor, Guid jobId, CancellationToken cancellationToken = default);
Task<ImportPostCheckResult> GetImportPostCheckAsync(DirectContentActor actor, Guid jobId, CancellationToken cancellationToken = default);
}

View File

@@ -2,6 +2,20 @@ namespace Tiku.Application.Learning;
public interface ILearningActivityService
{
Task<LearningStatsItem> GetStatsAsync(
LearningActor actor,
CancellationToken cancellationToken = default);
Task<LearningList<LearningTrendItem>> GetTrendAsync(
LearningActor actor,
LearningLimitFilter filter,
CancellationToken cancellationToken = default);
Task<LearningLeaderboardResult> GetLeaderboardAsync(
LearningActor actor,
LearningLimitFilter filter,
CancellationToken cancellationToken = default);
Task<AnswerRecordItem> SubmitAnswerAsync(
LearningActor actor,
SubmitAnswerCommand command,
@@ -22,6 +36,11 @@ public interface ILearningActivityService
LearningLimitFilter filter,
CancellationToken cancellationToken = default);
Task<WrongQuestionReviewPlan> GetWrongQuestionReviewPlanAsync(
LearningActor actor,
LearningLimitFilter filter,
CancellationToken cancellationToken = default);
Task<LearningActionResult> ResolveWrongQuestionAsync(
LearningActor actor,
QuestionActionCommand command,
@@ -32,11 +51,26 @@ public interface ILearningActivityService
LearningLimitFilter filter,
CancellationToken cancellationToken = default);
Task<WordReviewPlan> GetWordReviewPlanAsync(
LearningActor actor,
LearningLimitFilter filter,
CancellationToken cancellationToken = default);
Task<WordProgressItem> UpdateWordProgressAsync(
LearningActor actor,
WordProgressCommand command,
CancellationToken cancellationToken = default);
Task<WordProgressItem> ReviewWordAsync(
LearningActor actor,
WordReviewCommand command,
CancellationToken cancellationToken = default);
Task<WordStatsItem> GetWordStatsAsync(
LearningActor actor,
LearningLimitFilter filter,
CancellationToken cancellationToken = default);
Task<LearningList<FavoriteWordItem>> GetFavoriteWordsAsync(
LearningActor actor,
LearningLimitFilter filter,

View File

@@ -28,6 +28,62 @@ public sealed record FavoriteWordCommand(Guid WordId, bool? Favorite, string? No
public sealed record LearningLimitFilter(int? Limit = null, string? Status = null, Guid? UnitId = null);
public sealed record LearningStatsItem(
int AnswerCount,
int CorrectCount,
int WrongCount,
int FavoriteQuestionCount,
int FavoriteWordCount,
int WordProgressCount,
int PracticeSessionCount,
int PracticeReportCount);
public sealed record LearningTrendItem(DateOnly Date, int AnswerCount, int CorrectCount, int WrongCount);
public sealed record LearningLeaderboardItem(
Guid UserId,
string? DisplayName,
int AnswerCount,
int CorrectCount,
int WrongCount,
decimal Accuracy);
public sealed record LearningLeaderboardResult(
string Metric,
string Period,
IReadOnlyCollection<LearningLeaderboardItem> Items,
LearningLeaderboardItem? CurrentUser,
DateTimeOffset GeneratedAt);
public sealed record WrongQuestionReviewPlanItem(Guid QuestionId, int WrongCount, DateTimeOffset LastWrongAt);
public sealed record WrongQuestionReviewPlan(
IReadOnlyCollection<WrongQuestionReviewPlanItem> Items,
JsonElement NextAction);
public sealed record WordReviewPlanItem(
Guid WordId,
WordProgressStatus Status,
DateTimeOffset? NextReviewAt,
int CorrectCount,
int WrongCount,
WordDueLevel DueLevel);
public sealed record WordReviewPlan(
IReadOnlyCollection<WordReviewPlanItem> Items,
JsonElement NextAction);
public sealed record WordReviewCommand(Guid WordId, string? Result, DateTimeOffset? NextReviewAt);
public sealed record WordStatsItem(
int Total,
int NewCount,
int LearningCount,
int ReviewingCount,
int MasteredCount,
int DueCount,
int FavoriteCount);
public sealed record PracticeSessionCommand(
string? Mode,
string? TargetType,