Files
tiku-backend.net/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Nodes/PlatformQuestionBankNodeFoundation.cs
xiong 33375a38d7
Some checks failed
ci / release-gate (push) Has been cancelled
refactor(architecture): harden module boundaries
2026-08-04 12:10:36 +08:00

152 lines
8.0 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<IReadOnlyCollection<PlatformQuestionBankNodeItem>> GetNodesCoreAsync(
PlatformAdminActor actor,
Guid bankId,
CancellationToken cancellationToken = default)
{
return ExecuteAsync<IReadOnlyCollection<PlatformQuestionBankNodeItem>>(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<PlatformQuestionBankNodeItem> 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<IReadOnlyCollection<PlatformQuestionBankNodeItem>> BatchCreateNodesCoreAsync(
PlatformAdminActor actor,
BatchCreatePlatformQuestionBankNodesCommand command,
CancellationToken cancellationToken = default)
{
return ExecuteAsync<IReadOnlyCollection<PlatformQuestionBankNodeItem>>(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<PlatformQuestionBankNodeItem>();
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<PlatformQuestionBankNodeItem> 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<PlatformQuestionBankNodeItem> 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);
}
}