Files
tiku-backend.net/Tiku.Infrastructure/TenantAdmin/Supervision/TenantAdminDirectService.Supervision.cs
xiong c497a3ca8d
Some checks failed
ci / release-gate (push) Has been cancelled
清理代码
2026-08-03 12:31:39 +08:00

176 lines
8.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}