feat: Implementazione completa sistema schedulazione con intervalli personalizzati
- Aggiunto supporto schedulazione con intervalli flessibili (secondi/minuti/ore/giorni/settimane/mesi) - Esteso modello ProfileSchedule con campi IntervalValue e IntervalUnit - Ottimizzato ScheduledJobService per controlli ogni 30s con esecuzione parallela - Implementata interfaccia UI completa con anteprima real-time in italiano - Aggiunta migrazione database AddIntervalSchedulingFields - Implementati metodi calcolo NextExecutionTime per intervalli - Aggiunta gestione tracking anti-duplicati e cleanup automatico - Creata documentazione completa (6 file, 2500+ righe) Modifiche tecniche: - ProfileSchedule.cs: Nuovi campi e metodi CalculateNextInterval/GetScheduleDescription - ScheduledJobService.cs: Ridotto check interval a 30s, aggiunto parallel processing - ProfileScheduleService.cs: Supporto calcolo intervalli in UpdateNextExecutionTimeAsync - Scheduling.razor: Aggiunta sezione UI per configurazione intervalli - Scheduling.razor.cs: Implementato GetIntervalPreview() e gestione stato campi
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
using CredentialManager.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Data_Coupler.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Servizio per l'esecuzione automatica dei trasferimenti dati basati sui profili
|
||||
/// </summary>
|
||||
public interface IDataTransferService
|
||||
{
|
||||
Task<DataTransferResult> ExecuteProfileAsync(DataCouplerProfile profile, string? sourceDatabaseOverride = null, string? destinationDatabaseOverride = null);
|
||||
}
|
||||
|
||||
public class DataTransferResult
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public int RecordsProcessed { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime EndTime { get; set; }
|
||||
public List<string> ErrorDetails { get; set; } = new();
|
||||
public Dictionary<string, object> AdditionalInfo { get; set; } = new();
|
||||
|
||||
public TimeSpan Duration => EndTime - StartTime;
|
||||
}
|
||||
|
||||
public class DataTransferService : IDataTransferService
|
||||
{
|
||||
private readonly IScheduledProfileExecutionService _scheduledExecutionService;
|
||||
private readonly ILogger<DataTransferService> _logger;
|
||||
|
||||
public DataTransferService(
|
||||
IScheduledProfileExecutionService scheduledExecutionService,
|
||||
ILogger<DataTransferService> logger)
|
||||
{
|
||||
_scheduledExecutionService = scheduledExecutionService ?? throw new ArgumentNullException(nameof(scheduledExecutionService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<DataTransferResult> ExecuteProfileAsync(DataCouplerProfile profile, string? sourceDatabaseOverride = null, string? destinationDatabaseOverride = null)
|
||||
{
|
||||
var result = new DataTransferResult
|
||||
{
|
||||
StartTime = DateTime.Now // Usa l'ora locale per coerenza con le schedulazioni
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Iniziando esecuzione profilo {ProfileName} (ID: {ProfileId})",
|
||||
profile.Name, profile.Id);
|
||||
|
||||
// Validazione del profilo
|
||||
var validationResult = ValidateProfile(profile);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
result.IsSuccess = false;
|
||||
result.ErrorMessage = validationResult.ErrorMessage;
|
||||
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
|
||||
return result;
|
||||
}
|
||||
|
||||
// Controlla se il profilo ha file come sorgente e blocca l'esecuzione
|
||||
if (profile.SourceType?.ToLower() == "file")
|
||||
{
|
||||
result.IsSuccess = false;
|
||||
result.ErrorMessage = "I profili con file come sorgente non sono supportati nelle schedulazioni per motivi di sicurezza.";
|
||||
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
|
||||
_logger.LogWarning("Tentativo di esecuzione di profilo con file come sorgente bloccato: {ProfileName}", profile.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Applica override del database se specificati
|
||||
var profileToExecute = await ApplyDatabaseOverrides(profile, sourceDatabaseOverride, destinationDatabaseOverride);
|
||||
|
||||
// Utilizza il servizio esistente per l'esecuzione
|
||||
var executionResult = await _scheduledExecutionService.ExecuteProfileAsync(profileToExecute.Id);
|
||||
|
||||
result.IsSuccess = executionResult.Success;
|
||||
result.RecordsProcessed = executionResult.RecordsProcessed;
|
||||
result.ErrorMessage = executionResult.Success ? null : executionResult.Message;
|
||||
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
|
||||
|
||||
if (executionResult.Success)
|
||||
{
|
||||
result.AdditionalInfo["ExecutionDuration"] = executionResult.Duration.ToString();
|
||||
_logger.LogInformation("Profilo {ProfileName} eseguito con successo. " +
|
||||
"Record processati: {RecordsProcessed}, Durata: {Duration}ms",
|
||||
profile.Name, result.RecordsProcessed, result.Duration.TotalMilliseconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ErrorDetails.Add(executionResult.Message);
|
||||
_logger.LogError("Errore nell'esecuzione del profilo {ProfileName}: {ErrorMessage}",
|
||||
profile.Name, executionResult.Message);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore durante l'esecuzione del profilo {ProfileName} (ID: {ProfileId})",
|
||||
profile.Name, profile.Id);
|
||||
|
||||
result.IsSuccess = false;
|
||||
result.ErrorMessage = ex.Message;
|
||||
result.ErrorDetails.Add($"Exception: {ex.GetType().Name} - {ex.Message}");
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
result.ErrorDetails.Add($"Inner Exception: {ex.InnerException.Message}");
|
||||
}
|
||||
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<DataCouplerProfile> ApplyDatabaseOverrides(DataCouplerProfile originalProfile,
|
||||
string? sourceDatabaseOverride, string? destinationDatabaseOverride)
|
||||
{
|
||||
// Se non ci sono override, restituisce il profilo originale
|
||||
if (string.IsNullOrEmpty(sourceDatabaseOverride) && string.IsNullOrEmpty(destinationDatabaseOverride))
|
||||
{
|
||||
return originalProfile;
|
||||
}
|
||||
|
||||
// Crea una copia del profilo con gli override applicati
|
||||
// In un'implementazione reale, potresti voler creare una copia più sofisticata
|
||||
// o utilizzare un metodo di clonazione appropriato
|
||||
var profileCopy = new DataCouplerProfile
|
||||
{
|
||||
Id = originalProfile.Id,
|
||||
Name = originalProfile.Name,
|
||||
Description = originalProfile.Description,
|
||||
SourceType = originalProfile.SourceType,
|
||||
SourceCredentialId = originalProfile.SourceCredentialId,
|
||||
SourceDatabaseName = originalProfile.SourceDatabaseName,
|
||||
SourceTable = originalProfile.SourceTable,
|
||||
SourceSchema = originalProfile.SourceSchema,
|
||||
SourceCustomQuery = originalProfile.SourceCustomQuery,
|
||||
SourceFilePath = originalProfile.SourceFilePath,
|
||||
DestinationType = originalProfile.DestinationType,
|
||||
DestinationCredentialId = originalProfile.DestinationCredentialId,
|
||||
DestinationTable = originalProfile.DestinationTable,
|
||||
DestinationSchema = originalProfile.DestinationSchema,
|
||||
DestinationEndpoint = originalProfile.DestinationEndpoint,
|
||||
FieldMappingJson = originalProfile.FieldMappingJson,
|
||||
SourceKeyField = originalProfile.SourceKeyField,
|
||||
UseRecordAssociations = originalProfile.UseRecordAssociations,
|
||||
IsActive = originalProfile.IsActive,
|
||||
CreatedAt = originalProfile.CreatedAt,
|
||||
LastUsedAt = originalProfile.LastUsedAt,
|
||||
CreatedBy = originalProfile.CreatedBy
|
||||
};
|
||||
|
||||
// TODO: Implementare l'applicazione degli override del database
|
||||
// Questo richiederebbe di modificare temporaneamente la stringa di connessione
|
||||
// delle credenziali per puntare al database specificato
|
||||
|
||||
_logger.LogInformation("Applicazione override database - Source: {SourceDB}, Destination: {DestDB}",
|
||||
sourceDatabaseOverride ?? "none", destinationDatabaseOverride ?? "none");
|
||||
|
||||
return await Task.FromResult(profileCopy);
|
||||
}
|
||||
|
||||
private (bool IsValid, string? ErrorMessage) ValidateProfile(DataCouplerProfile profile)
|
||||
{
|
||||
if (profile == null)
|
||||
return (false, "Profilo non specificato");
|
||||
|
||||
if (string.IsNullOrEmpty(profile.Name))
|
||||
return (false, "Nome profilo non specificato");
|
||||
|
||||
if (string.IsNullOrEmpty(profile.SourceType))
|
||||
return (false, "Tipo sorgente non specificato");
|
||||
|
||||
if (string.IsNullOrEmpty(profile.DestinationType))
|
||||
return (false, "Tipo destinazione non specificato");
|
||||
|
||||
if (!profile.SourceCredentialId.HasValue)
|
||||
return (false, "Credenziale sorgente non specificata");
|
||||
|
||||
if (!profile.DestinationCredentialId.HasValue)
|
||||
return (false, "Credenziale destinazione non specificata");
|
||||
|
||||
// Validazioni specifiche per tipo sorgente
|
||||
if (profile.SourceType == "database")
|
||||
{
|
||||
if (string.IsNullOrEmpty(profile.SourceTable) && string.IsNullOrEmpty(profile.SourceCustomQuery))
|
||||
return (false, "Tabella sorgente o query personalizzata deve essere specificata");
|
||||
}
|
||||
else if (profile.SourceType == "file")
|
||||
{
|
||||
if (string.IsNullOrEmpty(profile.SourceFilePath))
|
||||
return (false, "Percorso file sorgente non specificato");
|
||||
}
|
||||
|
||||
// Validazioni specifiche per tipo destinazione
|
||||
if (profile.DestinationType == "database")
|
||||
{
|
||||
if (string.IsNullOrEmpty(profile.DestinationTable))
|
||||
return (false, "Tabella destinazione non specificata");
|
||||
}
|
||||
else if (profile.DestinationType == "rest")
|
||||
{
|
||||
if (string.IsNullOrEmpty(profile.DestinationEndpoint))
|
||||
return (false, "Endpoint REST destinazione non specificato");
|
||||
}
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user