This commit is contained in:
2025-11-09 02:08:31 +07:00
parent 1ed58d1a98
commit 7dba29c66f
57 changed files with 8362 additions and 359 deletions

View File

@@ -0,0 +1,288 @@
#nullable enable
using System.Text.Json;
using Managing.Api.Models.Responses;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Shared;
using Managing.Domain.Backtests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using static Managing.Common.Enums;
namespace Managing.Api.Controllers;
/// <summary>
/// Controller for managing job operations.
/// Provides endpoints for querying job status and progress.
/// Requires admin authorization for access.
/// </summary>
[ApiController]
[Authorize]
[Route("[controller]")]
[Produces("application/json")]
public class JobController : BaseController
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IAdminConfigurationService _adminService;
private readonly ILogger<JobController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="JobController"/> class.
/// </summary>
/// <param name="userService">The service for user management.</param>
/// <param name="serviceScopeFactory">The service scope factory for creating scoped services.</param>
/// <param name="adminService">The admin configuration service for authorization checks.</param>
/// <param name="logger">The logger instance.</param>
public JobController(
IUserService userService,
IServiceScopeFactory serviceScopeFactory,
IAdminConfigurationService adminService,
ILogger<JobController> logger) : base(userService)
{
_serviceScopeFactory = serviceScopeFactory;
_adminService = adminService;
_logger = logger;
}
/// <summary>
/// Checks if the current user is an admin
/// </summary>
private async Task<bool> IsUserAdmin()
{
try
{
var user = await GetUser();
return await _adminService.IsUserAdminAsync(user.Name);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking if user is admin");
return false;
}
}
/// <summary>
/// Gets the status of a job by its ID.
/// Admin only endpoint.
/// </summary>
/// <param name="jobId">The job ID to query</param>
/// <returns>The job status and result if completed</returns>
[HttpGet("{jobId}")]
public async Task<ActionResult<BacktestJobStatusResponse>> GetJobStatus(string jobId)
{
if (!await IsUserAdmin())
{
_logger.LogWarning("Non-admin user attempted to access job status endpoint");
return StatusCode(403, new { error = "Only admin users can access job status" });
}
if (!Guid.TryParse(jobId, out var jobGuid))
{
return BadRequest("Invalid job ID format. Must be a valid GUID.");
}
using var serviceScope = _serviceScopeFactory.CreateScope();
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
var job = await jobRepository.GetByIdAsync(jobGuid);
if (job == null)
{
return NotFound($"Job with ID {jobId} not found.");
}
var response = new BacktestJobStatusResponse
{
JobId = job.Id,
Status = job.Status.ToString(),
ProgressPercentage = job.ProgressPercentage,
CreatedAt = job.CreatedAt,
StartedAt = job.StartedAt,
CompletedAt = job.CompletedAt,
ErrorMessage = job.ErrorMessage,
Result = job.Status == BacktestJobStatus.Completed && !string.IsNullOrEmpty(job.ResultJson)
? JsonSerializer.Deserialize<LightBacktest>(job.ResultJson)
: null
};
return Ok(response);
}
/// <summary>
/// Gets a paginated list of jobs with optional filters and sorting.
/// Admin only endpoint.
/// </summary>
/// <param name="page">Page number (defaults to 1)</param>
/// <param name="pageSize">Number of items per page (defaults to 50, max 100)</param>
/// <param name="sortBy">Field to sort by (CreatedAt, StartedAt, CompletedAt, Priority, Status, JobType) - defaults to CreatedAt</param>
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
/// <param name="status">Optional status filter (Pending, Running, Completed, Failed, Cancelled)</param>
/// <param name="jobType">Optional job type filter (Backtest, GeneticBacktest)</param>
/// <param name="userId">Optional user ID filter</param>
/// <param name="workerId">Optional worker ID filter</param>
/// <param name="bundleRequestId">Optional bundle request ID filter</param>
/// <returns>A paginated list of jobs</returns>
[HttpGet]
public async Task<ActionResult<PaginatedJobsResponse>> GetJobs(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 50,
[FromQuery] string sortBy = "CreatedAt",
[FromQuery] string sortOrder = "desc",
[FromQuery] string? status = null,
[FromQuery] string? jobType = null,
[FromQuery] int? userId = null,
[FromQuery] string? workerId = null,
[FromQuery] string? bundleRequestId = null)
{
if (!await IsUserAdmin())
{
_logger.LogWarning("Non-admin user attempted to list jobs");
return StatusCode(403, new { error = "Only admin users can list jobs" });
}
// Validate pagination parameters
if (page < 1)
{
return BadRequest("Page must be greater than 0");
}
if (pageSize < 1 || pageSize > 100)
{
return BadRequest("Page size must be between 1 and 100");
}
if (sortOrder != "asc" && sortOrder != "desc")
{
return BadRequest("Sort order must be 'asc' or 'desc'");
}
// Parse status filter
BacktestJobStatus? statusFilter = null;
if (!string.IsNullOrEmpty(status))
{
if (Enum.TryParse<BacktestJobStatus>(status, true, out var parsedStatus))
{
statusFilter = parsedStatus;
}
else
{
return BadRequest($"Invalid status value. Valid values are: {string.Join(", ", Enum.GetNames<BacktestJobStatus>())}");
}
}
// Parse job type filter
JobType? jobTypeFilter = null;
if (!string.IsNullOrEmpty(jobType))
{
if (Enum.TryParse<JobType>(jobType, true, out var parsedJobType))
{
jobTypeFilter = parsedJobType;
}
else
{
return BadRequest($"Invalid job type value. Valid values are: {string.Join(", ", Enum.GetNames<JobType>())}");
}
}
// Parse bundle request ID
Guid? bundleRequestIdFilter = null;
if (!string.IsNullOrEmpty(bundleRequestId))
{
if (!Guid.TryParse(bundleRequestId, out var bundleGuid))
{
return BadRequest("Invalid bundle request ID format. Must be a valid GUID.");
}
bundleRequestIdFilter = bundleGuid;
}
using var serviceScope = _serviceScopeFactory.CreateScope();
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
var (jobs, totalCount) = await jobRepository.GetPaginatedAsync(
page,
pageSize,
sortBy,
sortOrder,
statusFilter,
jobTypeFilter,
userId,
workerId,
bundleRequestIdFilter);
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
var response = new PaginatedJobsResponse
{
Jobs = jobs.Select(j => new JobListItemResponse
{
JobId = j.Id,
Status = j.Status.ToString(),
JobType = j.JobType.ToString(),
ProgressPercentage = j.ProgressPercentage,
Priority = j.Priority,
UserId = j.UserId,
BundleRequestId = j.BundleRequestId,
GeneticRequestId = j.GeneticRequestId,
AssignedWorkerId = j.AssignedWorkerId,
CreatedAt = j.CreatedAt,
StartedAt = j.StartedAt,
CompletedAt = j.CompletedAt,
LastHeartbeat = j.LastHeartbeat,
ErrorMessage = j.ErrorMessage,
StartDate = j.StartDate,
EndDate = j.EndDate
}).ToList(),
TotalCount = totalCount,
CurrentPage = page,
PageSize = pageSize,
TotalPages = totalPages,
HasNextPage = page < totalPages,
HasPreviousPage = page > 1
};
return Ok(response);
}
/// <summary>
/// Gets a summary of jobs grouped by status and job type with counts.
/// Admin only endpoint.
/// </summary>
/// <returns>Summary statistics of jobs</returns>
[HttpGet("summary")]
public async Task<ActionResult<JobSummaryResponse>> GetJobSummary()
{
if (!await IsUserAdmin())
{
_logger.LogWarning("Non-admin user attempted to get job summary");
return StatusCode(403, new { error = "Only admin users can access job summary" });
}
using var serviceScope = _serviceScopeFactory.CreateScope();
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
var summary = await jobRepository.GetSummaryAsync();
var response = new JobSummaryResponse
{
StatusSummary = summary.StatusCounts.Select(s => new JobStatusSummary
{
Status = s.Status.ToString(),
Count = s.Count
}).ToList(),
JobTypeSummary = summary.JobTypeCounts.Select(j => new JobTypeSummary
{
JobType = j.JobType.ToString(),
Count = j.Count
}).ToList(),
StatusTypeSummary = summary.StatusTypeCounts.Select(st => new JobStatusTypeSummary
{
Status = st.Status.ToString(),
JobType = st.JobType.ToString(),
Count = st.Count
}).ToList(),
TotalJobs = summary.TotalJobs
};
return Ok(response);
}
}