chore: восстановление репозитория из снапшота v0.3.1
Прежняя git-история утрачена при переносе проекта на машину владельца (снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной истории. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
1c85091186
184 changed files with 33303 additions and 0 deletions
95
server/lib/db.php
Normal file
95
server/lib/db.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Подключение к SQLite через PDO и идемпотентная init-миграция схемы.
|
||||
* Контракт: docs/INTERFACES.md, раздел «Личный кабинет (этап 8)».
|
||||
*/
|
||||
|
||||
/**
|
||||
* Путь к БД: server/config.php, при отсутствии — config.example.php
|
||||
* (который читает env GOLEARN_DB — так работает smoke-тест).
|
||||
*
|
||||
* @return array{db_path: string}
|
||||
*/
|
||||
function golearn_config(): array
|
||||
{
|
||||
$file = __DIR__ . '/../config.php';
|
||||
if (!is_file($file)) {
|
||||
$file = __DIR__ . '/../config.example.php';
|
||||
}
|
||||
$config = require $file;
|
||||
if (!is_array($config) || empty($config['db_path']) || !is_string($config['db_path'])) {
|
||||
throw new RuntimeException('config: нет db_path');
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ленивый синглтон PDO. PRAGMA по контракту: WAL, busy_timeout=5000,
|
||||
* foreign_keys=ON. Ошибки — только исключения (текст наружу не отдаём).
|
||||
*/
|
||||
function db(): PDO
|
||||
{
|
||||
static $pdo = null;
|
||||
if ($pdo instanceof PDO) {
|
||||
return $pdo;
|
||||
}
|
||||
$config = golearn_config();
|
||||
$pdo = new PDO('sqlite:' . $config['db_path'], null, null, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
$pdo->exec('PRAGMA journal_mode=WAL');
|
||||
$pdo->exec('PRAGMA busy_timeout=5000');
|
||||
$pdo->exec('PRAGMA foreign_keys=ON');
|
||||
golearn_migrate($pdo);
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Идемпотентная init-миграция схемы (IF NOT EXISTS).
|
||||
*/
|
||||
function golearn_migrate(PDO $pdo): void
|
||||
{
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
email TEXT UNIQUE COLLATE NOCASE,
|
||||
pass_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
)'
|
||||
);
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS progress (
|
||||
user_id INTEGER PRIMARY KEY REFERENCES users(id),
|
||||
payload TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)'
|
||||
);
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS rate_log (
|
||||
ip TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
ts INTEGER NOT NULL
|
||||
)'
|
||||
);
|
||||
$pdo->exec(
|
||||
'CREATE INDEX IF NOT EXISTS idx_rate_log_ip_action_ts
|
||||
ON rate_log (ip, action, ts)'
|
||||
);
|
||||
// Этап 8.1: токены восстановления пароля (аддитивно, IF NOT EXISTS).
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS password_resets (
|
||||
id INTEGER PRIMARY KEY,
|
||||
email TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
expires_at INTEGER NOT NULL
|
||||
)'
|
||||
);
|
||||
$pdo->exec(
|
||||
'CREATE INDEX IF NOT EXISTS idx_password_resets_token_hash
|
||||
ON password_resets (token_hash)'
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue