Renommage : TrajetKmTracker -> SchotzIadKmTracker (backend, specs, frontend)
- Backend : projets, solution, namespaces et références TrajetKmTracker.Api/.Tests -> SchotzIadKmTracker.Api/.Tests (dossiers, .csproj, .sln, .http, namespaces C#). GUID UserSecretsId préservé. - Specs : dossier .kiro/specs/trajet-km-tracker -> schotziad-km-tracker et mise à jour des références internes (chemins, tags de propriétés). - Frontend : projet Angular trajet-km-tracker -> schotziad-km-tracker (package.json, angular.json, chemin dist, Dockerfile), titre applicatif 'SchotzIAD KM Tracker'. - Docker et scripts (backend/Dockerfile, frontend/Dockerfile, run-dev.sh) mis à jour. - Suppression du fichier résiduel package.json.bak. Backend (solution) et frontend (build production) compilent sans erreur.
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SchotzIadKmTracker.Api.Data;
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
using SchotzIadKmTracker.Api.Models.Entities;
|
||||
using SchotzIadKmTracker.Api.Repositories;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implémentation du Cache_Adresses. Les adresses sont stockées et comparées sous forme
|
||||
/// normalisée (trim + minuscules + suppression des accents) afin d'assurer un matching robuste.
|
||||
/// Les écritures (upsert + éviction LRU) sont exécutées en transaction.
|
||||
/// </summary>
|
||||
public class AdresseCacheService : IAdresseCacheService
|
||||
{
|
||||
/// <summary>Longueur minimale d'une requête de recherche locale (Requirement 9.3).</summary>
|
||||
private const int LongueurMinimaleRecherche = 3;
|
||||
|
||||
/// <summary>Taille maximale du cache avant éviction LRU (Requirement 9.5).</summary>
|
||||
private const int TailleMaximaleCache = 1_000;
|
||||
|
||||
private readonly IAdresseCacheRepository _repository;
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public AdresseCacheService(IAdresseCacheRepository repository, AppDbContext db)
|
||||
{
|
||||
_repository = repository;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AdresseCacheDto>> RechercherLocalAsync(
|
||||
string query,
|
||||
int limite = 5,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var fragment = Normaliser(query);
|
||||
if (fragment.Length < LongueurMinimaleRecherche)
|
||||
{
|
||||
return Array.Empty<AdresseCacheDto>();
|
||||
}
|
||||
|
||||
if (limite <= 0)
|
||||
{
|
||||
limite = 5;
|
||||
}
|
||||
|
||||
var entrees = await _repository.RechercherAsync(fragment, limite, cancellationToken);
|
||||
|
||||
return entrees
|
||||
.Select(e => new AdresseCacheDto(e.Adresse, e.Latitude, e.Longitude))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<AdresseCacheEntreeDto?> ObtenirParAdresseAsync(
|
||||
string adresse,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalisee = Normaliser(adresse);
|
||||
if (normalisee.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entree = await _repository.ObtenirParAdresseNormaliseeAsync(normalisee, cancellationToken);
|
||||
if (entree is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Toute utilisation d'une adresse en cache rafraîchit sa date de dernière utilisation.
|
||||
// Si une transaction est déjà ouverte par l'appelant (ex. création d'un Trajet),
|
||||
// on y participe sans en ouvrir une nouvelle : SQLite interdit les transactions imbriquées.
|
||||
entree.DerniereUtilisation = DateTime.UtcNow;
|
||||
if (_db.Database.CurrentTransaction is null)
|
||||
{
|
||||
await using var transaction = await _db.Database.BeginTransactionAsync(cancellationToken);
|
||||
await _repository.SauvegarderAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _repository.SauvegarderAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return new AdresseCacheEntreeDto(
|
||||
entree.Adresse,
|
||||
entree.Latitude,
|
||||
entree.Longitude,
|
||||
entree.GeometrieJson);
|
||||
}
|
||||
|
||||
public async Task UpsertAdresseAsync(
|
||||
string adresse,
|
||||
double latitude,
|
||||
double longitude,
|
||||
string? geometrieJson = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalisee = Normaliser(adresse);
|
||||
if (normalisee.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Si une transaction est déjà ouverte par l'appelant (ex. TrajetService.CreateAsync),
|
||||
// on y participe sans en ouvrir une nouvelle (SQLite interdit les transactions imbriquées).
|
||||
var transactionAmbiante = _db.Database.CurrentTransaction is not null;
|
||||
var transaction = transactionAmbiante
|
||||
? null
|
||||
: await _db.Database.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
var existante = await _repository.ObtenirParAdresseNormaliseeAsync(normalisee, cancellationToken);
|
||||
if (existante is not null)
|
||||
{
|
||||
// Adresse déjà présente : coordonnées inchangées, seule la date d'utilisation est
|
||||
// rafraîchie (Requirements 9.1, 9.6).
|
||||
existante.DerniereUtilisation = DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nouvelle adresse : insertion avec les coordonnées fournies.
|
||||
var nouvelle = new AdresseCache
|
||||
{
|
||||
Adresse = normalisee,
|
||||
Latitude = latitude,
|
||||
Longitude = longitude,
|
||||
GeometrieJson = geometrieJson,
|
||||
DerniereUtilisation = DateTime.UtcNow
|
||||
};
|
||||
await _repository.AjouterAsync(nouvelle, cancellationToken);
|
||||
}
|
||||
|
||||
await _repository.SauvegarderAsync(cancellationToken);
|
||||
|
||||
// Éviction LRU : conserver au plus les 1 000 entrées les plus récemment utilisées
|
||||
// et supprimer les plus anciennes (Requirement 9.5).
|
||||
var total = await _repository.CompterAsync(cancellationToken);
|
||||
if (total > TailleMaximaleCache)
|
||||
{
|
||||
var aSupprimer = await _repository.ObtenirPlusAnciennesAsync(
|
||||
total - TailleMaximaleCache, cancellationToken);
|
||||
|
||||
_repository.Supprimer(aSupprimer);
|
||||
await _repository.SauvegarderAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// On ne valide que la transaction que l'on a nous-mêmes ouverte ; sinon c'est
|
||||
// l'appelant qui validera la transaction ambiante.
|
||||
if (transaction is not null)
|
||||
{
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
await transaction.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalise un texte d'adresse : suppression des espaces de bord, passage en minuscules
|
||||
/// et suppression des accents (diacritiques). Cohérent avec la normalisation des Adresses_Nommées.
|
||||
/// </summary>
|
||||
private static string Normaliser(string? valeur)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(valeur))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var trimmee = valeur.Trim().ToLowerInvariant();
|
||||
|
||||
var decomposee = trimmee.Normalize(NormalizationForm.FormD);
|
||||
var builder = new StringBuilder(decomposee.Length);
|
||||
foreach (var caractere in decomposee)
|
||||
{
|
||||
var categorie = CharUnicodeInfo.GetUnicodeCategory(caractere);
|
||||
if (categorie != UnicodeCategory.NonSpacingMark)
|
||||
{
|
||||
builder.Append(caractere);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString().Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user