feat: add student video and profile experience

This commit is contained in:
2026-07-28 15:23:42 +08:00
parent 5e993298e7
commit 7a3cf09a99
31 changed files with 19821 additions and 170 deletions

View File

@@ -107,6 +107,27 @@ public sealed class ProfileController(
cancellationToken));
}
[HttpPost("check-in")]
[EndpointSummary("每日签到")]
[ProducesResponseType<CheckInResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<CheckInResult>> CheckIn(CancellationToken cancellationToken)
{
return Ok(await profileService.CheckInAsync(ResolveActor(), cancellationToken));
}
[HttpGet("score-events")]
[EndpointSummary("查询积分流水")]
[ProducesResponseType<ProfileScoreEventList>(StatusCodes.Status200OK)]
public async Task<ActionResult<ProfileScoreEventList>> ScoreEvents(
[FromQuery] ProfileQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await profileService.GetScoreEventsAsync(
ResolveActor(),
new ProfileScoreEventQuery(query.Limit, query.SourceType, query.From, query.To),
cancellationToken));
}
[HttpPost("feedbacks")]
[EndpointSummary("提交意见反馈")]
[ProducesResponseType<FeedbackItem>(StatusCodes.Status200OK)]

View File

@@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Assets;
using Tiku.Application.Catalog;
using Tiku.Application.Security;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
[Produces("application/json")]
[Route("api/questions/videos")]
public sealed class QuestionVideosController(
IVideoPlaybackService videoPlaybackService,
ICurrentUser currentUser,
ITenantContext currentTenant) : ControllerBase
{
[HttpGet]
[EndpointSummary("查询单道题目的解析视频")]
[ProducesResponseType<CatalogList<QuestionVideoCatalogItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<QuestionVideoCatalogItem>>> List(
[FromQuery] QuestionVideoQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await videoPlaybackService.GetQuestionVideosAsync(ResolveActor(), query.ToQuery(), cancellationToken));
}
[HttpPost("batch")]
[EndpointSummary("批量查询题目解析视频")]
[ProducesResponseType<CatalogList<QuestionVideoCatalogItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<QuestionVideoCatalogItem>>> Batch(
QuestionVideoQueryDto request,
CancellationToken cancellationToken)
{
return Ok(await videoPlaybackService.GetQuestionVideosAsync(ResolveActor(), request.ToQuery(), cancellationToken));
}
private VideoPlaybackActor ResolveActor()
{
if (currentTenant.TenantId is null || currentUser.UserId is null)
{
throw new VideoPlaybackException("Current video actor was not resolved.", "video_access_denied");
}
return new VideoPlaybackActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
}
}

View File

@@ -4,6 +4,7 @@ using Tiku.Api.Contracts;
using Tiku.Application.Assets;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Jobs;
using Tiku.Application.Security;
using Tiku.Domain.Catalog;
using Tiku.Domain.Content;
@@ -16,6 +17,7 @@ namespace Tiku.Api.Controllers;
[Route("api/tenant-content")]
public sealed class TenantContentDirectController(
IDirectContentService directContentService,
IBackgroundJobService backgroundJobService,
ICurrentUser currentUser,
ITenantContext currentTenant) : ControllerBase
{
@@ -318,14 +320,53 @@ public sealed class TenantContentDirectController(
[HttpPost("imports/{importType}")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("执行同步内容导入")]
[EndpointSummary("执行或排队内容导入")]
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<SimpleImportResult>> ExecuteImport(
[ProducesResponseType<BackgroundJobItem>(StatusCodes.Status202Accepted)]
public async Task<ActionResult<object>> ExecuteImport(
string importType,
DirectImportDto request,
CancellationToken cancellationToken)
{
return Ok(await directContentService.ExecuteImportAsync(ResolveActor(), request.ToCommand(importType, dryRun: false), cancellationToken));
var actor = ResolveActor();
var command = request.ToCommand(importType, dryRun: false);
if (request.Async == true || command.Items.Count > 100)
{
var job = await backgroundJobService.EnqueueAsync(
new CreateBackgroundJobCommand(
actor.TenantId,
"content_import",
System.Text.Json.JsonSerializer.SerializeToElement(new
{
createdBy = actor.UserId,
importType = command.ImportType,
sourceFormat = command.SourceFormat,
sourceName = command.SourceName,
regionId = command.RegionId,
entryId = command.EntryId,
contentNodeId = command.ContentNodeId,
subjectId = command.SubjectId,
categoryId = command.CategoryId,
questionBankId = command.QuestionBankId,
collectionId = command.CollectionId,
items = command.Items
})),
cancellationToken);
return Accepted(job);
}
return Ok(await directContentService.ExecuteImportAsync(actor, command, cancellationToken));
}
[HttpGet("imports/detail")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("查询内容导入任务详情")]
[ProducesResponseType<ContentImportJobDetail>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentImportJobDetail>> GetImportDetail(
[FromQuery] DirectImportJobDto query,
CancellationToken cancellationToken)
{
return Ok(await directContentService.GetImportJobAsync(ResolveActor(), query.JobId, cancellationToken));
}
[HttpGet("imports/issues")]

View File

@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Assets;
using Tiku.Application.Catalog;
using Tiku.Application.Security;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
[Produces("application/json")]
[Route("api/videos")]
public sealed class VideosController(
IVideoPlaybackService videoPlaybackService,
ICurrentUser currentUser,
ITenantContext currentTenant) : ControllerBase
{
[HttpGet("search")]
[EndpointSummary("搜索通用解析视频")]
[ProducesResponseType<CatalogList<VideoExplanationCatalogItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<VideoExplanationCatalogItem>>> Search(
[FromQuery] VideoSearchQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await videoPlaybackService.SearchAsync(ResolveActor(), query.ToQuery(), cancellationToken));
}
[HttpPost("play")]
[EndpointSummary("申请视频播放信息")]
[ProducesResponseType<VideoPlaybackItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<VideoPlaybackItem>> Play(
VideoPlayDto request,
CancellationToken cancellationToken)
{
return Ok(await videoPlaybackService.PlayAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPost("progress")]
[EndpointSummary("上报视频播放进度")]
[ProducesResponseType<VideoProgressItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<VideoProgressItem>> Progress(
VideoProgressDto request,
CancellationToken cancellationToken)
{
return Ok(await videoPlaybackService.ReportProgressAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
private VideoPlaybackActor ResolveActor()
{
if (currentTenant.TenantId is null || currentUser.UserId is null)
{
throw new VideoPlaybackException("Current video actor was not resolved.", "video_access_denied");
}
return new VideoPlaybackActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
}
}