Files
tiku-backend.net/Tiku.Infrastructure/PlatformAdmin/QuestionBanks/Catalog/PlatformQuestionBankCatalogFoundation.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

166 lines
7.7 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<PlatformQuestionBankItem>> GetBanksCoreAsync(
PlatformAdminActor actor,
PlatformQuestionBankFilter filter,
CancellationToken cancellationToken = default)
{
return ExecuteAsync<IReadOnlyCollection<PlatformQuestionBankItem>>(actor, "查询平台公共题库",
async (_, dbContext, tenantId, token) =>
{
var query = dbContext.QuestionBanks.AsNoTracking().Where(item => item.TenantId == tenantId);
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(item => item.Name.Contains(keyword));
}
if (!string.IsNullOrWhiteSpace(filter.Status) &&
!filter.Status.Equals("all", StringComparison.OrdinalIgnoreCase))
{
var status = ParseBankStatus(filter.Status);
query = query.Where(item => item.Status == status);
}
var banks = await query.OrderBy(item => item.Name).ThenBy(item => item.CreatedAt).ToArrayAsync(token);
var entryIds = banks.Where(item => item.ContentEntryId.HasValue)
.Select(item => item.ContentEntryId!.Value).ToArray();
var nodeCounts = await dbContext.ContentNodes.AsNoTracking()
.Where(item => item.TenantId == tenantId && entryIds.Contains(item.EntryId) && item.IsActive)
.GroupBy(item => item.EntryId)
.Select(group => new { EntryId = group.Key, Count = group.Count() })
.ToDictionaryAsync(item => item.EntryId, item => item.Count, token);
var questionCounts = await dbContext.Questions.AsNoTracking()
.Where(item =>
item.TenantId == tenantId && item.QuestionBankId.HasValue &&
item.Status != QuestionStatus.Archived)
.GroupBy(item => item.QuestionBankId!.Value)
.Select(group => new { BankId = group.Key, Count = group.Count() })
.ToDictionaryAsync(item => item.BankId, item => item.Count, token);
return banks.Select(item => ToBankItem(
item,
item.ContentEntryId.HasValue && nodeCounts.TryGetValue(item.ContentEntryId.Value, out var nodes)
? nodes
: 0,
questionCounts.TryGetValue(item.Id, out var questions) ? questions : 0)).ToArray();
}, cancellationToken);
}
protected Task<PlatformQuestionBankItem> UpsertBankCoreAsync(
PlatformAdminActor actor,
UpsertPlatformQuestionBankCommand command,
CancellationToken cancellationToken = default)
{
return ExecuteAsync(actor, "保存平台公共题库", async (_, dbContext, tenantId, token) =>
{
if (string.IsNullOrWhiteSpace(command.Name)) throw Error("题库名称不能为空。", "question_bank_name_required");
var bank = command.Id.HasValue
? await dbContext.QuestionBanks.SingleOrDefaultAsync(
item => item.TenantId == tenantId && item.Id == command.Id.Value, token)
: null;
if (command.Id.HasValue && bank is null) throw Error("公共题库不存在。", "question_bank_not_found");
ContentEntry? trackedEntry = null;
if (bank is null)
{
var entry = new ContentEntry
{
TenantId = tenantId,
EntryKey = $"public-question-bank-{Guid.NewGuid():N}",
Name = command.Name.Trim(),
EntryType = ContentEntryType.QuestionPractice,
Visibility = ContentVisibility.Public,
IsActive = true,
CreatedBy = actor.UserId,
UpdatedBy = actor.UserId
};
trackedEntry = entry;
bank = new QuestionBank
{
TenantId = tenantId,
ContentEntryId = entry.Id,
Status = QuestionBankStatus.Active
};
dbContext.ContentEntries.Add(entry);
dbContext.QuestionBanks.Add(bank);
}
else if (!bank.ContentEntryId.HasValue)
{
var entry = new ContentEntry
{
TenantId = tenantId,
EntryKey = $"public-question-bank-{bank.Id:N}",
Name = command.Name.Trim(),
EntryType = ContentEntryType.QuestionPractice,
Visibility = ContentVisibility.Public,
IsActive = true,
CreatedBy = actor.UserId,
UpdatedBy = actor.UserId
};
trackedEntry = entry;
dbContext.ContentEntries.Add(entry);
bank.ContentEntryId = entry.Id;
}
bank.Name = command.Name.Trim();
bank.Metadata = ObjectOrDefault(command.Metadata);
if (bank.ContentEntryId.HasValue)
{
var entry = trackedEntry ??
await dbContext.ContentEntries.SingleAsync(
item => item.TenantId == tenantId && item.Id == bank.ContentEntryId.Value, token);
entry.Name = bank.Name;
entry.UpdatedBy = actor.UserId;
}
AddAudit(dbContext, actor, "platform.question_bank.saved", bank.Id, new { bank.Name });
await dbContext.SaveChangesAsync(token);
return ToBankItem(bank, 0, 0);
}, cancellationToken);
}
protected Task<PlatformQuestionBankItem> ArchiveBankCoreAsync(
PlatformAdminActor actor,
Guid bankId,
CancellationToken cancellationToken = default)
{
return ExecuteAsync(actor, "归档平台公共题库", async (_, dbContext, tenantId, token) =>
{
var bank = await RequireBankAsync(dbContext, tenantId, bankId, token);
if (await dbContext.Questions.AnyAsync(
item => item.TenantId == tenantId && item.QuestionBankId == bankId &&
item.Status != QuestionStatus.Archived, token))
throw Error("题库中仍有未归档题目,不能归档题库。", "question_bank_not_empty");
if (bank.ContentEntryId.HasValue && await dbContext.ContentNodes.AnyAsync(
item => item.TenantId == tenantId && item.EntryId == bank.ContentEntryId && item.IsActive, token))
throw Error("题库中仍有启用的内容层级,不能归档题库。", "question_bank_nodes_not_archived");
bank.Status = QuestionBankStatus.Archived;
AddAudit(dbContext, actor, "platform.question_bank.archived", bank.Id, new { bank.Name });
await dbContext.SaveChangesAsync(token);
return ToBankItem(bank, 0, 0);
}, cancellationToken);
}
}