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

@@ -112,6 +112,83 @@ public sealed class AssetUploadConfirmDto
}
}
public sealed class UpsertAssetDto
{
public Guid? AssetId { get; set; }
public Guid? RegionId { get; set; }
public Guid? SubjectId { get; set; }
public Guid? CategoryId { get; set; }
public Guid? ContentNodeId { get; set; }
public string? LegacyId { get; set; }
public string? AssetKey { get; set; }
public string? Title { get; set; }
public string? Category { get; set; }
public string? Description { get; set; }
public string? FileName { get; set; }
public string? CdnUrl { get; set; }
public bool? IsPublic { get; set; }
public string? AssetType { get; set; }
public string? Visibility { get; set; }
public string? Status { get; set; }
public string? Provider { get; set; }
public string? Bucket { get; set; }
public string? ObjectKey { get; set; }
public string? MimeType { get; set; }
public long? FileSizeBytes { get; set; }
public string? ChecksumSha256 { get; set; }
public string? PreviewUrl { get; set; }
public string? PreviewObjectKey { get; set; }
public int? Order { get; set; }
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public UpsertAssetCommand ToCommand()
{
return new UpsertAssetCommand(
AssetId,
RegionId,
SubjectId,
CategoryId,
ContentNodeId,
LegacyId,
AssetKey,
Title,
Category,
Description,
FileName,
CdnUrl,
IsPublic,
AssetType,
Visibility,
Status,
Provider,
Bucket,
ObjectKey,
MimeType,
FileSizeBytes,
ChecksumSha256,
PreviewUrl,
PreviewObjectKey,
Order,
AccessRules,
Metadata);
}
}
public sealed class AssetAccessSignDto
{
[Required]
public Guid AssetId { get; set; }
[Range(60, 3600)]
public int? ExpiresInSeconds { get; set; }
public AssetAccessSignCommand ToCommand()
{
return new AssetAccessSignCommand(AssetId, ExpiresInSeconds);
}
}
public sealed class AssetManagementQueryDto
{
public Guid? RegionId { get; set; }
@@ -156,6 +233,20 @@ public sealed class AssetManagementQueryDto
}
}
public sealed class AssetEventQueryDto
{
public Guid? AssetId { get; set; }
public Guid? UserId { get; set; }
[Range(1, 500)]
public int? Limit { get; set; }
public AssetEventFilter ToFilter()
{
return new AssetEventFilter(AssetId, UserId, Limit);
}
}
public sealed class ImportJobQueryDto
{
[StringLength(50)]

View File

@@ -0,0 +1,449 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Tiku.Application.Content;
using Tiku.Domain.Common;
namespace Tiku.Api.Contracts;
public sealed class DirectContentQueryDto
{
public Guid? RegionId { get; set; }
public Guid? EntryId { get; set; }
public Guid? ContentNodeId { get; set; }
public Guid? ParentId { get; set; }
public Guid? SubjectId { get; set; }
public Guid? ChapterId { get; set; }
public Guid? UnitId { get; set; }
public Guid? SchoolId { get; set; }
public Guid? MajorId { get; set; }
public Guid? QuestionId { get; set; }
[StringLength(50)]
public string? Status { get; set; }
[StringLength(200)]
public string? Keyword { get; set; }
[Range(1900, 3000)]
public int? Year { get; set; }
[Range(1, 1000)]
public int? Limit { get; set; }
public AdminLimitFilter ToFilter()
{
return new AdminLimitFilter(
RegionId,
EntryId,
ContentNodeId,
ParentId,
SubjectId,
ChapterId,
UnitId,
SchoolId,
MajorId,
QuestionId,
Status,
Keyword,
Year,
Limit);
}
}
public sealed class DirectQuestionWriteDto
{
public Guid? QuestionId { get; set; }
public Guid? QuestionBankId { get; set; }
public Guid? SubjectId { get; set; }
public Guid? CategoryId { get; set; }
public Guid? NodeId { get; set; }
public Guid? EntryId { get; set; }
public Guid? ContentNodeId { get; set; }
public Guid? PrimaryCollectionId { get; set; }
public string? LegacyId { get; set; }
public string? Type { get; set; }
public string? TypeLabel { get; set; }
[Range(1, 5)]
public int? Difficulty { get; set; }
public JsonElement Tags { get; set; } = JsonDefaults.Array();
public string? Content { get; set; }
public JsonElement Options { get; set; } = JsonDefaults.Array();
public int? CorrectOptionIndex { get; set; }
public JsonElement CorrectOptionIndices { get; set; } = JsonDefaults.Array();
public string? AnswerText { get; set; }
public string? Explanation { get; set; }
public JsonElement SubQuestions { get; set; } = JsonDefaults.Array();
public string? CodeLang { get; set; }
public string? CodeTemplate { get; set; }
public string? MediaUrl { get; set; }
public string? Status { get; set; }
public JsonElement ExamMarkers { get; set; } = JsonDefaults.Object();
public string? SourceHash { get; set; }
public bool? CreateVersion { get; set; }
public QuestionWriteCommand ToCommand(bool createVersionDefault)
{
return new QuestionWriteCommand(
QuestionId,
QuestionBankId,
SubjectId,
CategoryId,
NodeId,
EntryId,
ContentNodeId,
PrimaryCollectionId,
LegacyId,
Type,
TypeLabel,
Difficulty,
Tags,
Content,
Options,
CorrectOptionIndex,
CorrectOptionIndices,
AnswerText,
Explanation,
SubQuestions,
CodeLang,
CodeTemplate,
MediaUrl,
Status,
ExamMarkers,
SourceHash,
CreateVersion ?? createVersionDefault);
}
}
public sealed class DirectVocabularyUnitDto
{
public Guid? Id { get; set; }
public Guid? RegionId { get; set; }
public Guid? EntryId { get; set; }
public Guid? ContentNodeId { get; set; }
public string? LegacyId { get; set; }
public required string Name { get; set; }
public string? Description { get; set; }
public int? WordCount { get; set; }
public int? Order { get; set; }
public bool? IsActive { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public VocabularyUnitCommand ToCommand()
{
return new VocabularyUnitCommand(Id, RegionId, EntryId, ContentNodeId, LegacyId, Name, Description, WordCount, Order, IsActive, Metadata);
}
}
public sealed class DirectVocabularyWordDto
{
public Guid? Id { get; set; }
public Guid? UnitId { get; set; }
public Guid? EntryId { get; set; }
public Guid? ContentNodeId { get; set; }
public string? LegacyId { get; set; }
public required string Word { get; set; }
public string? Phonetic { get; set; }
public string? Meaning { get; set; }
public string? Example { get; set; }
public string? ExampleTranslation { get; set; }
public int? Difficulty { get; set; }
public JsonElement Tags { get; set; } = JsonDefaults.Array();
public int? Order { get; set; }
public bool? IsActive { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public VocabularyWordCommand ToCommand()
{
return new VocabularyWordCommand(
Id,
UnitId,
EntryId,
ContentNodeId,
LegacyId,
Word,
Phonetic,
Meaning,
Example,
ExampleTranslation,
Difficulty,
Tags,
Order,
IsActive,
Metadata);
}
}
public sealed class DirectHandbookSubjectDto
{
public Guid? Id { get; set; }
public Guid? RegionId { get; set; }
public Guid? SchoolId { get; set; }
public Guid? MajorId { get; set; }
public Guid? EntryId { get; set; }
public Guid? ContentNodeId { get; set; }
public string? LegacyId { get; set; }
public required string Name { get; set; }
public string? Type { get; set; }
public string? Icon { get; set; }
public string? Color { get; set; }
public string? Description { get; set; }
public int? Order { get; set; }
public bool? IsActive { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public HandbookSubjectCommand ToCommand()
{
return new HandbookSubjectCommand(
Id,
RegionId,
SchoolId,
MajorId,
EntryId,
ContentNodeId,
LegacyId,
Name,
Type,
Icon,
Color,
Description,
Order,
IsActive,
Metadata);
}
}
public sealed class DirectHandbookChapterDto
{
public Guid? Id { get; set; }
public Guid? SubjectId { get; set; }
public Guid? EntryId { get; set; }
public Guid? ContentNodeId { get; set; }
public string? LegacyId { get; set; }
public required string Name { get; set; }
public string? Description { get; set; }
public int? Order { get; set; }
public bool? IsActive { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public HandbookChapterCommand ToCommand()
{
return new HandbookChapterCommand(Id, SubjectId, EntryId, ContentNodeId, LegacyId, Name, Description, Order, IsActive, Metadata);
}
}
public sealed class DirectHandbookEntryDto
{
public Guid? Id { get; set; }
public Guid? ChapterId { get; set; }
public Guid? EntryId { get; set; }
public Guid? ContentNodeId { get; set; }
public string? LegacyId { get; set; }
public required string Title { get; set; }
public string? Summary { get; set; }
public string? Content { get; set; }
public JsonElement Tags { get; set; } = JsonDefaults.Array();
public int? Order { get; set; }
public bool? IsActive { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public HandbookEntryCommand ToCommand()
{
return new HandbookEntryCommand(Id, ChapterId, EntryId, ContentNodeId, LegacyId, Title, Summary, Content, Tags, Order, IsActive, Metadata);
}
}
public sealed class DirectSchoolDto
{
public Guid? Id { get; set; }
public Guid? RegionId { get; set; }
public string? LegacyId { get; set; }
public required string Name { get; set; }
public string? ProfessionalExamDate { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public SchoolCommand ToCommand()
{
return new SchoolCommand(Id, RegionId, LegacyId, Name, ProfessionalExamDate, Metadata);
}
}
public sealed class DirectMajorDto
{
public Guid? Id { get; set; }
public Guid? RegionId { get; set; }
public Guid? SchoolId { get; set; }
public string? LegacyId { get; set; }
public required string Name { get; set; }
public string? Description { get; set; }
public string? StudyTips { get; set; }
public int? Order { get; set; }
public bool? IsActive { get; set; }
public MajorCommand ToCommand()
{
return new MajorCommand(Id, RegionId, SchoolId, LegacyId, Name, Description, StudyTips, Order, IsActive);
}
}
public sealed class DirectVideoDto
{
public Guid? Id { get; set; }
public Guid? SubjectId { get; set; }
public string? LegacyId { get; set; }
public required string Title { get; set; }
public string? Description { get; set; }
public string? VideoUrl { get; set; }
public string? ThumbnailUrl { get; set; }
public int? DurationSeconds { get; set; }
public JsonElement KnowledgeTags { get; set; } = JsonDefaults.Array();
public bool? IsGeneral { get; set; }
public int? Difficulty { get; set; }
public int? Order { get; set; }
public bool? IsActive { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public VideoExplanationCommand ToCommand()
{
return new VideoExplanationCommand(
Id,
SubjectId,
LegacyId,
Title,
Description,
VideoUrl,
ThumbnailUrl,
DurationSeconds,
KnowledgeTags,
IsGeneral,
Difficulty,
Order,
IsActive,
Metadata);
}
}
public sealed class DirectQuestionVideoDto
{
public Guid QuestionId { get; set; }
public Guid VideoId { get; set; }
public string? LegacyId { get; set; }
public string? VideoType { get; set; }
public int? Order { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public QuestionVideoCommand ToCommand()
{
return new QuestionVideoCommand(QuestionId, VideoId, LegacyId, VideoType, Order, Metadata);
}
}
public sealed class DirectOperationContentDto
{
public Guid? Id { get; set; }
public Guid? RegionId { get; set; }
public Guid? SchoolId { get; set; }
public string? LegacyId { get; set; }
public string? Title { get; set; }
public string? Subtitle { get; set; }
public string? Content { get; set; }
public string? Question { get; set; }
public string? Answer { get; set; }
public string? Link { get; set; }
public string? ButtonText { get; set; }
public string? ButtonLink { get; set; }
public string? BackgroundColor { get; set; }
public string? BorderColor { get; set; }
public string? ExamName { get; set; }
public DateTimeOffset? ExamAt { get; set; }
public string? ExamType { get; set; }
public string? Description { get; set; }
public int? Order { get; set; }
public bool? IsActive { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public OperationContentCommand ToCommand()
{
return new OperationContentCommand(
Id,
RegionId,
SchoolId,
LegacyId,
Title,
Subtitle,
Content,
Question,
Answer,
Link,
ButtonText,
ButtonLink,
BackgroundColor,
BorderColor,
ExamName,
ExamAt,
ExamType,
Description,
Order,
IsActive,
Metadata);
}
}
public sealed class DirectImportDto
{
public string? SourceFormat { get; set; }
public string? SourceName { get; set; }
public Guid? RegionId { get; set; }
public Guid? EntryId { get; set; }
public Guid? ContentNodeId { get; set; }
public Guid? SubjectId { get; set; }
public Guid? CategoryId { get; set; }
public Guid? QuestionBankId { get; set; }
public Guid? CollectionId { get; set; }
public IReadOnlyCollection<JsonElement>? Items { get; set; }
public IReadOnlyCollection<JsonElement>? Units { get; set; }
public IReadOnlyCollection<JsonElement>? Words { get; set; }
public IReadOnlyCollection<JsonElement>? Subjects { get; set; }
public IReadOnlyCollection<JsonElement>? Entries { get; set; }
public IReadOnlyCollection<JsonElement>? Fields { get; set; }
public IReadOnlyCollection<JsonElement>? Schools { get; set; }
public IReadOnlyCollection<JsonElement>? Majors { get; set; }
public IReadOnlyCollection<JsonElement>? Records { get; set; }
public IReadOnlyCollection<JsonElement>? Videos { get; set; }
public SimpleImportCommand ToCommand(string importType, bool dryRun)
{
return new SimpleImportCommand(
importType,
SourceFormat,
SourceName,
RegionId,
EntryId,
ContentNodeId,
SubjectId,
CategoryId,
QuestionBankId,
CollectionId,
ResolveItems(importType),
dryRun);
}
private IReadOnlyCollection<JsonElement> ResolveItems(string importType)
{
return importType.ToLowerInvariant() switch
{
"questions" => Items ?? [],
"vocabulary" => Words ?? Units ?? Items ?? [],
"handbook" => Entries ?? Subjects ?? Items ?? [],
"scoreline" => Records ?? Schools ?? Majors ?? Fields ?? Items ?? [],
"videos" => Videos ?? Items ?? [],
_ => Items ?? []
};
}
}
public sealed class DirectImportJobDto
{
public Guid JobId { get; set; }
}

View File

@@ -184,3 +184,19 @@ public sealed class FavoriteWordDto
return new FavoriteWordCommand(WordId, Favorite, Note);
}
}
public sealed class WordReviewDto
{
[Required]
public Guid WordId { get; set; }
[StringLength(50)]
public string? Result { get; set; }
public DateTimeOffset? NextReviewAt { get; set; }
public WordReviewCommand ToCommand()
{
return new WordReviewCommand(WordId, Result, NextReviewAt);
}
}

View File

@@ -15,6 +15,34 @@ public sealed class LearningController(
ICurrentUser currentUser,
ICurrentTenant currentTenant) : ControllerBase
{
[HttpGet("stats")]
[EndpointSummary("查询学习统计")]
[ProducesResponseType<LearningStatsItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<LearningStatsItem>> GetStats(CancellationToken cancellationToken)
{
return Ok(await learningActivityService.GetStatsAsync(ResolveActor(), cancellationToken));
}
[HttpGet("trend")]
[EndpointSummary("查询学习趋势")]
[ProducesResponseType<LearningList<LearningTrendItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<LearningList<LearningTrendItem>>> GetTrend(
[FromQuery] LearningLimitQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.GetTrendAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpGet("leaderboard")]
[EndpointSummary("查询学习排行榜")]
[ProducesResponseType<LearningLeaderboardResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<LearningLeaderboardResult>> GetLeaderboard(
[FromQuery] LearningLimitQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.GetLeaderboardAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPost("practice-sessions")]
[EndpointSummary("创建练习会话")]
[ProducesResponseType<PracticeSessionItem>(StatusCodes.Status200OK)]
@@ -153,6 +181,19 @@ public sealed class LearningController(
cancellationToken));
}
[HttpGet("wrong-questions/review-plan")]
[EndpointSummary("生成错题复习计划")]
[ProducesResponseType<WrongQuestionReviewPlan>(StatusCodes.Status200OK)]
public async Task<ActionResult<WrongQuestionReviewPlan>> GetWrongQuestionReviewPlan(
[FromQuery] LearningLimitQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.GetWrongQuestionReviewPlanAsync(
ResolveActor(),
query.ToFilter(),
cancellationToken));
}
[HttpPost("wrong-questions/resolve")]
[EndpointSummary("将错题标记为已解决")]
[ProducesResponseType<LearningActionResult>(StatusCodes.Status200OK)]
@@ -180,6 +221,19 @@ public sealed class LearningController(
cancellationToken));
}
[HttpGet("vocabulary/review-plan")]
[EndpointSummary("生成单词复习计划")]
[ProducesResponseType<WordReviewPlan>(StatusCodes.Status200OK)]
public async Task<ActionResult<WordReviewPlan>> GetWordReviewPlan(
[FromQuery] LearningLimitQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.GetWordReviewPlanAsync(
ResolveActor(),
query.ToFilter(),
cancellationToken));
}
[HttpPost("vocabulary/progress")]
[EndpointSummary("更新单词学习进度")]
[ProducesResponseType<WordProgressItem>(StatusCodes.Status200OK)]
@@ -194,6 +248,33 @@ public sealed class LearningController(
cancellationToken));
}
[HttpPost("vocabulary/review")]
[EndpointSummary("提交单词复习结果")]
[ProducesResponseType<WordProgressItem>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
public async Task<ActionResult<WordProgressItem>> ReviewWord(
WordReviewDto request,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.ReviewWordAsync(
ResolveActor(),
request.ToCommand(),
cancellationToken));
}
[HttpGet("vocabulary/stats")]
[EndpointSummary("查询单词学习统计")]
[ProducesResponseType<WordStatsItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<WordStatsItem>> GetWordStats(
[FromQuery] LearningLimitQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.GetWordStatsAsync(
ResolveActor(),
query.ToFilter(),
cancellationToken));
}
[HttpGet("vocabulary/favorites")]
[EndpointSummary("查询收藏单词")]
[ProducesResponseType<LearningList<FavoriteWordItem>>(StatusCodes.Status200OK)]

View File

@@ -166,6 +166,45 @@ public sealed class TenantContentController(
cancellationToken));
}
[HttpGet("assets/access-events")]
[EndpointSummary("查询资产访问审计事件")]
[ProducesResponseType<CatalogList<ContentAssetAccessEventItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<ContentAssetAccessEventItem>>> GetAssetAccessEvents(
[FromQuery] AssetEventQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await assetManagementService.GetAccessEventsAsync(
ResolveActor(),
query.ToFilter(),
cancellationToken));
}
[HttpGet("assets/security-scan-events")]
[EndpointSummary("查询资产安全扫描事件")]
[ProducesResponseType<CatalogList<ContentAssetSecurityScanEventItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<ContentAssetSecurityScanEventItem>>> GetAssetSecurityScanEvents(
[FromQuery] AssetEventQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await assetManagementService.GetSecurityScanEventsAsync(
ResolveActor(),
query.ToFilter(),
cancellationToken));
}
[HttpPut("assets")]
[EndpointSummary("新增或更新内容资产")]
[ProducesResponseType<ContentManagementResult<ContentAssetManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<ContentAssetManagementItem>>> UpsertAsset(
UpsertAssetDto request,
CancellationToken cancellationToken)
{
return Ok(await assetManagementService.UpsertAssetAsync(
ResolveActor(),
request.ToCommand(),
cancellationToken));
}
[HttpPost("assets/uploads/sign")]
[EndpointSummary("创建资产上传签名")]
[ProducesResponseType<AssetUploadSignResult>(StatusCodes.Status200OK)]
@@ -196,6 +235,32 @@ public sealed class TenantContentController(
cancellationToken));
}
[HttpPost("assets/sign-download")]
[EndpointSummary("签发管理侧资产下载地址")]
[ProducesResponseType<AssetManagementSignedAccessResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<AssetManagementSignedAccessResult>> SignAssetDownload(
AssetAccessSignDto request,
CancellationToken cancellationToken)
{
return Ok(await assetManagementService.SignDownloadAsync(
ResolveActor(),
request.ToCommand(),
cancellationToken));
}
[HttpPost("assets/sign-preview")]
[EndpointSummary("签发管理侧资产预览地址")]
[ProducesResponseType<AssetManagementSignedAccessResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<AssetManagementSignedAccessResult>> SignAssetPreview(
AssetAccessSignDto request,
CancellationToken cancellationToken)
{
return Ok(await assetManagementService.SignPreviewAsync(
ResolveActor(),
request.ToCommand(),
cancellationToken));
}
[HttpGet("import-jobs")]
[EndpointSummary("查询内容导入任务")]
[ProducesResponseType<CatalogList<ContentImportJobItem>>(StatusCodes.Status200OK)]

View File

@@ -0,0 +1,315 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Assets;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Domain.Catalog;
using Tiku.Domain.Content;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Produces("application/json")]
[Route("api/tenant-content")]
public sealed class TenantContentDirectController(
IDirectContentService directContentService,
ICurrentUser currentUser,
ICurrentTenant currentTenant) : ControllerBase
{
[HttpPost("questions")]
[EndpointSummary("创建题目及首个版本")]
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> CreateQuestion(
DirectQuestionWriteDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.CreateQuestionAsync(ResolveActor(), request.ToCommand(createVersionDefault: true), cancellationToken));
}
[HttpPatch("questions")]
[EndpointSummary("更新题目并可选择创建新版本")]
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> UpdateQuestion(
DirectQuestionWriteDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.UpdateQuestionAsync(ResolveActor(), request.ToCommand(createVersionDefault: false), cancellationToken));
}
[HttpGet("vocabulary-units")]
[EndpointSummary("查询管理侧词汇单元")]
[ProducesResponseType<CatalogList<VocabularyUnit>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<VocabularyUnit>>> GetVocabularyUnits(
[FromQuery] DirectContentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetVocabularyUnitsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("vocabulary-units")]
[EndpointSummary("新增或更新词汇单元")]
[ProducesResponseType<ContentManagementResult<VocabularyUnit>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<VocabularyUnit>>> UpsertVocabularyUnit(
DirectVocabularyUnitDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.UpsertVocabularyUnitAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("vocabulary-words")]
[EndpointSummary("查询管理侧词汇")]
[ProducesResponseType<CatalogList<VocabularyWord>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<VocabularyWord>>> GetVocabularyWords(
[FromQuery] DirectContentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetVocabularyWordsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("vocabulary-words")]
[EndpointSummary("新增或更新词汇")]
[ProducesResponseType<ContentManagementResult<VocabularyWord>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<VocabularyWord>>> UpsertVocabularyWord(
DirectVocabularyWordDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.UpsertVocabularyWordAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("handbook-subjects")]
[EndpointSummary("查询管理侧知识手册科目")]
[ProducesResponseType<CatalogList<HandbookSubject>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<HandbookSubject>>> GetHandbookSubjects(
[FromQuery] DirectContentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetHandbookSubjectsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("handbook-subjects")]
[EndpointSummary("新增或更新知识手册科目")]
[ProducesResponseType<ContentManagementResult<HandbookSubject>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<HandbookSubject>>> UpsertHandbookSubject(
DirectHandbookSubjectDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.UpsertHandbookSubjectAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("handbook-chapters")]
[EndpointSummary("查询管理侧知识手册章节")]
[ProducesResponseType<CatalogList<HandbookChapter>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<HandbookChapter>>> GetHandbookChapters(
[FromQuery] DirectContentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetHandbookChaptersAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("handbook-chapters")]
[EndpointSummary("新增或更新知识手册章节")]
[ProducesResponseType<ContentManagementResult<HandbookChapter>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<HandbookChapter>>> UpsertHandbookChapter(
DirectHandbookChapterDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.UpsertHandbookChapterAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("handbook-entries")]
[EndpointSummary("查询管理侧知识手册条目")]
[ProducesResponseType<CatalogList<HandbookEntry>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<HandbookEntry>>> GetHandbookEntries(
[FromQuery] DirectContentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetHandbookEntriesAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("handbook-entries")]
[EndpointSummary("新增或更新知识手册条目")]
[ProducesResponseType<ContentManagementResult<HandbookEntry>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<HandbookEntry>>> UpsertHandbookEntry(
DirectHandbookEntryDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.UpsertHandbookEntryAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("scoreline/schools")]
[EndpointSummary("查询管理侧分数线院校")]
[ProducesResponseType<CatalogList<School>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<School>>> GetSchools(
[FromQuery] DirectContentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetSchoolsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("scoreline/schools")]
[EndpointSummary("新增或更新分数线院校")]
[ProducesResponseType<ContentManagementResult<School>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<School>>> UpsertSchool(
DirectSchoolDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.UpsertSchoolAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("scoreline/majors")]
[EndpointSummary("查询管理侧分数线专业")]
[ProducesResponseType<CatalogList<Major>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<Major>>> GetMajors(
[FromQuery] DirectContentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetMajorsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("scoreline/majors")]
[EndpointSummary("新增或更新分数线专业")]
[ProducesResponseType<ContentManagementResult<Major>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<Major>>> UpsertMajor(
DirectMajorDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.UpsertMajorAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("scoreline/years")]
[EndpointSummary("查询分数线年份")]
[ProducesResponseType<CatalogList<int>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<int>>> GetScorelineYears(
[FromQuery] DirectContentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetScorelineYearsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpGet("scoreline/trend")]
[EndpointSummary("查询分数线趋势摘要")]
[ProducesResponseType<CatalogList<ScorelineTrendItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<ScorelineTrendItem>>> GetScorelineTrend(
[FromQuery] DirectContentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetScorelineTrendAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpGet("videos")]
[EndpointSummary("查询租户视频解析")]
[ProducesResponseType<CatalogList<VideoManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<VideoManagementItem>>> GetVideos(
[FromQuery] DirectContentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetVideosAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("videos")]
[EndpointSummary("新增或更新视频解析")]
[ProducesResponseType<ContentManagementResult<VideoManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<VideoManagementItem>>> UpsertVideo(
DirectVideoDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.UpsertVideoAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPost("question-videos")]
[EndpointSummary("绑定题目与解析视频")]
[ProducesResponseType<ContentManagementResult<QuestionVideoManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<QuestionVideoManagementItem>>> BindQuestionVideo(
DirectQuestionVideoDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.BindQuestionVideoAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("operations/{kind}")]
[EndpointSummary("查询运营内容")]
[ProducesResponseType<CatalogList<OperationContentItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<OperationContentItem>>> GetOperationContent(
string kind,
[FromQuery] DirectContentQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetOperationContentAsync(ResolveActor(), kind, query.ToFilter(), cancellationToken));
}
[HttpPut("operations/{kind}")]
[EndpointSummary("新增或更新运营内容")]
[ProducesResponseType<ContentManagementResult<OperationContentItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<OperationContentItem>>> UpsertOperationContent(
string kind,
DirectOperationContentDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.UpsertOperationContentAsync(ResolveActor(), kind, request.ToCommand(), cancellationToken));
}
[HttpPost("imports/preview/{importType}")]
[EndpointSummary("预览内容导入数据")]
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<SimpleImportResult>> PreviewImport(
string importType,
DirectImportDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.PreviewImportAsync(ResolveActor(), request.ToCommand(importType, dryRun: true), cancellationToken));
}
[HttpPost("imports/{importType}")]
[EndpointSummary("执行同步内容导入")]
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<SimpleImportResult>> ExecuteImport(
string importType,
DirectImportDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.ExecuteImportAsync(ResolveActor(), request.ToCommand(importType, dryRun: false), cancellationToken));
}
[HttpGet("imports/issues")]
[EndpointSummary("查询内容导入问题明细")]
[ProducesResponseType<CatalogList<ContentImportIssueModel>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<ContentImportIssueModel>>> GetImportIssues(
[FromQuery] DirectImportJobDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetImportIssuesAsync(ResolveActor(), query.JobId, cancellationToken));
}
[HttpPost("imports/post-check")]
[EndpointSummary("执行内容导入后完整性检查")]
[ProducesResponseType<ImportPostCheckResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<ImportPostCheckResult>> RunImportPostCheck(
DirectImportJobDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.RunImportPostCheckAsync(ResolveActor(), request.JobId, cancellationToken));
}
[HttpGet("imports/post-check")]
[EndpointSummary("查询内容导入后检查状态")]
[ProducesResponseType<ImportPostCheckResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<ImportPostCheckResult>> GetImportPostCheck(
[FromQuery] DirectImportJobDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetImportPostCheckAsync(ResolveActor(), query.JobId, cancellationToken));
}
private DirectContentActor ResolveActor()
{
if (currentTenant.TenantId is null || currentUser.UserId is null)
{
throw new ContentManagementException("Tenant content actor was not resolved.", "tenant_content_access_denied");
}
return new DirectContentActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
}
}