diff --git a/public/index.php b/public/index.php index 4256b0d..7bf22d6 100644 --- a/public/index.php +++ b/public/index.php @@ -173,15 +173,17 @@ $app->post('/api/table/update', function (Request $request, Response $response) $schema = $payload['schema']; $table = $payload['table']; $row = $payload['row']; + $originalRow = $payload['originalRow'] ?? null; $metaArr = $meta->getTableMeta($schema, $table); - $result = $ds->updateRow($schema, $table, $row, $metaArr['columns'], $metaArr['primaryKey']); + $result = $ds->updateRow($schema, $table, $row, $metaArr['columns'], $metaArr['primaryKey'], $originalRow); $response->getBody()->write(json_encode($result)); return $response->withHeader('Content-Type', 'application/json'); } catch (\Exception $e) { $error = ['error' => true, 'message' => $e->getMessage()]; $response->getBody()->write(json_encode($error)); - return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + $status = ((int)$e->getCode() === 409 || str_starts_with($e->getMessage(), 'CONFLICT:')) ? 409 : 400; + return $response->withHeader('Content-Type', 'application/json')->withStatus($status); } }); diff --git a/public/js/core.js b/public/js/core.js index 30aa27d..c0dc362 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -27,7 +27,9 @@ async function api(url, method = 'GET', body) { if (!res.ok) { // Сервер вернул JSON с ошибкой - throw new Error(data.message || data.error || 'Неизвестная ошибка'); + const rawMessage = data.message || data.error || 'Неизвестная ошибка'; + const cleanMessage = String(rawMessage).replace(/^CONFLICT:\s*/i, ''); + throw new Error(cleanMessage); } // Проверяем на ошибку в успешном ответе diff --git a/public/js/operations.js b/public/js/operations.js index 58a355b..8480a42 100644 --- a/public/js/operations.js +++ b/public/js/operations.js @@ -806,7 +806,8 @@ async function showEditModal(selectedRows) { await api('/api/table/update', 'POST', { schema: currentSchema, table: currentTable, - row: updatedRow + row: updatedRow, + originalRow: row }); updated++; } catch (err) { diff --git a/public/js/table.js b/public/js/table.js index d3d1cc1..a32ce99 100644 --- a/public/js/table.js +++ b/public/js/table.js @@ -566,7 +566,7 @@ async function selectTable(schema, tableName, restoreState = false) { }); // Функция сохранения строки - async function saveRow(rowPos, rowData, rowElement) { + async function saveRow(rowPos, rowData, rowElement, originalRowData = null) { console.log('💾 === СОХРАНЕНИЕ ==='); console.log(' Data:', rowData); @@ -581,7 +581,8 @@ async function selectTable(schema, tableName, restoreState = false) { const result = await api('/api/table/update', 'POST', { schema: currentSchema, table: currentTable, - row: rowData + row: rowData, + originalRow: originalRowData || rowData }); console.log('✅ СОХРАНЕНО:', result); @@ -694,9 +695,11 @@ async function selectTable(schema, tableName, restoreState = false) { clearTimeout(dirtyRows.get(rowPos).timeout); } + const originalRowData = { ...rowData, [cell.getField()]: oldValue }; + const timeout = setTimeout(async () => { console.log('⏰ Автосохранение...'); - await saveRow(rowPos, rowData, row); + await saveRow(rowPos, rowData, row, originalRowData); }, 2000); dirtyRows.set(rowPos, { data: rowData, element: row, timeout: timeout }); diff --git a/src/DataService.php b/src/DataService.php index c30043b..e31353b 100644 --- a/src/DataService.php +++ b/src/DataService.php @@ -209,7 +209,7 @@ private function formatPDOError(\PDOException $e, string $schema, string $table, } -public function updateRow(string $schema, string $table, array $row, array $columns, array $pk): array +public function updateRow(string $schema, string $table, array $row, array $columns, array $pk, ?array $originalRow = null): array { $schema = $this->validateIdentifier($schema, 'schema'); $table = $this->validateIdentifier($table, 'table'); @@ -248,6 +248,22 @@ public function updateRow(string $schema, string $table, array $row, array $colu $params[":pk_$name"] = $row[$name]; } + $trackedColumns = []; + if (is_array($originalRow)) { + foreach ($columns as $c) { + $name = $this->validateIdentifier((string)$c['COLUMN_NAME'], 'column'); + if (in_array($name, $pk, true)) { + continue; + } + if (!array_key_exists($name, $originalRow)) { + continue; + } + $whereParts[] = "`$name` <=> :orig_$name"; + $params[":orig_$name"] = $originalRow[$name]; + $trackedColumns[] = $name; + } + } + $sql = sprintf( "UPDATE `%s`.`%s` SET %s WHERE %s", $schema, $table, @@ -262,6 +278,44 @@ public function updateRow(string $schema, string $table, array $row, array $colu $stmt->execute($params); $rowCount = $stmt->rowCount(); error_log("Rows updated: $rowCount"); + + if ($rowCount === 0 && !empty($trackedColumns)) { + $pkWhere = []; + $selectParams = []; + foreach ($pk as $name) { + $name = $this->validateIdentifier((string)$name, 'column'); + $pkWhere[] = "`$name` = :pk_$name"; + $selectParams[":pk_$name"] = $row[$name]; + } + + $selectedColumns = array_merge($pk, $trackedColumns); + $selectSql = sprintf( + "SELECT %s FROM `%s`.`%s` WHERE %s LIMIT 1", + implode(', ', array_map(fn($col) => "`$col`", $selectedColumns)), + $schema, + $table, + implode(' AND ', $pkWhere) + ); + $selectStmt = $this->pdo->prepare($selectSql); + $selectStmt->execute($selectParams); + $currentRow = $selectStmt->fetch(PDO::FETCH_ASSOC); + + if ($currentRow === false) { + throw new \RuntimeException('CONFLICT: Запись была удалена другим пользователем.', 409); + } + + foreach ($trackedColumns as $name) { + $expected = $originalRow[$name] ?? null; + $actual = $currentRow[$name] ?? null; + if ($expected != $actual) { + throw new \RuntimeException( + 'CONFLICT: Запись была изменена в другой сессии. Обновите таблицу и повторите.', + 409 + ); + } + } + } + return ['updated' => $rowCount]; } catch (\PDOException $e) { error_log("UPDATE FAILED: " . $e->getMessage());