using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Tiku.Application.Assets; using Tiku.Application.PlatformAdmin.Operations; using Tiku.Application.Security; using Tiku.Application.Storage; using Tiku.Domain.Operations; using Tiku.Domain.Platform; using Tiku.Infrastructure.Persistence; using Tiku.Infrastructure.Storage; namespace Tiku.Infrastructure.PlatformAdmin.Operations; internal sealed class PlatformOperationsQueryService( TikuDbContext dbContext, IRedisSecurityStore redisSecurityStore, IAssetSecurityScanner assetSecurityScanner, IObjectStorageService objectStorageService, IOptions aliyunOssOptions) : IPlatformOperationsQueryService { public async Task GetHealthAsync(CancellationToken cancellationToken = default) { var database = await dbContext.Database.CanConnectAsync(cancellationToken); var redis = !redisSecurityStore.IsConfigured || await redisSecurityStore.PingAsync(cancellationToken); var clamAv = await assetSecurityScanner.CheckHealthAsync(cancellationToken); var storageProvider = objectStorageService.ConfiguredDefaultProvider(); var storageConfigured = storageProvider switch { ObjectStorageProviders.AliyunOss => aliyunOssOptions.Value.IsConfigured, ObjectStorageProviders.LocalDev => true, _ => false }; var heartbeat = await dbContext.WorkerHeartbeats.AsNoTracking() .MaxAsync(item => (DateTimeOffset?)item.LastHeartbeatAt, cancellationToken); var workerReady = heartbeat >= DateTimeOffset.UtcNow.AddMinutes(-2); return new PlatformDependencyHealth( database && redis && clamAv && workerReady && storageConfigured, database, redisSecurityStore.IsConfigured, redis, workerReady, heartbeat, clamAv, storageProvider, storageConfigured, DateTimeOffset.UtcNow); } public async Task> GetWorkersAsync( CancellationToken cancellationToken = default) { var staleBefore = DateTimeOffset.UtcNow.AddMinutes(-2); return await dbContext.WorkerHeartbeats.AsNoTracking() .OrderBy(item => item.WorkerId) .ThenBy(item => item.Processor) .Select(item => new PlatformWorkerState( item.WorkerId, item.Processor, item.StartedAt, item.LastHeartbeatAt, item.LastIterationStartedAt, item.LastIterationCompletedAt, item.LastSucceededAt, item.LastError, item.IsRunning, item.LastHeartbeatAt < staleBefore)) .ToArrayAsync(cancellationToken); } public async Task GetJobMetricsAsync(CancellationToken cancellationToken = default) { var now = DateTimeOffset.UtcNow; var counts = await dbContext.BackgroundJobs.AsNoTracking() .GroupBy(item => item.Status) .Select(group => new PlatformMetricCount(group.Key.ToString(), group.Count())) .ToArrayAsync(cancellationToken); var oldest = await dbContext.BackgroundJobs.AsNoTracking() .Where(item => item.Status == BackgroundJobStatus.Pending) .MinAsync(item => (DateTimeOffset?)item.CreatedAt, cancellationToken); var expired = await dbContext.BackgroundJobs.AsNoTracking() .CountAsync(item => item.Status == BackgroundJobStatus.Processing && item.LockExpiresAt < now, cancellationToken); return new PlatformJobMetrics( counts, oldest, oldest.HasValue ? Math.Max(0, (now - oldest.Value).TotalSeconds) : 0, expired, now); } public async Task GetGovernanceMetricsAsync( CancellationToken cancellationToken = default) { var now = DateTimeOffset.UtcNow; var approvals = await dbContext.PlatformApprovalRequests.AsNoTracking() .GroupBy(item => item.Status) .Select(group => new PlatformMetricCount(group.Key.ToString(), group.Count())) .ToArrayAsync(cancellationToken); var expired = await dbContext.PlatformApprovalRequests.AsNoTracking() .CountAsync(item => item.Status == PlatformApprovalRequestStatus.Pending && item.ExpiresAt <= now, cancellationToken); var drafts = await dbContext.PlatformConfigurationVersions.AsNoTracking() .CountAsync(item => item.Status == PlatformConfigurationVersionStatus.Draft, cancellationToken); var notifications = await dbContext.PlatformNotificationDeliveries.AsNoTracking() .GroupBy(item => item.Status) .Select(group => new PlatformMetricCount(group.Key.ToString(), group.Count())) .ToArrayAsync(cancellationToken); return new PlatformGovernanceMetrics(approvals, expired, drafts, notifications, now); } }