176 lines
8.7 KiB
C#
176 lines
8.7 KiB
C#
using System.Text.Json;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Tiku.Application.Catalog;
|
||
using Tiku.Application.Content;
|
||
using Tiku.Application.TenantAdmin;
|
||
using Tiku.Domain.Learning;
|
||
using Tiku.Domain.Tenancy;
|
||
using PointActivityClaimStatus = Tiku.Domain.Commerce.PointActivityClaimStatus;
|
||
using PointExchangeOrderStatus = Tiku.Domain.Commerce.PointExchangeOrderStatus;
|
||
|
||
namespace Tiku.Infrastructure.TenantAdmin;
|
||
|
||
public sealed partial class TenantAdminDirectService
|
||
{
|
||
public async Task<CatalogList<TenantSupervisionRuleItem>> GetSupervisionRulesAsync(
|
||
TenantAdminActor actor,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
await RequireDataScopeAsync(actor, cancellationToken);
|
||
return new CatalogList<TenantSupervisionRuleItem>(
|
||
await GetSupervisionRulesCoreAsync(actor.TenantId, cancellationToken));
|
||
}
|
||
|
||
public async Task<ContentManagementResult<TenantSupervisionRuleItem>> UpsertSupervisionRuleAsync(
|
||
TenantAdminActor actor,
|
||
UpsertTenantSupervisionRuleCommand command,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
await RequireDataScopeAsync(actor, cancellationToken);
|
||
var code = NormalizeCode(command.Code);
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(command.Title);
|
||
var rules = (await GetSupervisionRulesCoreAsync(actor.TenantId, cancellationToken)).ToList();
|
||
var item = new TenantSupervisionRuleItem(
|
||
code,
|
||
command.Title.Trim(),
|
||
command.Enabled,
|
||
command.DaysWithoutCheckIn,
|
||
command.MaxQuestionsAnsweredToday,
|
||
Normalize(command.FollowupType) ?? StudentFollowupType.Risk.ToString(),
|
||
Normalize(command.Priority) ?? StudentFollowupPriority.High.ToString(),
|
||
JsonObjectOrDefault(command.Metadata));
|
||
rules.RemoveAll(rule => string.Equals(rule.Code, code, StringComparison.Ordinal));
|
||
rules.Add(item);
|
||
await SaveSupervisionRulesCoreAsync(actor.TenantId, rules.OrderBy(rule => rule.Code).ToArray(),
|
||
cancellationToken);
|
||
await AddAuditAsync(actor, "tenant.supervision_rule.upserted", "tenant_settings", actor.TenantId,
|
||
cancellationToken);
|
||
await dbContext.SaveChangesAsync(cancellationToken);
|
||
return new ContentManagementResult<TenantSupervisionRuleItem>(item);
|
||
}
|
||
|
||
public async Task<TenantSupervisionPreview> PreviewSupervisionAsync(
|
||
TenantAdminActor actor,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
||
return new TenantSupervisionPreview(await BuildSupervisionRiskStudentsAsync(actor, scope, cancellationToken));
|
||
}
|
||
|
||
public async Task<TenantSupervisionGenerateResult> GenerateSupervisionFollowupsAsync(
|
||
TenantAdminActor actor,
|
||
TenantSupervisionGenerateCommand command,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
||
await AssertTenantMemberAsync(actor.TenantId, command.AssignedToUserId, cancellationToken);
|
||
var riskStudents = await BuildSupervisionRiskStudentsAsync(actor, scope, cancellationToken);
|
||
if (command.UserIds is { Count: > 0 })
|
||
{
|
||
var selected = command.UserIds.Where(id => id != Guid.Empty).Distinct().ToHashSet();
|
||
riskStudents = riskStudents.Where(item => selected.Contains(item.UserId)).ToArray();
|
||
}
|
||
|
||
var created = 0;
|
||
foreach (var student in riskStudents)
|
||
{
|
||
if (await dbContext.TenantStudentFollowups.AnyAsync(item =>
|
||
item.TenantId == actor.TenantId &&
|
||
item.StudentUserId == student.UserId &&
|
||
item.Status != StudentFollowupStatus.Done &&
|
||
item.FollowupType == StudentFollowupType.Risk,
|
||
cancellationToken))
|
||
continue;
|
||
|
||
dbContext.TenantStudentFollowups.Add(new TenantStudentFollowup
|
||
{
|
||
TenantId = actor.TenantId,
|
||
StudentUserId = student.UserId,
|
||
AssignedToUserId = command.AssignedToUserId,
|
||
Title = "学习风险督导",
|
||
Description = string.Join(";", student.Reasons),
|
||
FollowupType = StudentFollowupType.Risk,
|
||
Priority = StudentFollowupPriority.High,
|
||
Status = StudentFollowupStatus.Open,
|
||
DueAt = command.DueAt,
|
||
CreatedBy = actor.UserId,
|
||
UpdatedBy = actor.UserId,
|
||
Metadata = JsonSerializer.SerializeToElement(new { ruleCodes = student.RuleCodes })
|
||
});
|
||
created++;
|
||
}
|
||
|
||
await AddAuditAsync(actor, "tenant.supervision_followups.generated", "tenant_student_followups", actor.TenantId,
|
||
cancellationToken);
|
||
await dbContext.SaveChangesAsync(cancellationToken);
|
||
return new TenantSupervisionGenerateResult(created);
|
||
}
|
||
|
||
public async Task<TenantFollowupReport> GetFollowupReportAsync(
|
||
TenantAdminActor actor,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
await RequireDataScopeAsync(actor, cancellationToken);
|
||
var now = DateTimeOffset.UtcNow;
|
||
return new TenantFollowupReport(
|
||
await dbContext.TenantStudentFollowups.CountAsync(
|
||
item => item.TenantId == actor.TenantId && item.Status == StudentFollowupStatus.Open,
|
||
cancellationToken),
|
||
await dbContext.TenantStudentFollowups.CountAsync(
|
||
item => item.TenantId == actor.TenantId && item.Status == StudentFollowupStatus.InProgress,
|
||
cancellationToken),
|
||
await dbContext.TenantStudentFollowups.CountAsync(
|
||
item => item.TenantId == actor.TenantId && item.Status == StudentFollowupStatus.Done,
|
||
cancellationToken),
|
||
await dbContext.TenantStudentFollowups.CountAsync(item =>
|
||
item.TenantId == actor.TenantId &&
|
||
item.DueAt.HasValue &&
|
||
item.DueAt < now &&
|
||
item.Status != StudentFollowupStatus.Done &&
|
||
item.Status != StudentFollowupStatus.Cancelled,
|
||
cancellationToken));
|
||
}
|
||
|
||
public async Task<TenantFeedbackReport> GetFeedbackReportAsync(
|
||
TenantAdminActor actor,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
await RequireDataScopeAsync(actor, cancellationToken);
|
||
return new TenantFeedbackReport(
|
||
await dbContext.Reports.CountAsync(
|
||
item => item.TenantId == actor.TenantId && item.Status == ReportStatus.Pending, cancellationToken),
|
||
await dbContext.Reports.CountAsync(
|
||
item => item.TenantId == actor.TenantId && item.Status == ReportStatus.Accepted, cancellationToken),
|
||
await dbContext.Reports.CountAsync(
|
||
item => item.TenantId == actor.TenantId && item.Status == ReportStatus.Rejected, cancellationToken),
|
||
await dbContext.Reports.CountAsync(
|
||
item => item.TenantId == actor.TenantId && item.Status == ReportStatus.Resolved, cancellationToken),
|
||
await dbContext.Reports.CountAsync(
|
||
item => item.TenantId == actor.TenantId && item.Status == ReportStatus.Closed, cancellationToken));
|
||
}
|
||
|
||
public async Task<TenantPointRiskReport> GetPointRiskReportAsync(
|
||
TenantAdminActor actor,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
await RequireDataScopeAsync(actor, cancellationToken);
|
||
var negativeScoreUsers = await dbContext.Users
|
||
.Where(user => user.Score < 0 &&
|
||
dbContext.TenantMemberships.Any(member =>
|
||
member.TenantId == actor.TenantId &&
|
||
member.UserId == user.Id &&
|
||
member.Status == MembershipStatus.Active))
|
||
.CountAsync(cancellationToken);
|
||
var since = DateTimeOffset.UtcNow.AddDays(-7);
|
||
var highClaimUsers = await dbContext.PointActivityClaims
|
||
.Where(item => item.TenantId == actor.TenantId && item.Status == PointActivityClaimStatus.Claimed &&
|
||
item.ClaimedAt >= since)
|
||
.GroupBy(item => item.UserId)
|
||
.Where(group => group.Sum(item => item.Points) >= 1000)
|
||
.CountAsync(cancellationToken);
|
||
var cancelledExchangeOrders = await dbContext.PointExchangeOrders.CountAsync(
|
||
item => item.TenantId == actor.TenantId && item.Status == PointExchangeOrderStatus.Cancelled,
|
||
cancellationToken);
|
||
return new TenantPointRiskReport(negativeScoreUsers, highClaimUsers, cancelledExchangeOrders);
|
||
}
|
||
} |