diff --git a/cms/lib/classes/AcaiAPI.php b/cms/lib/classes/AcaiAPI.php index aa9e21e..a8102fe 100644 --- a/cms/lib/classes/AcaiAPI.php +++ b/cms/lib/classes/AcaiAPI.php @@ -129,7 +129,7 @@ class AcaiAPI 'order' => time() + $i, 'width' => 640, 'height' => 480, - ], [], ['ignoreSchema' => true, 'prefix' => $prefix]); + ], [], ['ignoreSchema' => true, 'prefix' => $prefix, 'skipTouchParent' => true]); } } } @@ -154,6 +154,15 @@ class AcaiAPI self::updateCategoryMetadata($table, $prefix); } + // Fotos nuevas = registro cambiado (ver "FOTOS -> FECHA DEL REGISTRO"). + // skipTouchParent: las que registra el propio insert/update del padre, + // que ya sello su fecha en el mismo SET. + if ($table === 'uploads' && $count && empty($options['skipTouchParent'])) { + $pairs = []; + foreach ($records as $r) $pairs[] = [$r['tableName'] ?? '', $r['recordNum'] ?? '']; + self::touchUploadParents($pairs, $prefix); + } + return !empty($options['return_last_id']) ? $lastId : $count; } @@ -198,6 +207,11 @@ class AcaiAPI if ($whereSql === null || $whereSql === '') return 0; + // Fotos cambiadas = registro cambiado: los padres de ANTES (y abajo los + // de despues, por si la foto cambia de registro). + $touchParents = ($table === 'uploads' && empty($options['skipTouchParent'])); + $parents = $touchParents ? self::uploadParents($where, $prefix) : []; + $count = 0; foreach ($records as $record) { @@ -273,7 +287,7 @@ class AcaiAPI 'order' => time() + $i, 'width' => 640, 'height' => 480, - ], [], ['ignoreSchema' => true, 'prefix' => $prefix]); + ], [], ['ignoreSchema' => true, 'prefix' => $prefix, 'skipTouchParent' => true]); } } } @@ -283,6 +297,10 @@ class AcaiAPI self::updateCategoryMetadata($table, $prefix); } + if ($touchParents && $count) { + self::touchUploadParents(array_merge($parents, self::uploadParents($where, $prefix)), $prefix); + } + return $count; } @@ -313,8 +331,14 @@ class AcaiAPI self::error(['info' => $sql, 'params' => $params]); } + // Fotos borradas = registro cambiado: hay que saber de quien eran ANTES. + $parents = ($table === 'uploads' && empty($options['skipTouchParent'])) + ? self::uploadParents($where, $prefix) : []; + $db = Db::getInstance(); - return $db->execute($sql, $params ? $params : null) !== false; + $ok = $db->execute($sql, $params ? $params : null) !== false; + if ($ok && $parents) self::touchUploadParents($parents, $prefix); + return $ok; } @@ -1343,6 +1367,126 @@ class AcaiAPI } + // ═══════════════════════════════════════════════════════════════════ + // FOTOS -> FECHA DEL REGISTRO + // ═══════════════════════════════════════════════════════════════════ + // + // Cambiar las fotos de un registro (filas de `uploads` con + // tableName/recordNum) NO tocaba su `updatedDate`: el sync de Forge decide + // por esa fecha que esta pendiente, y un registro al que solo se le + // cambiaban las fotos (desde VSCode, el agente o el panel de fotos) nunca + // se publicaba (reydimobel, 2026-09-26: siete categorias con las fotos + // sustituidas en local y produccion enseñando las antiguas, en 404). + // Regla: toda escritura en `uploads` sella la fecha de los registros a los + // que pertenecen esas fotos. insert/update/delete de AcaiAPI lo hacen + // solos; quien escriba `uploads` por SQL directo llama a + // `touchUploadParents` (builder_saas/mcp_respond.php). + + /** Cache por peticion: tabla completa => [columna => true] | null. */ + private static $auditColsCache = []; + + /** + * Los registros `[tableName, recordNum]` (sin repetir) a los que pertenecen + * las fotos de `uploads` que cumplen `$where` (mismos formatos que + * buildWhere). Las fotos sin registro todavia (recordNum vacio, las + * temporales de preSaveTempId) no cuentan. Nunca lanza: [] si falla. + */ + static function uploadParents($where, $prefix = null) + { + global $TABLE_PREFIX; + $prefix = $prefix === null ? $TABLE_PREFIX : $prefix; + $params = []; + $whereSql = self::buildWhere($where, $params); + if (!$whereSql) return []; + try { + $rows = Db::getInstance()->executeS( + "SELECT DISTINCT tableName, recordNum FROM `{$prefix}uploads` WHERE {$whereSql}", + $params ?: null + ); + } catch (\Throwable $e) { + return []; + } + $pairs = []; + foreach ((array) $rows as $row) { + if (is_array($row)) $pairs[] = [$row['tableName'] ?? '', $row['recordNum'] ?? '']; + } + return $pairs; + } + + /** + * Sella `updatedDate` (y `updatedByUserNum` si existe) de los registros + * `[[tableName, recordNum], ...]`. Salta lo que no es un registro de + * verdad: tabla con nombre raro, recordNum no numerico o 0, tabla sin + * columna `updatedDate`. Nunca lanza ni corta al llamador: un fallo aqui + * no puede impedir guardar una foto. Devuelve cuantos registros sello. + */ + static function touchUploadParents($pairs, $prefix = null) + { + global $TABLE_PREFIX; + $prefix = $prefix === null ? $TABLE_PREFIX : $prefix; + $porTabla = []; + foreach ((array) $pairs as $pair) { + $tabla = is_array($pair) ? (string) ($pair[0] ?? '') : ''; + $num = is_array($pair) ? (string) ($pair[1] ?? '') : ''; + if (!preg_match('/^[A-Za-z0-9_]+$/', $tabla) || !ctype_digit($num) || (int) $num === 0) continue; + $porTabla[$tabla][(int) $num] = true; + } + $sellados = 0; + foreach ($porTabla as $tabla => $nums) { + $sellados += self::touchRecords($prefix . $tabla, array_keys($nums)); + } + if ($sellados) self::$requestGetCache = []; + return $sellados; + } + + private static function touchRecords($fullTable, array $nums) + { + try { + $db = Db::getInstance(); + $cols = self::auditColumns($db, $fullTable); + if (!$cols || !isset($cols['updatedDate'])) return 0; + $set = ['`updatedDate` = ?']; + $params = [date('Y-m-d H:i:s')]; + if (isset($cols['updatedByUserNum'])) { + $user = (class_exists('API') && isset(API::$user['num'])) ? ((int) API::$user['num'] ?: 1) : 1; + $set[] = '`updatedByUserNum` = ?'; + $params[] = $user; + } + $ph = implode(',', array_fill(0, count($nums), '?')); + $ok = $db->execute( + "UPDATE `{$fullTable}` SET " . implode(', ', $set) . " WHERE num IN ($ph)", + array_merge($params, $nums) + ); + return $ok === false ? 0 : count($nums); + } catch (\Throwable $e) { + return 0; + } + } + + /** `[columna => true]` de las columnas de auditoria de `$fullTable`, o null. */ + private static function auditColumns($db, $fullTable) + { + if (array_key_exists($fullTable, self::$auditColsCache)) { + return self::$auditColsCache[$fullTable]; + } + try { + $rows = $db->executeS( + "SELECT COLUMN_NAME FROM information_schema.COLUMNS" + . " WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?" + . " AND COLUMN_NAME IN ('updatedDate', 'updatedByUserNum')", + [$fullTable] + ); + } catch (\Throwable $e) { + return self::$auditColsCache[$fullTable] = null; + } + $cols = []; + foreach ((array) $rows as $row) { + if (is_array($row) && !empty($row['COLUMN_NAME'])) $cols[$row['COLUMN_NAME']] = true; + } + return self::$auditColsCache[$fullTable] = $cols; + } + + // ═══════════════════════════════════════════════════════════════════ // WRITE HELPERS (private) // ═══════════════════════════════════════════════════════════════════