diff --git a/php/inc_fx_import_resultats.php b/php/inc_fx_import_resultats.php index 1753790..67b4dcc 100644 --- a/php/inc_fx_import_resultats.php +++ b/php/inc_fx_import_resultats.php @@ -2857,15 +2857,98 @@ function fxImportResultatsResetPreview($intEveId) { ); } +/** + * MSIN-4482 — DELETE par lots (évite monolithe qui verrouille MySQL / met le site down). + * @param string $strSqlBase SQL sans LIMIT, ex. "DELETE FROM t WHERE …" + * @param int $intChunk taille du lot + * @param int $intPauseUs microsecondes entre lots + * @return int lignes affectées cumulées + */ +function fxImportResultatsDeleteBatchedLimit($strSqlBase, $intChunk = 250, $intPauseUs = 40000) { + global $objDatabase; + + $intChunk = max(50, intval($intChunk)); + $intPauseUs = max(0, intval($intPauseUs)); + $intTotal = 0; + $intGuard = 0; + $intMaxLoops = 20000; // filet anti-boucle + + do { + @set_time_limit(120); + $sql = rtrim((string)$strSqlBase) . ' LIMIT ' . $intChunk; + if ($objDatabase->fxQuery($sql) === false) { + break; + } + $intAff = intval($objDatabase->affected_rows); + $intTotal += $intAff; + $intGuard++; + if ($intAff > 0 && $intPauseUs > 0) { + usleep($intPauseUs); + } + } while ($intAff > 0 && $intGuard < $intMaxLoops); + + return $intTotal; +} + +/** + * MSIN-4482 — DELETE … WHERE col IN (ids) par chunks PHP. + * @param int[] $arrIds + * @param string $strExtraWhere clause AND additionnelle déjà sécurisée (ex. "eve_id = 12") + */ +function fxImportResultatsDeleteBatchedIn($strTable, $strCol, $arrIds, $intChunk = 200, $intPauseUs = 40000, $strExtraWhere = '') { + global $objDatabase; + + $strTable = preg_replace('/[^a-z0-9_]/i', '', (string)$strTable); + $strCol = preg_replace('/[^a-z0-9_]/i', '', (string)$strCol); + if ($strTable === '' || $strCol === '') { + return 0; + } + + $arrIds = array_values(array_unique(array_filter(array_map('intval', (array)$arrIds), function ($n) { + return $n > 0; + }))); + if (count($arrIds) < 1) { + return 0; + } + + $intChunk = max(50, intval($intChunk)); + $intPauseUs = max(0, intval($intPauseUs)); + $strExtraWhere = trim((string)$strExtraWhere); + $intTotal = 0; + $intN = count($arrIds); + + for ($i = 0; $i < $intN; $i += $intChunk) { + @set_time_limit(120); + $arrChunk = array_slice($arrIds, $i, $intChunk); + if (count($arrChunk) < 1) { + continue; + } + $sql = 'DELETE FROM ' . $strTable + . ' WHERE ' . $strCol . ' IN (' . implode(',', $arrChunk) . ')'; + if ($strExtraWhere !== '') { + $sql .= ' AND (' . $strExtraWhere . ')'; + } + if ($objDatabase->fxQuery($sql) !== false) { + $intTotal += intval($objDatabase->affected_rows); + } + if ($intPauseUs > 0 && ($i + $intChunk) < $intN) { + usleep($intPauseUs); + } + } + + return $intTotal; +} + /** * MSIN-4482 — efface UNIQUEMENT les données d'import (par_origine=import_fichier) de l'événement. * Ne touche pas aux inscriptions en ligne / panier réel / inscriptions_questions. * Exige confirm = "RESET". + * Soft sur le serveur : lots + micro-pauses (pas un DELETE géant qui couche MySQL). */ function fxImportResultatsReset($intEveId, $strConfirm) { global $objDatabase; - @set_time_limit(300); + @set_time_limit(600); $intEveId = intval($intEveId); if ($intEveId <= 0) { @@ -2878,11 +2961,16 @@ function fxImportResultatsReset($intEveId, $strConfirm) { ); } - $arrPrev = fxImportResultatsResetPreview($intEveId); - if (($arrPrev['state'] ?? '') !== 'ok') { - return $arrPrev; - } - if (intval($arrPrev['participants'] ?? 0) < 1) { + $strOrig = fxImportResultatsDbEsc(MSIN_IMPORT_RESULTATS_ORIGIN); + + // Comptage léger seulement (pas le preview multi-JOIN avant le gros travail) + $tabCnt = $objDatabase->fxGetResults( + "SELECT COUNT(*) AS n FROM resultats_participants" + . " WHERE eve_id = " . $intEveId + . " AND par_origine = '" . $strOrig . "'" + ); + $intParCnt = is_array($tabCnt) ? intval($tabCnt[1]['n'] ?? 0) : 0; + if ($intParCnt < 1) { return array( 'state' => 'ok', 'message' => 'Rien à effacer — aucun participant import_fichier sur cet événement.', @@ -2892,68 +2980,70 @@ function fxImportResultatsReset($intEveId, $strConfirm) { ); } - $strOrig = fxImportResultatsDbEsc(MSIN_IMPORT_RESULTATS_ORIGIN); - $intDelQue = 0; - $intDelPar = 0; - $intDelPec = 0; - - // Sous-requête IDs participants import (évite JOIN DELETE lents / gros IN PHP) - $strSubPar = "SELECT pid FROM (" - . " SELECT par_id AS pid FROM resultats_participants" - . " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'" - . " UNION" - . " SELECT par_id_original AS pid FROM resultats_participants" - . " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'" - . " AND par_id_original > 0" - . ") _ms1_imp_par"; - $strSubPec = "SELECT pid FROM (" - . " SELECT pec_id AS pid FROM resultats_participants" - . " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'" - . " AND pec_id > 0" - . ") _ms1_imp_pec"; - - // 1) Réponses questions des participants import seulement - $sqlDelQue = "DELETE FROM resultats_questions WHERE par_id IN (" . $strSubPar . ")" - . " OR pec_id IN (" . $strSubPec . ")"; - if ($objDatabase->fxQuery($sqlDelQue) !== false) { - $intDelQue = intval($objDatabase->affected_rows); + // Charger les IDs en mémoire (SELECT léger) puis DELETE par lots sur PK/index + $tabRows = $objDatabase->fxGetResults( + "SELECT par_id, par_id_original, pec_id FROM resultats_participants" + . " WHERE eve_id = " . $intEveId + . " AND par_origine = '" . $strOrig . "'" + ); + $arrParIds = array(); + $arrPecIds = array(); + if (is_array($tabRows)) { + $intMax = fxcountpatch($tabRows); + for ($i = 1; $i <= $intMax; $i++) { + $intParId = intval($tabRows[$i]['par_id'] ?? 0); + $intParOrig = intval($tabRows[$i]['par_id_original'] ?? 0); + $intPecId = intval($tabRows[$i]['pec_id'] ?? 0); + if ($intParId > 0) { + $arrParIds[] = $intParId; + } + if ($intParOrig > 0) { + $arrParIds[] = $intParOrig; + } + if ($intPecId > 0) { + $arrPecIds[] = $intPecId; + } + } } + $arrParIds = array_values(array_unique($arrParIds)); + $arrPecIds = array_values(array_unique($arrPecIds)); + + // Lots modestes : ~200 IDs / 40 ms — laisse InnoDB digérer sans saturer le pool + $intChunk = 200; + $intPause = 40000; + + // 1) Questions (souvent le plus gros volume) — deux passes indexées, pas de OR monolithe + $intDelQue = fxImportResultatsDeleteBatchedIn('resultats_questions', 'par_id', $arrParIds, $intChunk, $intPause); + $intDelQue += fxImportResultatsDeleteBatchedIn('resultats_questions', 'pec_id', $arrPecIds, $intChunk, $intPause); // 2) Produits éventuels - @$objDatabase->fxQuery( - "DELETE FROM resultats_produits_new WHERE par_id IN (" . $strSubPar . ")" - . " OR pec_id IN (" . $strSubPec . ")" + fxImportResultatsDeleteBatchedIn('resultats_produits_new', 'par_id', $arrParIds, $intChunk, $intPause); + fxImportResultatsDeleteBatchedIn('resultats_produits_new', 'pec_id', $arrPecIds, $intChunk, $intPause); + + // 3) Coquilles pec (avant participants) — toujours borné à eve_id + $strEveWhere = 'eve_id = ' . $intEveId; + $intDelPec = 0; + $intDelPec += fxImportResultatsDeleteBatchedIn('resultats_epreuves_commandees', 'pec_id', $arrPecIds, $intChunk, $intPause, $strEveWhere); + $intDelPec += fxImportResultatsDeleteBatchedIn('resultats_epreuves_commandees', 'pec_id_original', $arrPecIds, $intChunk, $intPause, $strEveWhere); + $strPrefix = 'I' . $intEveId . 'X%'; + $intDelPec += fxImportResultatsDeleteBatchedLimit( + "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId + . " AND no_panier LIKE '" . fxImportResultatsDbEsc($strPrefix) . "'", + $intChunk, + $intPause ); - // 3) Coquilles pec liées à l'import (avant delete participants) - $sqlDelPec = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId - . " AND pec_id IN (" . $strSubPec . ")"; - if ($objDatabase->fxQuery($sqlDelPec) !== false) { - $intDelPec += intval($objDatabase->affected_rows); - } - // Aussi pec_id_original = pec participant - $sqlDelPecOrig = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId - . " AND pec_id_original IN (" . $strSubPec . ")"; - if ($objDatabase->fxQuery($sqlDelPecOrig) !== false) { - $intDelPec += intval($objDatabase->affected_rows); - } - $strPrefix = 'I' . $intEveId . 'X%'; - $sqlDelPec2 = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId - . " AND no_panier LIKE '" . fxImportResultatsDbEsc($strPrefix) . "'"; - if ($objDatabase->fxQuery($sqlDelPec2) !== false) { - $intDelPec += intval($objDatabase->affected_rows); - } - - // 4) Participants import - $sqlDelPar = "DELETE FROM resultats_participants WHERE eve_id = " . $intEveId - . " AND par_origine = '" . $strOrig . "'"; - if ($objDatabase->fxQuery($sqlDelPar) !== false) { - $intDelPar = intval($objDatabase->affected_rows); - } + // 4) Participants import — LIMIT pour ne pas locker la table entière d’un coup + $intDelPar = fxImportResultatsDeleteBatchedLimit( + "DELETE FROM resultats_participants WHERE eve_id = " . $intEveId + . " AND par_origine = '" . $strOrig . "'", + $intChunk, + $intPause + ); return array( 'state' => 'ok', - 'message' => 'Reset import OK — participants ' . $intDelPar + 'message' => 'Reset import OK (par lots) — participants ' . $intDelPar . ', pec ' . $intDelPec . ', réponses questions ' . $intDelQue . '. Catalogue questions événement intact. Inscriptions en ligne intactes.', @@ -4450,7 +4540,7 @@ function fxImportResultatsRenderEventPanel($intEveId) { $('#ms1_import_reset_data').prop('disabled', true); setBusy(true, 'Reset import en cours…', - 'Suppression de milliers de lignes — 30 s à 2 min selon la charge. Le chrono tourne = ça travaille.' + 'Suppression par petits lots (serveur protégé) — peut prendre 1–3 min. Le chrono tourne = OK.' ); showResetFlash('warning', 'Reset en cours — ne fermez pas la page.'); $.ajax({ diff --git a/php/inc_settings.php b/php/inc_settings.php index 555e891..bc25f34 100644 --- a/php/inc_settings.php +++ b/php/inc_settings.php @@ -7,7 +7,7 @@ * Constantes * * **************/ -define('_VERSION_CODE', '4.72.957'); // MSIN-4482 — Actualiser compteurs import (SQL preview rapide) +define('_VERSION_CODE', '4.72.958'); // MSIN-4482 — reset import par lots (ne plus saturer MySQL) define('_DATE_CODE', '2026-08-04'); //MSIN-4290 define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe');