Files
tiku-backend.net/Tiku.Infrastructure/TenantAdmin/Classes/TenantClassService.cs

266 lines
13 KiB
C#

using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
using Tiku.Domain.Catalog;
using Tiku.Domain.Common;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Security;
namespace Tiku.Infrastructure.TenantAdmin;
internal sealed class TenantClassService(TenantAdminServiceDependencies dependencies)
: TenantAdminServiceBase(dependencies), ITenantClassService
{
public async Task<TenantAdminClassList> GetClassesAsync(
TenantAdminActor actor,
TenantAdminClassFilter filter,
CancellationToken cancellationToken = default)
{
var scope = await RequireDataScopeAsync(actor, cancellationToken);
var regionIds = scope.RegionIds.ToArray();
var classIds = scope.ClassIds.ToArray();
var query = dbContext.TenantClasses.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId)
.ApplyDataScope(
scope,
item => item.CreatedBy == actor.UserId,
item => classIds.Contains(item.Id) ||
(item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value)));
if (filter.RegionId.HasValue) query = query.Where(item => item.RegionId == filter.RegionId.Value);
if (!string.IsNullOrWhiteSpace(filter.Status))
query = query.Where(item =>
item.Status == ParseEnum<TenantRecordStatus>(filter.Status, "invalid_class_status"));
else
query = query.Where(item => item.Status != TenantRecordStatus.Archived);
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(item =>
item.Name.Contains(keyword) ||
(item.Code != null && item.Code.Contains(keyword)));
}
var items = await query
.OrderBy(item => item.SortOrder)
.ThenByDescending(item => item.CreatedAt)
.Take(ResolveLimit(filter.Limit))
.Select(item => new
{
Class = item,
RegionName = dbContext.Regions
.Where(region => region.TenantId == actor.TenantId && region.Id == item.RegionId)
.Select(region => region.Name)
.FirstOrDefault(),
StudentCount = dbContext.TenantClassMembers.Count(member =>
member.TenantId == actor.TenantId &&
member.ClassId == item.Id &&
member.Status == TenantClassMemberStatus.Active &&
member.MemberType == TenantClassMemberType.Student),
StaffCount = dbContext.TenantClassMembers.Count(member =>
member.TenantId == actor.TenantId &&
member.ClassId == item.Id &&
member.Status == TenantClassMemberStatus.Active &&
member.MemberType != TenantClassMemberType.Student)
})
.ToArrayAsync(cancellationToken);
return new TenantAdminClassList(
items.Select(item => ToClassItem(item.Class, item.RegionName, item.StudentCount, item.StaffCount))
.ToArray(),
scope.Mode != DataScopeMode.All);
}
public async Task<ContentManagementResult<TenantAdminClassItem>> UpsertClassAsync(
TenantAdminActor actor,
UpsertTenantAdminClassCommand command,
CancellationToken cancellationToken = default)
{
var scope = await RequireDataScopeAsync(actor, cancellationToken);
ArgumentException.ThrowIfNullOrWhiteSpace(command.Name);
await AssertReferenceAsync<Region>(actor.TenantId, command.RegionId, "region_not_found", cancellationToken);
var item = await ResolveTenantEntityAsync(dbContext.TenantClasses, actor.TenantId, command.Id, command.LegacyId,
cancellationToken);
var isNew = item is null;
if (item is not null && !scope.AllowsResource(actor.UserId, item.CreatedBy, item.RegionId, item.Id))
throw new TenantAdminDirectException("Class was not found.", "class_not_found");
if (item is null && !scope.AllowsResource(actor.UserId, actor.UserId, command.RegionId, command.Id))
throw new TenantAdminDirectException("Class was not found.", "class_not_found");
item ??= new TenantClass
{ Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId, CreatedBy = actor.UserId };
item.RegionId = command.RegionId;
item.LegacyId = Normalize(command.LegacyId);
item.Code = Normalize(command.Code);
item.Name = command.Name.Trim();
item.Description = Normalize(command.Description);
item.Status = ParseEnum(command.Status, TenantRecordStatus.Active, "invalid_class_status");
item.SortOrder = command.Order ?? item.SortOrder;
item.Metadata = JsonObjectOrDefault(command.Metadata);
item.UpdatedBy = actor.UserId;
if (isNew) dbContext.TenantClasses.Add(item);
await AddAuditAsync(actor, "tenant.class.upserted", "tenant_classes", item.Id, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
var regionName = item.RegionId.HasValue
? await dbContext.Regions
.Where(region => region.TenantId == actor.TenantId && region.Id == item.RegionId.Value)
.Select(region => region.Name)
.FirstOrDefaultAsync(cancellationToken)
: null;
return new ContentManagementResult<TenantAdminClassItem>(ToClassItem(item, regionName, 0, 0));
}
public async Task<ContentManagementResult<TenantAdminClassItem>> DisableClassAsync(
TenantAdminActor actor,
Guid classId,
CancellationToken cancellationToken = default)
{
var scope = await RequireDataScopeAsync(actor, cancellationToken);
var regionIds = scope.RegionIds.ToArray();
var classIds = scope.ClassIds.ToArray();
var item = await dbContext.TenantClasses
.Where(entity => entity.TenantId == actor.TenantId && entity.Id == classId)
.ApplyDataScope(
scope,
entity => entity.CreatedBy == actor.UserId,
entity => classIds.Contains(entity.Id) ||
(entity.RegionId.HasValue && regionIds.Contains(entity.RegionId.Value)))
.FirstOrDefaultAsync(cancellationToken);
if (item is null) throw new TenantAdminDirectException("Class was not found.", "class_not_found");
item.Status = TenantRecordStatus.Disabled;
item.UpdatedBy = actor.UserId;
await AddAuditAsync(actor, "tenant.class.disabled", "tenant_classes", item.Id, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<TenantAdminClassItem>(ToClassItem(item, null, 0, 0));
}
public async Task<CatalogList<TenantAdminClassMemberItem>> GetClassMembersAsync(
TenantAdminActor actor,
TenantAdminClassMemberFilter filter,
CancellationToken cancellationToken = default)
{
var scope = await RequireDataScopeAsync(actor, cancellationToken);
await AssertClassAsync(actor, scope, filter.ClassId, cancellationToken);
var classIds = scope.ClassIds.ToArray();
var regionIds = scope.RegionIds.ToArray();
var query = dbContext.TenantClassMembers.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId && item.ClassId == filter.ClassId)
.ApplyDataScope(
scope,
item => item.UserId == actor.UserId || item.CreatedBy == actor.UserId,
item => classIds.Contains(item.ClassId) || dbContext.TenantClasses.Any(tenantClass =>
tenantClass.TenantId == actor.TenantId &&
tenantClass.Id == item.ClassId &&
tenantClass.RegionId.HasValue &&
regionIds.Contains(tenantClass.RegionId.Value)));
if (!string.IsNullOrWhiteSpace(filter.MemberType))
query = query.Where(item =>
item.MemberType == ParseEnum<TenantClassMemberType>(filter.MemberType, "invalid_class_member_type"));
query = query.Where(item =>
item.Status == ParseEnum(filter.Status, TenantClassMemberStatus.Active, "invalid_class_member_status"));
var items = await query
.OrderBy(item => item.MemberType == TenantClassMemberType.HeadTeacher ? 0 :
item.MemberType == TenantClassMemberType.Teacher ? 1 :
item.MemberType == TenantClassMemberType.Assistant ? 2 : 9)
.ThenBy(item => item.JoinedAt)
.Take(ResolveLimit(filter.Limit))
.Join(
dbContext.Users.AsNoTracking(),
member => member.UserId,
user => user.Id,
(member, user) => ToClassMemberItem(member, ToUserSummary(user)))
.ToArrayAsync(cancellationToken);
return new CatalogList<TenantAdminClassMemberItem>(items);
}
public async Task<ContentManagementResult<TenantAdminClassMemberItem>> UpsertClassMemberAsync(
TenantAdminActor actor,
UpsertTenantAdminClassMemberCommand command,
CancellationToken cancellationToken = default)
{
var scope = await RequireDataScopeAsync(actor, cancellationToken);
await AssertClassAsync(actor, scope, command.ClassId, cancellationToken);
await using var transaction = dbContext.Database.CurrentTransaction is null
? await dbContext.Database.BeginTransactionAsync(cancellationToken)
: null;
var memberType = ParseEnum(command.MemberType, TenantClassMemberType.Student, "invalid_class_member_type");
var status = ParseEnum(command.Status, TenantClassMemberStatus.Active, "invalid_class_member_status");
var user = await ResolveUserAsync(command.User,
memberType == TenantClassMemberType.Student ? "student" : "teacher", cancellationToken);
await EnsureMembershipAsync(actor.TenantId, user.Id,
memberType == TenantClassMemberType.Student ? TenantRole.Student : TenantRole.Teacher, cancellationToken);
if (memberType == TenantClassMemberType.Student)
await EnsureStudentProfileAsync(actor.TenantId, user.Id, null, null, null, null, JsonDefaults.Object(),
JsonDefaults.Object(), JsonDefaults.Object(), cancellationToken);
var item = await dbContext.TenantClassMembers.FirstOrDefaultAsync(member =>
member.TenantId == actor.TenantId &&
member.ClassId == command.ClassId &&
member.UserId == user.Id &&
member.MemberType == memberType,
cancellationToken);
var isNew = item is null;
item ??= new TenantClassMember
{
TenantId = actor.TenantId,
ClassId = command.ClassId,
UserId = user.Id,
MemberType = memberType,
CreatedBy = actor.UserId
};
item.Status = status;
item.LeftAt = status is TenantClassMemberStatus.Active ? null : DateTimeOffset.UtcNow;
item.Metadata = JsonObjectOrDefault(command.Metadata);
item.UpdatedBy = actor.UserId;
if (isNew) dbContext.TenantClassMembers.Add(item);
await AddAuditAsync(actor, "tenant.class_member.upserted", "tenant_class_members", item.Id, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
if (transaction is not null) await transaction.CommitAsync(cancellationToken);
return new ContentManagementResult<TenantAdminClassMemberItem>(ToClassMemberItem(item, ToUserSummary(user)));
}
public async Task<ContentManagementResult<TenantAdminClassMemberItem>> RemoveClassMemberAsync(
TenantAdminActor actor,
Guid classMemberId,
CancellationToken cancellationToken = default)
{
var scope = await RequireDataScopeAsync(actor, cancellationToken);
var classIds = scope.ClassIds.ToArray();
var regionIds = scope.RegionIds.ToArray();
var item = await dbContext.TenantClassMembers
.Where(member => member.TenantId == actor.TenantId && member.Id == classMemberId)
.ApplyDataScope(
scope,
member => member.UserId == actor.UserId || member.CreatedBy == actor.UserId,
member => classIds.Contains(member.ClassId) || dbContext.TenantClasses.Any(tenantClass =>
tenantClass.TenantId == actor.TenantId &&
tenantClass.Id == member.ClassId &&
tenantClass.RegionId.HasValue &&
regionIds.Contains(tenantClass.RegionId.Value)))
.FirstOrDefaultAsync(cancellationToken);
if (item is null) throw new TenantAdminDirectException("Class member was not found.", "class_member_not_found");
var user = await dbContext.Users.SingleAsync(user => user.Id == item.UserId, cancellationToken);
item.Status = TenantClassMemberStatus.Removed;
item.LeftAt = DateTimeOffset.UtcNow;
item.UpdatedBy = actor.UserId;
await AddAuditAsync(actor, "tenant.class_member.removed", "tenant_class_members", item.Id, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<TenantAdminClassMemberItem>(ToClassMemberItem(item, ToUserSummary(user)));
}
}