266 lines
13 KiB
C#
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)));
|
|
}
|
|
}
|