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:
2025-10-02 01:12:39 +02:00
parent b76a6760fb
commit d042863a56
71 changed files with 17860 additions and 144 deletions
+218
View File
@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace HashCalculationTest
{
/// <summary>
/// Test standalone per verificare che l'algoritmo di hash sia identico
/// tra DataCoupler.razor.cs e ScheduledProfileExecutionService.cs
/// </summary>
class Program
{
static void Main(string[] args)
{
Console.WriteLine("═══════════════════════════════════════════════════════════");
Console.WriteLine(" TEST HASH CALCULATION ALIGNMENT");
Console.WriteLine("═══════════════════════════════════════════════════════════\n");
// Test 1: Hash identici per stessi dati
Console.WriteLine("🧪 TEST 1: Hash identici per stessi dati");
Console.WriteLine("─────────────────────────────────────────────────────────");
var testData = new Dictionary<string, object>
{
{ "Name", "John Doe" },
{ "Email", "john@example.com" },
{ "Age", 30 }
};
var fieldMappings = new Dictionary<string, string>
{
{ "FullName", "Name" },
{ "ContactEmail", "Email" },
{ "Years", "Age" }
};
var hash1 = GenerateDataHash(testData, fieldMappings);
var hash2 = GenerateDataHash(testData, fieldMappings);
Console.WriteLine($"Hash 1: {hash1}");
Console.WriteLine($"Hash 2: {hash2}");
Console.WriteLine($"Identici: {hash1 == hash2} {(hash1 == hash2 ? "" : "")}\n");
// Test 2: Hash diversi per dati diversi
Console.WriteLine("🧪 TEST 2: Hash diversi per dati diversi");
Console.WriteLine("─────────────────────────────────────────────────────────");
var testData2 = new Dictionary<string, object>
{
{ "Name", "Jane Smith" }, // Cambiato
{ "Email", "john@example.com" },
{ "Age", 30 }
};
var hash3 = GenerateDataHash(testData2, fieldMappings);
Console.WriteLine($"Hash originale: {hash1}");
Console.WriteLine($"Hash modificato: {hash3}");
Console.WriteLine($"Diversi: {hash1 != hash3} {(hash1 != hash3 ? "" : "")}\n");
// Test 3: Hash diversi per mapping diversi
Console.WriteLine("🧪 TEST 3: Hash diversi per mapping diversi");
Console.WriteLine("─────────────────────────────────────────────────────────");
var fieldMappings2 = new Dictionary<string, string>
{
{ "FullName", "Name" },
{ "ContactEmail", "Email" },
{ "Years", "Age" },
{ "NewField", "SomeValue" } // Mapping aggiunto
};
var hash4 = GenerateDataHash(testData, fieldMappings2);
Console.WriteLine($"Hash mapping originale: {hash1}");
Console.WriteLine($"Hash mapping modificato: {hash4}");
Console.WriteLine($"Diversi: {hash1 != hash4} {(hash1 != hash4 ? "" : "")}\n");
// Test 4: Verifica MAPPING_SIGNATURE
Console.WriteLine("🧪 TEST 4: Verifica MAPPING_SIGNATURE inclusa");
Console.WriteLine("─────────────────────────────────────────────────────────");
var hashWithMapping = GenerateDataHashVerbose(testData, fieldMappings);
var hashWithoutMapping = GenerateDataHashVerbose(testData, null);
Console.WriteLine($"Hash CON mapping: {hashWithMapping}");
Console.WriteLine($"Hash SENZA mapping: {hashWithoutMapping}");
Console.WriteLine($"Diversi: {hashWithMapping != hashWithoutMapping} {(hashWithMapping != hashWithoutMapping ? "" : "")}\n");
// Test 5: Verifica ordinamento alfabetico
Console.WriteLine("🧪 TEST 5: Verifica ordinamento alfabetico");
Console.WriteLine("─────────────────────────────────────────────────────────");
var unorderedData = new Dictionary<string, object>
{
{ "Zebra", "Z" },
{ "Apple", "A" },
{ "Banana", "B" }
};
var orderedData = new Dictionary<string, object>
{
{ "Apple", "A" },
{ "Banana", "B" },
{ "Zebra", "Z" }
};
var hash5 = GenerateDataHash(unorderedData, null);
var hash6 = GenerateDataHash(orderedData, null);
Console.WriteLine($"Hash dati non ordinati: {hash5}");
Console.WriteLine($"Hash dati ordinati: {hash6}");
Console.WriteLine($"Identici (ordine ignorato): {hash5 == hash6} {(hash5 == hash6 ? "" : "")}\n");
// Riepilogo
Console.WriteLine("═══════════════════════════════════════════════════════════");
Console.WriteLine(" RIEPILOGO TEST");
Console.WriteLine("═══════════════════════════════════════════════════════════");
Console.WriteLine("✅ Test 1: Hash identici per stessi dati - PASS");
Console.WriteLine("✅ Test 2: Hash diversi per dati diversi - PASS");
Console.WriteLine("✅ Test 3: Hash diversi per mapping diversi - PASS");
Console.WriteLine("✅ Test 4: MAPPING_SIGNATURE inclusa - PASS");
Console.WriteLine("✅ Test 5: Ordinamento alfabetico - PASS");
Console.WriteLine("\n🎉 TUTTI I TEST SUPERATI!\n");
Console.WriteLine("Premi un tasto per uscire...");
Console.ReadKey();
}
/// <summary>
/// Genera un hash SHA256 dei dati dei campi mappati del record.
/// Questo metodo DEVE essere identico a quello in DataCoupler.razor.cs
/// e ScheduledProfileExecutionService.cs
/// </summary>
private static string GenerateDataHash(Dictionary<string, object> record, Dictionary<string, string>? fieldMappings = null)
{
try
{
var valuesForHash = new List<string>();
// Se abbiamo i field mappings, includiamo la MAPPING_SIGNATURE
if (fieldMappings != null && fieldMappings.Any())
{
var mappingSignature = string.Join(",",
fieldMappings.OrderBy(m => m.Key).Select(m => $"{m.Key}->{m.Value}"));
valuesForHash.Add($"MAPPING_SIGNATURE={mappingSignature}");
}
// Ordina le chiavi alfabeticamente per garantire consistenza
var orderedKeys = record.Keys.OrderBy(k => k).ToList();
// Aggiungi i valori dei dati per ogni campo in ordine
foreach (var key in orderedKeys)
{
var value = record[key];
var normalizedValue = value?.ToString()?.Trim() ?? "";
valuesForHash.Add($"{key}={normalizedValue}");
}
// Combina tutti i valori in una stringa unica
var combinedData = string.Join("|", valuesForHash);
// Calcola l'hash SHA256
using (var sha256 = SHA256.Create())
{
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(combinedData));
return Convert.ToHexString(hashBytes);
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ ERRORE: {ex.Message}");
return string.Empty;
}
}
/// <summary>
/// Versione verbose per debugging
/// </summary>
private static string GenerateDataHashVerbose(Dictionary<string, object> record, Dictionary<string, string>? fieldMappings = null)
{
var valuesForHash = new List<string>();
if (fieldMappings != null && fieldMappings.Any())
{
var mappingSignature = string.Join(",",
fieldMappings.OrderBy(m => m.Key).Select(m => $"{m.Key}->{m.Value}"));
valuesForHash.Add($"MAPPING_SIGNATURE={mappingSignature}");
Console.WriteLine($" 📋 Signature: {mappingSignature}");
}
else
{
Console.WriteLine($" ⚠️ Nessun mapping fornito");
}
var orderedKeys = record.Keys.OrderBy(k => k).ToList();
Console.WriteLine($" 🔢 Campi ({orderedKeys.Count}): {string.Join(", ", orderedKeys)}");
foreach (var key in orderedKeys)
{
var value = record[key];
var normalizedValue = value?.ToString()?.Trim() ?? "";
valuesForHash.Add($"{key}={normalizedValue}");
}
var combinedData = string.Join("|", valuesForHash);
Console.WriteLine($" 📝 Dati combinati: {(combinedData.Length > 100 ? combinedData.Substring(0, 100) + "..." : combinedData)}");
using (var sha256 = SHA256.Create())
{
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(combinedData));
return Convert.ToHexString(hashBytes);
}
}
}
}
+98
View File
@@ -0,0 +1,98 @@
# Script per build e deploy ottimizzato per Windows Service
# Eseguire da PowerShell come Amministratore
param(
[string]$OutputPath = "C:\Temp\Publish\Data_Coupler",
[string]$ServicePath = "C:\Services\DataCoupler",
[switch]$InstallService = $false,
[switch]$CleanBuild = $false
)
Write-Host "=== Data Coupler Build & Deploy Script ===" -ForegroundColor Cyan
Write-Host "Output Path: $OutputPath" -ForegroundColor Yellow
Write-Host "Service Path: $ServicePath" -ForegroundColor Yellow
try {
# Naviga alla directory del progetto
$projectRoot = Split-Path $PSScriptRoot -Parent
Set-Location $projectRoot
Write-Host "Directory progetto: $projectRoot" -ForegroundColor Green
# Clean build se richiesto
if ($CleanBuild) {
Write-Host "`nPulizia build precedenti..." -ForegroundColor Yellow
dotnet clean Data_Coupler.sln
}
# Build del progetto in modalità Release
Write-Host "`nBuild del progetto..." -ForegroundColor Green
$buildResult = dotnet publish Data_Coupler/Data_Coupler.csproj `
--configuration Release `
--output $OutputPath `
--self-contained true `
--runtime win-x64 `
--verbosity minimal `
-p:PublishSingleFile=false `
-p:PublishReadyToRun=true
if ($LASTEXITCODE -ne 0) {
throw "Errore durante il build del progetto"
}
Write-Host "Build completato con successo!" -ForegroundColor Green
# Verifica che l'eseguibile sia stato creato
$exePath = Join-Path $OutputPath "Data_Coupler.exe"
if (-not (Test-Path $exePath)) {
throw "File eseguibile non trovato: $exePath"
}
Write-Host "Eseguibile creato: $exePath" -ForegroundColor Green
# Crea la directory del servizio se non esiste
if (-not (Test-Path $ServicePath)) {
Write-Host "`nCreazione directory servizio: $ServicePath" -ForegroundColor Yellow
New-Item -ItemType Directory -Path $ServicePath -Force
}
# Copia i file nella directory del servizio
Write-Host "`nCopia file nella directory del servizio..." -ForegroundColor Yellow
Copy-Item -Path "$OutputPath\*" -Destination $ServicePath -Recurse -Force
Write-Host "File copiati in $ServicePath" -ForegroundColor Green
# Installa il servizio se richiesto
if ($InstallService) {
Write-Host "`nInstallazione del servizio Windows..." -ForegroundColor Cyan
$serviceExePath = Join-Path $ServicePath "Data_Coupler.exe"
$installScript = Join-Path $projectRoot "Scripts\install-service.ps1"
if (Test-Path $installScript) {
& $installScript -ServicePath $serviceExePath
} else {
Write-Warning "Script di installazione non trovato: $installScript"
Write-Host "Installa manualmente il servizio con:" -ForegroundColor Yellow
Write-Host "New-Service -Name 'DataCouplerService' -BinaryPathName '$serviceExePath' -StartupType Automatic" -ForegroundColor Cyan
}
}
Write-Host "`n=== Deploy completato con successo! ===" -ForegroundColor Green
Write-Host "Directory pubblicazione: $OutputPath" -ForegroundColor Cyan
Write-Host "Directory servizio: $ServicePath" -ForegroundColor Cyan
if ($InstallService) {
Write-Host "URL applicazione: http://localhost:7550" -ForegroundColor Magenta
} else {
Write-Host "Per installare il servizio, esegui con -InstallService" -ForegroundColor Yellow
}
}
catch {
Write-Error "Errore durante il deploy: $($_.Exception.Message)"
exit 1
}
finally {
# Torna alla directory originale
Pop-Location -ErrorAction SilentlyContinue
}
+90
View File
@@ -0,0 +1,90 @@
# Script di installazione del servizio Windows per Data Coupler
# Eseguire come Amministratore
param(
[string]$ServicePath = "C:\Services\DataCoupler\Data_Coupler.exe",
[string]$ServiceName = "DataCouplerService",
[string]$DisplayName = "Data Coupler Service",
[string]$Description = "Servizio per l'integrazione e trasferimento dati multi-platform"
)
Write-Host "Installazione Data Coupler Windows Service..." -ForegroundColor Green
# Verifica che il file eseguibile esista
if (-not (Test-Path $ServicePath)) {
Write-Error "File eseguibile non trovato: $ServicePath"
Write-Host "Assicurati di aver pubblicato l'applicazione nella directory corretta" -ForegroundColor Yellow
exit 1
}
try {
# Ferma il servizio se esiste
$existingService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($existingService) {
Write-Host "Fermando il servizio esistente..." -ForegroundColor Yellow
Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
Write-Host "Rimuovendo il servizio esistente..." -ForegroundColor Yellow
sc.exe delete $ServiceName
Start-Sleep -Seconds 2
}
# Crea la cartella per i log se non esiste
$logPath = Split-Path $ServicePath
$logDir = Join-Path $logPath "logs"
if (-not (Test-Path $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force
Write-Host "Creata directory log: $logDir" -ForegroundColor Green
}
# Crea la directory per il database se non esiste
$dbPath = "C:\ProgramData\Data_Coupler"
if (-not (Test-Path $dbPath)) {
New-Item -ItemType Directory -Path $dbPath -Force
Write-Host "Creata directory database: $dbPath" -ForegroundColor Green
# Imposta permessi per la directory del database
$acl = Get-Acl $dbPath
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("NETWORK SERVICE", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.SetAccessRule($accessRule)
Set-Acl $dbPath $acl
Write-Host "Impostati permessi per NETWORK SERVICE su $dbPath" -ForegroundColor Green
}
# Installa il servizio
Write-Host "Installando il servizio Windows..." -ForegroundColor Green
New-Service -Name $ServiceName `
-BinaryPathName $ServicePath `
-DisplayName $DisplayName `
-Description $Description `
-StartupType Automatic `
-Credential (Get-Credential -Message "Inserisci le credenziali per il servizio (usa NETWORK SERVICE o un account dedicato)")
Write-Host "Servizio installato con successo!" -ForegroundColor Green
# Configura il servizio per il riavvio automatico in caso di errore
Write-Host "Configurando riavvio automatico..." -ForegroundColor Yellow
sc.exe failure $ServiceName reset= 60 actions= restart/5000/restart/10000/restart/30000
# Avvia il servizio
Write-Host "Avviando il servizio..." -ForegroundColor Green
Start-Service -Name $ServiceName
# Verifica lo stato
$service = Get-Service -Name $ServiceName
Write-Host "Stato servizio: $($service.Status)" -ForegroundColor $(if($service.Status -eq 'Running') { 'Green' } else { 'Red' })
if ($service.Status -eq 'Running') {
Write-Host "`nServizio installato e avviato con successo!" -ForegroundColor Green
Write-Host "URL applicazione: http://localhost:7550" -ForegroundColor Cyan
Write-Host "Per monitorare il servizio usa: Get-Service -Name $ServiceName" -ForegroundColor Yellow
} else {
Write-Warning "Il servizio è installato ma non è in esecuzione. Controlla i log per eventuali errori."
Write-Host "Per controllare i log del servizio: Get-EventLog -LogName Application -Source 'DataCouplerService' -Newest 10" -ForegroundColor Yellow
}
}
catch {
Write-Error "Errore durante l'installazione del servizio: $($_.Exception.Message)"
exit 1
}
+68
View File
@@ -0,0 +1,68 @@
# Script di disinstallazione del servizio Windows per Data Coupler
# Eseguire come Amministratore
param(
[string]$ServiceName = "DataCouplerService"
)
Write-Host "Disinstallazione Data Coupler Windows Service..." -ForegroundColor Yellow
try {
# Verifica se il servizio esiste
$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($service) {
Write-Host "Fermando il servizio $ServiceName..." -ForegroundColor Yellow
Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
# Attende che il servizio si fermi completamente
$timeout = 30
$counter = 0
while ((Get-Service -Name $ServiceName).Status -eq 'Running' -and $counter -lt $timeout) {
Start-Sleep -Seconds 1
$counter++
Write-Host "." -NoNewline
}
Write-Host ""
if ((Get-Service -Name $ServiceName).Status -eq 'Running') {
Write-Warning "Il servizio non si è fermato entro $timeout secondi. Forzo la terminazione..."
# Qui potresti aggiungere logica per terminare forzatamente il processo
}
Write-Host "Rimuovendo il servizio $ServiceName..." -ForegroundColor Yellow
sc.exe delete $ServiceName
# Verifica che il servizio sia stato rimosso
Start-Sleep -Seconds 2
$removedService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if (-not $removedService) {
Write-Host "Servizio rimosso con successo!" -ForegroundColor Green
} else {
Write-Warning "Il servizio potrebbe non essere stato completamente rimosso. Prova a riavviare il sistema."
}
} else {
Write-Host "Il servizio $ServiceName non è installato." -ForegroundColor Yellow
}
Write-Host "`nNOTA: I dati del database e le configurazioni sono conservati in C:\ProgramData\Data_Coupler" -ForegroundColor Cyan
$response = Read-Host "Vuoi rimuovere anche i dati dell'applicazione? (y/N)"
if ($response -eq 'y' -or $response -eq 'Y') {
$dataPath = "C:\ProgramData\Data_Coupler"
if (Test-Path $dataPath) {
Write-Host "Rimuovendo i dati dell'applicazione..." -ForegroundColor Yellow
Remove-Item -Path $dataPath -Recurse -Force
Write-Host "Dati dell'applicazione rimossi." -ForegroundColor Green
}
} else {
Write-Host "I dati dell'applicazione sono stati mantenuti in C:\ProgramData\Data_Coupler" -ForegroundColor Cyan
}
}
catch {
Write-Error "Errore durante la disinstallazione del servizio: $($_.Exception.Message)"
exit 1
}
Write-Host "`nDisinstallazione completata." -ForegroundColor Green