Serveur Rust : module grid, phase 2 (calcul de la grille, routes /api/grid et /api/v1)

- product_rows.rs : getProductRows complet (7 requêtes FF en parallèle,
  métriques réseau Qlik, report du dernier snapshot, filtre gamme Y,
  rattrapage d'un magasin par l'API FF), branché sur rows_cache (calculs en
  cours partagés, compteur d'enregistrements, compléments par magasin),
  instantané grid_rows resservi s'il date du mois (rafraîchi au-delà de 20 h),
  persistance non bloquante ; getStorePatch ; get_product_rows_for_sync pour la
  synchro nocturne et le préchauffage.
- store_patch.rs : buildStorePatchEntries.
- enrich.rs (api-enrich) et api_gammes.rs (affecterGammes, lireCorpsJson).
- Routes /api/grid/rows (NDJSON), /api/grid/rows/store-patch, /api/v1/grid
  (diffusion complète sans limit), /products/search, /products/{codein},
  /products/{codein}/gamme, /gammes, /fournisseurs, /network/{codeCentrale}.
- store.rs : upsert_grid_rows_shared, écriture par lots depuis les lignes
  partagées du cache (sans copie intégrale).

Écart volontaire : après un calcul à la demande, /api/v1 attend la fin de
l'écriture de l'instantané avant de le relire (le TS ne l'attendait pas et
pouvait répondre à tort « aucun article »).

Test d'intégration sur la base ff_fixture (GRID_FF_TEST_DATABASE_URL) : calcul,
cache, instantané, complément magasin via une fausse API FF, enrichissement et
toutes les routes (session et clé d'API).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LG2LLJuswrrTTPKJmunEVf
This commit is contained in:
Claude committed 2026-10-12 19:54:54 +00:00
1 parent 5a84faffb2
commit 97c1dea849
10 files changed
+2306 -19

No files matched your search

+31 -7
View File
@@ -159,16 +159,40 @@ pub async fn upsert_grid_rows(code_fournisseur: &str, rows: &[ProductRow]) -> Ap
if code_fournisseur.is_empty() || rows.is_empty() {
return Ok(0);
}
let values: Vec<&ProductRow> = rows.iter().filter(|r| !r.codein.is_empty()).collect();
upsert_lots(code_fournisseur, values.len(), |plage| values[plage].iter().map(|r| (*r).clone()).collect()).await
}
/// Même écriture depuis les lignes partagées du cache mémoire : chaque lot est
/// copié sous verrou, puis écrit sans le tenir (pas de copie intégrale d'un
/// fournisseur de 130 000 articles).
pub async fn upsert_grid_rows_shared(code_fournisseur: &str, rows: &super::rows_cache::GridRows) -> AppResult<usize> {
let indices: Vec<usize> = {
let lignes = rows.rows.read();
if code_fournisseur.is_empty() || lignes.is_empty() {
return Ok(0);
}
lignes.iter().enumerate().filter(|(_, r)| !r.codein.is_empty()).map(|(i, _)| i).collect()
};
upsert_lots(code_fournisseur, indices.len(), |plage| {
let lignes = rows.rows.read();
indices[plage].iter().filter_map(|i| lignes.get(*i).cloned()).collect()
})
.await
}
/// Écrit `nb` lignes par lots de [`CHUNK_UPSERT`] (`lot(plage)` fournit les lignes
/// d'une plage), puis purge les lignes du fournisseur restées sur un calcul antérieur.
async fn upsert_lots(code_fournisseur: &str, nb: usize, lot: impl Fn(std::ops::Range<usize>) -> Vec<ProductRow>) -> AppResult<usize> {
if nb == 0 {
return Ok(0);
}
let pool = db::pool()?;
let computed_at = maintenant_utc_ms();
let values: Vec<&ProductRow> = rows.iter().filter(|r| !r.codein.is_empty()).collect();
if values.is_empty() {
return Ok(0);
}
let mut written = 0usize;
for batch in values.chunks(CHUNK_UPSERT) {
for debut in (0..nb).step_by(CHUNK_UPSERT) {
let batch = lot(debut..(debut + CHUNK_UPSERT).min(nb));
let mut qb: QueryBuilder<Postgres> = QueryBuilder::new(
"insert into grid_rows (code_fournisseur, codein, nom_fournisseur, libelle1, gtin, reference, \
code_centrale, code1, code2, code3, code_gamme, code_gamme_init, total_ca, total_quantite, \
@@ -177,7 +201,7 @@ pub async fn upsert_grid_rows(code_fournisseur: &str, rows: &[ProductRow]) -> Ap
);
let mut erreur_payload = None;
qb.push_values(batch.iter(), |mut b, r| {
let mut payload = (*r).clone();
let mut payload = r.clone();
payload.payload_version = Some(GRID_PAYLOAD_VERSION);
let payload = serde_json::to_value(&payload).unwrap_or_else(|e| {
erreur_payload = Some(e.to_string());