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

@@ -1,6 +1,7 @@
using System.Text.Json;
using Managing.Api.Models.Requests;
using Managing.Api.Models.Responses;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Abstractions.Shared;
using Managing.Application.Hubs;
@@ -34,6 +35,7 @@ public class BacktestController : BaseController
private readonly IAccountService _accountService;
private readonly IMoneyManagementService _moneyManagementService;
private readonly IGeneticService _geneticService;
private readonly IServiceScopeFactory _serviceScopeFactory;
/// <summary>
/// Initializes a new instance of the <see cref="BacktestController"/> class.
@@ -51,13 +53,15 @@ public class BacktestController : BaseController
IAccountService accountService,
IMoneyManagementService moneyManagementService,
IGeneticService geneticService,
IUserService userService) : base(userService)
IUserService userService,
IServiceScopeFactory serviceScopeFactory) : base(userService)
{
_hubContext = hubContext;
_backtester = backtester;
_accountService = accountService;
_moneyManagementService = moneyManagementService;
_geneticService = geneticService;
_serviceScopeFactory = serviceScopeFactory;
}
/// <summary>
@@ -788,6 +792,51 @@ public class BacktestController : BaseController
return Ok(new { Unsubscribed = true, RequestId = requestId });
}
/// <summary>
/// Gets the status of a bundle backtest request, aggregating all job statuses.
/// </summary>
/// <param name="bundleRequestId">The bundle request ID</param>
/// <returns>The bundle status with aggregated job statistics</returns>
[HttpGet]
[Route("Bundle/{bundleRequestId}/Status")]
public async Task<ActionResult<BundleBacktestStatusResponse>> GetBundleStatus(string bundleRequestId)
{
if (!Guid.TryParse(bundleRequestId, out var bundleGuid))
{
return BadRequest("Invalid bundle request ID format. Must be a valid GUID.");
}
var user = await GetUser();
var bundleRequest = _backtester.GetBundleBacktestRequestByIdForUser(user, bundleGuid);
if (bundleRequest == null)
{
return NotFound($"Bundle backtest request with ID {bundleRequestId} not found.");
}
// Get all jobs for this bundle
using var serviceScope = _serviceScopeFactory.CreateScope();
var jobRepository = serviceScope.ServiceProvider.GetRequiredService<IBacktestJobRepository>();
var jobs = await jobRepository.GetByBundleRequestIdAsync(bundleGuid);
var response = new BundleBacktestStatusResponse
{
BundleRequestId = bundleGuid,
Status = bundleRequest.Status.ToString(),
TotalJobs = jobs.Count(),
CompletedJobs = jobs.Count(j => j.Status == BacktestJobStatus.Completed),
FailedJobs = jobs.Count(j => j.Status == BacktestJobStatus.Failed),
RunningJobs = jobs.Count(j => j.Status == BacktestJobStatus.Running),
PendingJobs = jobs.Count(j => j.Status == BacktestJobStatus.Pending),
ProgressPercentage = bundleRequest.ProgressPercentage,
CreatedAt = bundleRequest.CreatedAt,
CompletedAt = bundleRequest.CompletedAt,
ErrorMessage = bundleRequest.ErrorMessage
};
return Ok(response);
}
/// <summary>
/// Runs a genetic algorithm optimization with the specified configuration.
/// This endpoint saves the genetic request to the database and returns the request ID.

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