Files
tiku-backend.net/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Questions/PlatformQuestionAdministrationFoundation.cs

191 lines
9.5 KiB
C#

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Assets;
using Tiku.Application.Learning;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Operations;
using Tiku.Domain.QuestionBanks;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.PlatformAdmin;
internal abstract partial class PlatformQuestionBankServiceBase
{
protected Task<PlatformQuestionPage> GetQuestionsCoreAsync(
PlatformAdminActor actor,
PlatformQuestionBankFilter filter,
CancellationToken cancellationToken = default)
{
return ExecuteAsync(actor, "查询公共题库题目", async (_, dbContext, tenantId, token) =>
{
if (!filter.QuestionBankId.HasValue) throw Error("请选择公共题库。", "question_bank_id_required");
await RequireBankAsync(dbContext, tenantId, filter.QuestionBankId.Value, token);
var query = dbContext.Questions.AsNoTracking().Where(item =>
item.TenantId == tenantId && item.QuestionBankId == filter.QuestionBankId);
if (filter.ContentNodeId.HasValue) query = query.Where(item => item.ContentNodeId == filter.ContentNodeId);
if (!string.IsNullOrWhiteSpace(filter.Type))
{
var type = filter.Type.Trim();
query = query.Where(item => item.Type == type);
}
if (filter.Difficulty.HasValue) query = query.Where(item => item.Difficulty == filter.Difficulty);
if (!string.IsNullOrWhiteSpace(filter.Status) &&
!filter.Status.Equals("all", StringComparison.OrdinalIgnoreCase))
{
var status = ParseQuestionStatus(filter.Status);
query = query.Where(item => item.Status == status);
}
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(item => item.Type.Contains(keyword) || dbContext.QuestionVersions.Any(version =>
version.TenantId == tenantId && version.QuestionId == item.Id &&
version.Id == item.CurrentVersionId && version.Content != null &&
version.Content.Contains(keyword)));
}
var page = Math.Max(1, filter.Page);
var pageSize = Math.Clamp(filter.PageSize, 1, MaxPageSize);
var total = await query.CountAsync(token);
var questions = await query.OrderByDescending(item => item.UpdatedAt).Skip((page - 1) * pageSize)
.Take(pageSize).ToArrayAsync(token);
var versionIds = questions.Where(item => item.CurrentVersionId.HasValue)
.Select(item => item.CurrentVersionId!.Value).ToArray();
var versions = await dbContext.QuestionVersions.AsNoTracking()
.Where(item => item.TenantId == tenantId && versionIds.Contains(item.Id))
.ToDictionaryAsync(item => item.Id, token);
return new PlatformQuestionPage(
questions.Select(item => ToQuestionItem(item,
item.CurrentVersionId.HasValue && versions.TryGetValue(item.CurrentVersionId.Value, out var version)
? version
: null)).ToArray(), total, page, pageSize);
}, cancellationToken);
}
protected Task<PlatformQuestionItem> UpsertQuestionCoreAsync(
PlatformAdminActor actor,
UpsertPlatformQuestionCommand command,
CancellationToken cancellationToken = default)
{
return ExecuteAsync(actor, "保存公共题库题目", async (_, dbContext, tenantId, token) =>
{
var bank = await RequireBankWithEntryAsync(dbContext, tenantId, command.QuestionBankId, token);
var node = await RequireNodeAsync(dbContext, tenantId, bank, command.ContentNodeId, token);
ValidateQuestion(command);
var question = command.Id.HasValue
? await dbContext.Questions.SingleOrDefaultAsync(
item => item.TenantId == tenantId && item.Id == command.Id.Value && item.QuestionBankId == bank.Id,
token)
: null;
if (command.Id.HasValue && question is null) throw Error("题目不存在。", "question_not_found");
question ??= new Question
{
TenantId = tenantId,
QuestionBankId = bank.Id,
EntryId = bank.ContentEntryId,
CreatedAt = DateTimeOffset.UtcNow
};
if (!command.Id.HasValue) dbContext.Questions.Add(question);
ApplyQuestion(question, command, bank.ContentEntryId!.Value, node.Id);
await dbContext.SaveChangesAsync(token);
var nextVersion = await dbContext.QuestionVersions
.Where(item => item.TenantId == tenantId && item.QuestionId == question.Id)
.Select(item => (int?)item.VersionNo).MaxAsync(token) ?? 0;
var version = BuildVersion(actor, tenantId, question.Id, nextVersion + 1, command);
dbContext.QuestionVersions.Add(version);
question.CurrentVersionId = version.Id;
AddAudit(dbContext, actor, "platform.question_bank.question_saved", question.Id,
new { bankId = bank.Id, nodeId = node.Id, version = version.VersionNo });
await dbContext.SaveChangesAsync(token);
return ToQuestionItem(question, version);
}, cancellationToken);
}
protected Task<int> ArchiveQuestionsCoreAsync(
PlatformAdminActor actor,
ArchivePlatformQuestionsCommand command,
CancellationToken cancellationToken = default)
{
return ExecuteAsync(actor, "归档公共题库题目", async (_, dbContext, tenantId, token) =>
{
var ids = command.QuestionIds.Distinct().Take(500).ToArray();
if (ids.Length == 0) throw Error("请选择需要归档的题目。", "question_ids_required");
var questions = await dbContext.Questions.Where(item => item.TenantId == tenantId && ids.Contains(item.Id))
.ToArrayAsync(token);
foreach (var question in questions) question.Status = QuestionStatus.Archived;
AddAudit(dbContext, actor, "platform.question_bank.questions_archived", Guid.NewGuid(),
new { questionIds = questions.Select(item => item.Id).ToArray() });
await dbContext.SaveChangesAsync(token);
return questions.Length;
}, cancellationToken);
}
private static void ValidateQuestion(UpsertPlatformQuestionCommand command)
{
if (string.IsNullOrWhiteSpace(command.Content)) throw Error("题干不能为空。", "question_content_required");
var type = string.IsNullOrWhiteSpace(command.Type) ? "choice" : command.Type.Trim();
if (!SupportedQuestionTypes.Contains(type)) throw Error("题型不受支持。", "question_type_invalid");
if (command.Difficulty is < 1 or > 5) throw Error("难度必须在 1 到 5 之间。", "question_difficulty_invalid");
if (ParseQuestionStatus(command.Status) == QuestionStatus.Published &&
!QuestionGrader.HasValidAuthoritativeAnswer(
type,
command.CorrectOptionIndex,
command.CorrectOptionIndices,
command.AnswerText))
throw Error("发布题目必须提供有效的标准答案。", "question_grading_rule_invalid");
}
private static void ApplyQuestion(Question question, UpsertPlatformQuestionCommand command, Guid entryId,
Guid nodeId)
{
question.QuestionBankId = command.QuestionBankId;
question.EntryId = entryId;
question.ContentNodeId = nodeId;
question.LegacyId = Normalize(command.LegacyId);
question.Type = Normalize(command.Type) ?? "choice";
question.TypeLabel = Normalize(command.TypeLabel);
question.Difficulty = command.Difficulty;
question.Tags = ArrayOrDefault(command.Tags);
question.MediaUrl = Normalize(command.MediaUrl);
question.ExamMarkers = ObjectOrDefault(command.ExamMarkers);
question.Status = ParseQuestionStatus(command.Status);
}
private static QuestionVersion BuildVersion(PlatformAdminActor actor, Guid tenantId, Guid questionId, int versionNo,
UpsertPlatformQuestionCommand command)
{
return new QuestionVersion
{
TenantId = tenantId,
QuestionId = questionId,
VersionNo = versionNo,
QuestionType = Normalize(command.Type) ?? "choice",
TypeLabel = Normalize(command.TypeLabel),
Difficulty = command.Difficulty,
Tags = ArrayOrDefault(command.Tags),
Content = Normalize(command.Content),
Options = ArrayOrDefault(command.Options),
CorrectOptionIndex = command.CorrectOptionIndex,
CorrectOptionIndices = ArrayOrDefault(command.CorrectOptionIndices),
AnswerText = Normalize(command.AnswerText),
Explanation = Normalize(command.Explanation),
SubQuestions = ArrayOrDefault(command.SubQuestions),
CodeLang = Normalize(command.CodeLang),
CodeTemplate = Normalize(command.CodeTemplate),
SourceHash = Normalize(command.SourceHash) ?? Hash(JsonSerializer.Serialize(command)),
CreatedBy = actor.UserId
};
}
}