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> GetNodesCoreAsync( PlatformAdminActor actor, Guid bankId, CancellationToken cancellationToken = default) { return ExecuteAsync>(actor, "查询公共题库内容结构", async (_, dbContext, tenantId, token) => { var bank = await RequireBankWithEntryAsync(dbContext, tenantId, bankId, token); var nodes = await dbContext.ContentNodes.AsNoTracking() .Where(item => item.TenantId == tenantId && item.EntryId == bank.ContentEntryId) .OrderBy(item => item.Depth).ThenBy(item => item.SortOrder).ThenBy(item => item.Name) .ToArrayAsync(token); var counts = await dbContext.Questions.AsNoTracking() .Where(item => item.TenantId == tenantId && item.QuestionBankId == bankId && item.ContentNodeId.HasValue && item.Status != QuestionStatus.Archived) .GroupBy(item => item.ContentNodeId!.Value) .Select(group => new { NodeId = group.Key, Count = group.Count() }) .ToDictionaryAsync(item => item.NodeId, item => item.Count, token); return nodes.Select(item => ToNodeItem(item, bankId, counts.TryGetValue(item.Id, out var count) ? count : 0)).ToArray(); }, cancellationToken); } protected Task UpsertNodeCoreServiceAsync( PlatformAdminActor actor, UpsertPlatformQuestionBankNodeCommand command, CancellationToken cancellationToken = default) { return ExecuteAsync(actor, "保存公共题库内容节点", async (_, dbContext, tenantId, token) => { var bank = await RequireBankWithEntryAsync(dbContext, tenantId, command.QuestionBankId, token); return await UpsertNodeCoreAsync(dbContext, actor, tenantId, bank, command, token); }, cancellationToken); } protected Task> BatchCreateNodesCoreAsync( PlatformAdminActor actor, BatchCreatePlatformQuestionBankNodesCommand command, CancellationToken cancellationToken = default) { return ExecuteAsync>(actor, "批量创建章节或试卷", async (_, dbContext, tenantId, token) => { if (command.NodeType is not ContentNodeType.Chapter and not ContentNodeType.Paper) throw Error("批量创建仅支持章节或试卷。", "node_batch_type_invalid"); var bank = await RequireBankWithEntryAsync(dbContext, tenantId, command.QuestionBankId, token); var names = command.Names.Select(item => item.Trim()).Where(item => item.Length > 0) .Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); if (names.Length is 0 or > 100) throw Error("请提供 1 至 100 个不重复的名称。", "node_batch_names_invalid"); var result = new List(); var order = 0; foreach (var name in names) result.Add(await UpsertNodeCoreAsync(dbContext, actor, tenantId, bank, new UpsertPlatformQuestionBankNodeCommand( null, command.QuestionBankId, command.ParentId, null, name, command.NodeType, order++, true, JsonDefaults.Object()), token)); return result; }, cancellationToken); } protected Task ArchiveNodeCoreAsync( PlatformAdminActor actor, Guid nodeId, CancellationToken cancellationToken = default) { return ExecuteAsync(actor, "归档公共题库内容节点", async (_, dbContext, tenantId, token) => { var node = await dbContext.ContentNodes.SingleOrDefaultAsync( item => item.TenantId == tenantId && item.Id == nodeId, token) ?? throw Error("内容节点不存在。", "question_bank_node_not_found"); if (await dbContext.ContentNodes.AnyAsync( item => item.TenantId == tenantId && item.ParentId == nodeId && item.IsActive, token)) throw Error("该节点仍有启用的下级节点,不能归档。", "question_bank_node_has_children"); if (await dbContext.Questions.AnyAsync( item => item.TenantId == tenantId && item.ContentNodeId == nodeId && item.Status != QuestionStatus.Archived, token)) throw Error("该节点仍有未归档题目,不能归档。", "question_bank_node_has_questions"); node.IsActive = false; node.UpdatedBy = actor.UserId; var bankId = await dbContext.QuestionBanks .Where(item => item.TenantId == tenantId && item.ContentEntryId == node.EntryId).Select(item => item.Id) .SingleAsync(token); AddAudit(dbContext, actor, "platform.question_bank.node_archived", node.Id, new { node.Name }); await dbContext.SaveChangesAsync(token); return ToNodeItem(node, bankId, 0); }, cancellationToken); } private static async Task UpsertNodeCoreAsync(IPlatformQuestionBankAdministrationPersistence dbContext, PlatformAdminActor actor, Guid tenantId, QuestionBank bank, UpsertPlatformQuestionBankNodeCommand command, CancellationToken token) { if (string.IsNullOrWhiteSpace(command.Name)) throw Error("节点名称不能为空。", "question_bank_node_name_required"); ContentNode? parent = null; if (command.ParentId.HasValue) parent = await RequireNodeAsync(dbContext, tenantId, bank, command.ParentId.Value, token); var node = command.Id.HasValue ? await dbContext.ContentNodes.SingleOrDefaultAsync( item => item.TenantId == tenantId && item.Id == command.Id.Value && item.EntryId == bank.ContentEntryId, token) : null; if (command.Id.HasValue && node is null) throw Error("内容节点不存在。", "question_bank_node_not_found"); node ??= new ContentNode { TenantId = tenantId, EntryId = bank.ContentEntryId!.Value, CreatedBy = actor.UserId }; if (!command.Id.HasValue) dbContext.ContentNodes.Add(node); node.ParentId = parent?.Id; node.NodeKey = string.IsNullOrWhiteSpace(command.NodeKey) ? $"node-{node.Id:N}" : command.NodeKey.Trim(); node.Name = command.Name.Trim(); node.NodeType = command.NodeType; node.Depth = parent is null ? 0 : parent.Depth + 1; node.Path = parent is null ? $"n{node.Id:N}" : $"{parent.Path}.n{node.Id:N}"; node.SortOrder = command.SortOrder; node.IsActive = true; node.IsSelectable = command.IsSelectable; node.IsLeaf = command.NodeType is ContentNodeType.Chapter or ContentNodeType.Paper; node.Metadata = ObjectOrDefault(command.Metadata); node.UpdatedBy = actor.UserId; AddAudit(dbContext, actor, "platform.question_bank.node_saved", node.Id, new { bankId = bank.Id, node.Name, node.NodeType }); await dbContext.SaveChangesAsync(token); return ToNodeItem(node, bank.Id, 0); } }