feat: 日志内容搜索、设置分类Tabs、日报推送、删除合并/条件清理/无分片清理

This commit is contained in:
2026-04-28 10:56:16 +08:00
parent 5a635a40f9
commit fcfa94dee3
12 changed files with 319 additions and 29 deletions
@@ -20,6 +20,7 @@ public interface ISystemLogService
Guid? recordSessionId = null,
Guid? recordTaskId = null,
SystemLogLevel? level = null,
string? content = null,
int take = 200,
CancellationToken cancellationToken = default);
}
@@ -96,10 +96,13 @@ public interface ISystemLogRepository
Guid? recordSessionId = null,
Guid? recordTaskId = null,
SystemLogLevel? level = null,
string? content = null,
int take = 200,
CancellationToken cancellationToken = default);
void RemoveRange(IEnumerable<SystemLogEntry> entries);
Task<IReadOnlyList<Guid>> ListSessionIdsWithoutTasksAsync(CancellationToken cancellationToken = default);
}
public interface IUserAccountRepository
@@ -283,6 +283,29 @@ public sealed class RecordSessionService
cancellationToken);
}
public async Task<DeleteCompletedRecordTasksResultDto> DeleteEmptyAsync(
bool deleteFiles,
CancellationToken cancellationToken = default)
{
await _stoppedOrphanRecordSessionCleanupService.CleanupAsync(cancellationToken: cancellationToken);
await ReconcileActiveSessionsAsync(null, cancellationToken);
var emptySessionIds = await _systemLogRepository.ListSessionIdsWithoutTasksAsync(cancellationToken);
if (emptySessionIds.Count == 0)
{
return RecordService.CreateEmptyDeleteResult();
}
return await DeleteAsync(
new DeleteRecordSessionsRequest
{
SessionIds = emptySessionIds.ToArray(),
DeleteFiles = deleteFiles
},
cancellationToken);
}
private async Task ReconcileActiveSessionsAsync(Guid? liveRoomId, CancellationToken cancellationToken)
{
var sessions = await _recordSessionRepository.ListAsync(liveRoomId, cancellationToken);
@@ -64,6 +64,7 @@ public sealed class SystemLogService : ISystemLogService
Guid? recordSessionId = null,
Guid? recordTaskId = null,
SystemLogLevel? level = null,
string? content = null,
int take = 200,
CancellationToken cancellationToken = default)
{
@@ -72,6 +73,7 @@ public sealed class SystemLogService : ISystemLogService
recordSessionId,
recordTaskId,
level,
content,
take,
cancellationToken);
return entries
@@ -271,6 +271,7 @@ public sealed class SystemLogRepository : ISystemLogRepository
Guid? recordSessionId = null,
Guid? recordTaskId = null,
SystemLogLevel? level = null,
string? content = null,
int take = 200,
CancellationToken cancellationToken = default)
{
@@ -296,6 +297,13 @@ public sealed class SystemLogRepository : ISystemLogRepository
query = query.Where(item => item.Level == level.Value);
}
if (!string.IsNullOrWhiteSpace(content))
{
query = query.Where(item =>
item.Message.Contains(content) ||
(item.Detail != null && item.Detail.Contains(content)));
}
var items = await query.ToListAsync(cancellationToken);
return items
.OrderByDescending(static item => item.CreatedAt)
@@ -304,6 +312,20 @@ public sealed class SystemLogRepository : ISystemLogRepository
}
public void RemoveRange(IEnumerable<SystemLogEntry> entries) => _dbContext.SystemLogEntries.RemoveRange(entries);
public async Task<IReadOnlyList<Guid>> ListSessionIdsWithoutTasksAsync(CancellationToken cancellationToken = default)
{
var sessionIdsWithTasks = await _dbContext.RecordTasks
.Select(item => item.RecordSessionId)
.Distinct()
.ToListAsync(cancellationToken);
var allSessionIds = await _dbContext.RecordSessions
.Select(item => item.Id)
.ToListAsync(cancellationToken);
return allSessionIds.Except(sessionIdsWithTasks).ToList();
}
}
public sealed class UserAccountRepository : IUserAccountRepository
@@ -22,7 +22,8 @@ public sealed class LogsController : ControllerBase
[FromQuery] Guid? recordSessionId,
[FromQuery] Guid? recordTaskId,
[FromQuery] SystemLogLevel? level,
[FromQuery] string? content,
[FromQuery] int take = 200,
CancellationToken cancellationToken = default) =>
Ok(await _systemLogService.ListAsync(liveRoomId, recordSessionId, recordTaskId, level, take, cancellationToken));
Ok(await _systemLogService.ListAsync(liveRoomId, recordSessionId, recordTaskId, level, content, take, cancellationToken));
}
@@ -87,6 +87,12 @@ public sealed class RecordSessionsController : ControllerBase
CancellationToken cancellationToken) =>
Ok(await _recordSessionService.DeleteMissingFilesAsync(request, cancellationToken));
[HttpPost("delete-empty")]
public async Task<ActionResult<DeleteCompletedRecordTasksResultDto>> DeleteEmpty(
[FromQuery] bool deleteFiles = false,
CancellationToken cancellationToken = default) =>
Ok(await _recordSessionService.DeleteEmptyAsync(deleteFiles, cancellationToken));
[HttpPost("{id:guid}/upload")]
public async Task<ActionResult<RecordArtifactUploadBatchResultDto>> Upload(Guid id, CancellationToken cancellationToken) =>
Ok(await _recordUploadService.UploadSessionAsync(id, cancellationToken));
@@ -1,4 +1,5 @@
using System.Globalization;
using LiveRecorder.Application.Abstractions.Notifications;
using LiveRecorder.Application.Models.Reports;
using LiveRecorder.Application.Services;
using Microsoft.AspNetCore.Mvc;
@@ -10,10 +11,17 @@ namespace LiveRecorder.WebApi.Controllers;
public sealed class ReportsController : ControllerBase
{
private readonly SessionAnalyticsService _sessionAnalyticsService;
private readonly IEmailNotificationService _emailNotificationService;
private readonly IWebhookNotificationService _webhookNotificationService;
public ReportsController(SessionAnalyticsService sessionAnalyticsService)
public ReportsController(
SessionAnalyticsService sessionAnalyticsService,
IEmailNotificationService emailNotificationService,
IWebhookNotificationService webhookNotificationService)
{
_sessionAnalyticsService = sessionAnalyticsService;
_emailNotificationService = emailNotificationService;
_webhookNotificationService = webhookNotificationService;
}
[HttpGet("daily")]
@@ -30,4 +38,43 @@ public sealed class ReportsController : ControllerBase
return Ok(await _sessionAnalyticsService.GetDailyReviewAsync(localDate, utcOffsetMinutes, cancellationToken));
}
[HttpPost("daily/push")]
public async Task<ActionResult<DailyReviewPushResultDto>> PushDaily(
[FromQuery] string? date,
[FromQuery] int utcOffsetMinutes = 0,
CancellationToken cancellationToken = default)
{
var reviewDate = DateOnly.FromDateTime(DateTime.Today.AddDays(-1));
if (!string.IsNullOrWhiteSpace(date) &&
DateOnly.TryParseExact(date, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedDate))
{
reviewDate = parsedDate;
}
var report = await _sessionAnalyticsService.GetDailyReviewAsync(reviewDate, utcOffsetMinutes, cancellationToken);
var s = report.Summary;
var summary = $"回顾日报 {reviewDate:yyyy-MM-dd}\n直播间: {s.ActiveLiveRoomCount}, 会话: {s.SessionCount}, 分片: {s.SegmentCount}, 录制时长: {s.TotalDurationSeconds / 3600.0:F1}h, 弹幕: {s.TotalDanmakuCount}";
var result = new DailyReviewPushResultDto();
try
{
await _webhookNotificationService.SendExceptionAsync(
"DailyReview",
summary,
System.Text.Json.JsonSerializer.Serialize(report),
cancellationToken: cancellationToken);
result.WebhookSent = true;
}
catch { }
result.Message = result.WebhookSent ? "日报已通过 Webhook 推送。" : "日报推送失败,请检查 Webhook 配置。";
return Ok(result);
}
}
public sealed class DailyReviewPushResultDto
{
public bool WebhookSent { get; set; }
public string Message { get; set; } = string.Empty;
}