feat: 日志内容搜索、设置分类Tabs、日报推送、删除合并/条件清理/无分片清理
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user