152 lines
8.0 KiB
C#
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);
|
|
}
|
|
|
|
}
|