Files
tiku-backend.net/Tiku.Infrastructure/TenantAdmin/StudentEngagement/TenantStudentEngagementService.cs
xiong 8493f8f0ed
Some checks failed
ci / release-gate (push) Has been cancelled
style: format module migration files
2026-08-04 09:57:54 +08:00

215 lines
11 KiB
C#

using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.TenantAdmin;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Security;
namespace Tiku.Infrastructure.TenantAdmin;
internal sealed class TenantStudentEngagementService(TenantAdminServiceDependencies dependencies)
: TenantAdminServiceBase(dependencies), ITenantStudentEngagementService
{
public async Task<CatalogList<TenantAdminStudentNoteItem>> GetStudentNotesAsync(
TenantAdminActor actor,
TenantAdminStudentActivityFilter filter,
CancellationToken cancellationToken = default)
{
var scope = await RequireDataScopeAsync(actor, cancellationToken);
var regionIds = scope.RegionIds.ToArray();
var classIds = scope.ClassIds.ToArray();
var query = dbContext.TenantStudentNotes.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId)
.ApplyDataScope(
scope,
item => item.StudentUserId == actor.UserId || item.CreatedBy == actor.UserId,
item => dbContext.StudentProfiles.Any(profile =>
profile.TenantId == actor.TenantId &&
profile.UserId == item.StudentUserId &&
profile.RegionId.HasValue &&
regionIds.Contains(profile.RegionId.Value)) ||
dbContext.TenantClassMembers.Any(member =>
member.TenantId == actor.TenantId &&
member.UserId == item.StudentUserId &&
member.Status == TenantClassMemberStatus.Active &&
classIds.Contains(member.ClassId)));
if (filter.StudentUserId.HasValue)
query = query.Where(item => item.StudentUserId == filter.StudentUserId.Value);
var items = await query
.OrderByDescending(item => item.IsPinned)
.ThenByDescending(item => item.CreatedAt)
.Take(ResolveLimit(filter.Limit))
.Select(item => ToNoteItem(item))
.ToArrayAsync(cancellationToken);
return new CatalogList<TenantAdminStudentNoteItem>(items);
}
public async Task<ContentManagementResult<TenantAdminStudentNoteItem>> UpsertStudentNoteAsync(
TenantAdminActor actor,
UpsertTenantAdminStudentNoteCommand command,
CancellationToken cancellationToken = default)
{
var scope = await RequireDataScopeAsync(actor, cancellationToken);
ArgumentException.ThrowIfNullOrWhiteSpace(command.Content);
await AssertStudentAsync(actor, scope, command.StudentUserId, cancellationToken);
TenantStudentNote? item = null;
if (command.Id.HasValue)
{
var regionIds = scope.RegionIds.ToArray();
var classIds = scope.ClassIds.ToArray();
item = await dbContext.TenantStudentNotes
.Where(note => note.TenantId == actor.TenantId && note.Id == command.Id.Value)
.ApplyDataScope(
scope,
note => note.StudentUserId == actor.UserId || note.CreatedBy == actor.UserId,
note => dbContext.StudentProfiles.Any(profile =>
profile.TenantId == actor.TenantId &&
profile.UserId == note.StudentUserId &&
profile.RegionId.HasValue &&
regionIds.Contains(profile.RegionId.Value)) ||
dbContext.TenantClassMembers.Any(member =>
member.TenantId == actor.TenantId &&
member.UserId == note.StudentUserId &&
member.Status == TenantClassMemberStatus.Active &&
classIds.Contains(member.ClassId)))
.FirstOrDefaultAsync(cancellationToken);
if (item is null)
throw new TenantAdminDirectException("Student note was not found.", "student_note_not_found");
}
var isNew = item is null;
item ??= new TenantStudentNote
{
Id = command.Id ?? Guid.NewGuid(),
TenantId = actor.TenantId,
StudentUserId = command.StudentUserId,
CreatedBy = actor.UserId
};
item.NoteType = ParseEnum(command.NoteType, StudentNoteType.General, "invalid_student_note_type");
item.Content = command.Content.Trim();
item.Visibility = ParseEnum(command.Visibility, StudentNoteVisibility.TenantStaff,
"invalid_student_note_visibility");
item.IsPinned = command.IsPinned ?? item.IsPinned;
item.Metadata = JsonObjectOrDefault(command.Metadata);
item.UpdatedBy = actor.UserId;
if (isNew) dbContext.TenantStudentNotes.Add(item);
await AddAuditAsync(actor, "tenant.student_note.upserted", "tenant_student_notes", item.Id, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<TenantAdminStudentNoteItem>(ToNoteItem(item));
}
public async Task<CatalogList<TenantAdminStudentFollowupItem>> GetStudentFollowupsAsync(
TenantAdminActor actor,
TenantAdminStudentActivityFilter filter,
CancellationToken cancellationToken = default)
{
var scope = await RequireDataScopeAsync(actor, cancellationToken);
var regionIds = scope.RegionIds.ToArray();
var classIds = scope.ClassIds.ToArray();
var query = dbContext.TenantStudentFollowups.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId)
.ApplyDataScope(
scope,
item => item.StudentUserId == actor.UserId || item.AssignedToUserId == actor.UserId ||
item.CreatedBy == actor.UserId,
item => (item.ClassId.HasValue && classIds.Contains(item.ClassId.Value)) ||
dbContext.StudentProfiles.Any(profile =>
profile.TenantId == actor.TenantId &&
profile.UserId == item.StudentUserId &&
profile.RegionId.HasValue &&
regionIds.Contains(profile.RegionId.Value)) ||
dbContext.TenantClassMembers.Any(member =>
member.TenantId == actor.TenantId &&
member.UserId == item.StudentUserId &&
member.Status == TenantClassMemberStatus.Active &&
classIds.Contains(member.ClassId)));
if (filter.StudentUserId.HasValue)
query = query.Where(item => item.StudentUserId == filter.StudentUserId.Value);
if (!string.IsNullOrWhiteSpace(filter.Status))
query = query.Where(item =>
item.Status == ParseEnum<StudentFollowupStatus>(filter.Status, "invalid_student_followup_status"));
var items = await query
.OrderBy(item =>
item.Status == StudentFollowupStatus.Done || item.Status == StudentFollowupStatus.Cancelled)
.ThenBy(item => item.DueAt ?? DateTimeOffset.MaxValue)
.ThenByDescending(item => item.CreatedAt)
.Take(ResolveLimit(filter.Limit))
.Select(item => ToFollowupItem(item))
.ToArrayAsync(cancellationToken);
return new CatalogList<TenantAdminStudentFollowupItem>(items);
}
public async Task<ContentManagementResult<TenantAdminStudentFollowupItem>> UpsertStudentFollowupAsync(
TenantAdminActor actor,
UpsertTenantAdminStudentFollowupCommand command,
CancellationToken cancellationToken = default)
{
var scope = await RequireDataScopeAsync(actor, cancellationToken);
ArgumentException.ThrowIfNullOrWhiteSpace(command.Title);
await AssertStudentAsync(actor, scope, command.StudentUserId, cancellationToken);
await AssertClassAsync(actor, scope, command.ClassId, cancellationToken);
await AssertTenantMemberAsync(actor.TenantId, command.AssignedToUserId, cancellationToken);
TenantStudentFollowup? item = null;
if (command.Id.HasValue)
{
var regionIds = scope.RegionIds.ToArray();
var classIds = scope.ClassIds.ToArray();
item = await dbContext.TenantStudentFollowups
.Where(followup => followup.TenantId == actor.TenantId && followup.Id == command.Id.Value)
.ApplyDataScope(
scope,
followup => followup.StudentUserId == actor.UserId ||
followup.AssignedToUserId == actor.UserId ||
followup.CreatedBy == actor.UserId,
followup => (followup.ClassId.HasValue && classIds.Contains(followup.ClassId.Value)) ||
dbContext.StudentProfiles.Any(profile =>
profile.TenantId == actor.TenantId &&
profile.UserId == followup.StudentUserId &&
profile.RegionId.HasValue &&
regionIds.Contains(profile.RegionId.Value)) ||
dbContext.TenantClassMembers.Any(member =>
member.TenantId == actor.TenantId &&
member.UserId == followup.StudentUserId &&
member.Status == TenantClassMemberStatus.Active &&
classIds.Contains(member.ClassId)))
.FirstOrDefaultAsync(cancellationToken);
if (item is null)
throw new TenantAdminDirectException("Student followup was not found.", "student_followup_not_found");
}
var isNew = item is null;
item ??= new TenantStudentFollowup
{
Id = command.Id ?? Guid.NewGuid(),
TenantId = actor.TenantId,
StudentUserId = command.StudentUserId,
CreatedBy = actor.UserId
};
item.AssignedToUserId = command.AssignedToUserId;
item.ClassId = command.ClassId;
item.Title = command.Title.Trim();
item.Description = Normalize(command.Description);
item.FollowupType = ParseEnum(command.FollowupType, StudentFollowupType.Learning,
"invalid_student_followup_type");
item.Priority = ParseEnum(command.Priority, StudentFollowupPriority.Normal,
"invalid_student_followup_priority");
item.Status = ParseEnum(command.Status, StudentFollowupStatus.Open, "invalid_student_followup_status");
item.DueAt = command.DueAt;
item.CompletedAt = item.Status == StudentFollowupStatus.Done ? DateTimeOffset.UtcNow : null;
item.CompletedBy = item.Status == StudentFollowupStatus.Done ? actor.UserId : null;
item.Metadata = JsonObjectOrDefault(command.Metadata);
item.UpdatedBy = actor.UserId;
if (isNew) dbContext.TenantStudentFollowups.Add(item);
await AddAuditAsync(actor, "tenant.student_followup.upserted", "tenant_student_followups", item.Id,
cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<TenantAdminStudentFollowupItem>(ToFollowupItem(item));
}
}