chore: init saasshop repo + sql migrations runner + gitee go

This commit is contained in:
萝卜
2026-03-10 11:31:02 +00:00
commit 50f15cdea8
210 changed files with 29534 additions and 0 deletions

138
scripts/sql_migrate.php Normal file
View File

@@ -0,0 +1,138 @@
<?php
/**
* SQL 结构迁移执行器(轻量版)
*
* 目标:让数据库结构变更以 "V{n}__xxx.sql" 脚本形式沉淀(仅结构,不推业务数据)。
* 使用php scripts/sql_migrate.php
*
* 约定:
* - 脚本目录database/migrations
* - 脚本命名V1__xxx.sql, V2__xxx.sql ...(按数字升序执行)
* - 记录表schema_sql_migrations记录已执行版本
*/
$baseDir = realpath(__DIR__ . '/../database/migrations');
if ($baseDir === false) {
fwrite(STDERR, "SQL migrations dir not found.\n");
exit(1);
}
// 读取 .env尽量少依赖 Laravel 容器,便于 CI/部署直接跑)
$envPath = realpath(__DIR__ . '/../.env');
$env = [];
if ($envPath && file_exists($envPath)) {
foreach (file($envPath, FILE_IGNORE_NEW_LINES) as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
if (!str_contains($line, '=')) {
continue;
}
[$k, $v] = explode('=', $line, 2);
$k = trim($k);
$v = trim($v);
$v = trim($v, "\"'");
$env[$k] = $v;
}
}
// env 变量优先级:系统环境变量 > .env
$getEnv = function (string $key, string $default = '') use ($env): string {
$v = getenv($key);
if ($v !== false && $v !== '') {
return (string) $v;
}
return (string) ($env[$key] ?? $default);
};
$driver = $getEnv('DB_CONNECTION', 'mysql');
$host = $getEnv('DB_HOST', '127.0.0.1');
$port = $getEnv('DB_PORT', $driver === 'pgsql' ? '5432' : ($driver === 'sqlite' ? '' : '3306'));
$db = $getEnv('DB_DATABASE', '');
$user = $getEnv('DB_USERNAME', '');
$pass = $getEnv('DB_PASSWORD', '');
if ($driver === 'sqlite') {
// sqlite: DB_DATABASE 可能是绝对路径
$dsn = $db !== '' ? "sqlite:" . $db : 'sqlite::memory:';
} elseif ($driver === 'pgsql') {
$dsn = "pgsql:host={$host};port={$port};dbname={$db}";
} else {
// mysql
$charset = $getEnv('DB_CHARSET', 'utf8mb4');
$dsn = "mysql:host={$host};port={$port};dbname={$db};charset={$charset}";
}
try {
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (Throwable $e) {
fwrite(STDERR, "DB connect failed: {$e->getMessage()}\n");
exit(2);
}
// 确保迁移记录表存在
// applied_at 使用 TEXT确保 sqlite/mysql 下都可用(避免 DATETIME 在部分方言下不兼容)
$pdo->exec("CREATE TABLE IF NOT EXISTS schema_sql_migrations (\n version VARCHAR(50) PRIMARY KEY,\n description VARCHAR(255) NULL,\n applied_at TEXT NOT NULL\n)");
$applied = [];
foreach ($pdo->query('SELECT version FROM schema_sql_migrations') as $row) {
$applied[(string) $row['version']] = true;
}
$files = glob($baseDir . '/V*__*.sql');
sort($files);
$pending = [];
foreach ($files as $file) {
$name = basename($file);
if (!preg_match('/^(V\d+)__.+\.sql$/', $name, $m)) {
continue;
}
$version = $m[1];
if (isset($applied[$version])) {
continue;
}
$pending[] = ['version' => $version, 'file' => $file, 'name' => $name];
}
if (count($pending) === 0) {
fwrite(STDOUT, "No pending SQL migrations.\n");
exit(0);
}
foreach ($pending as $item) {
$sql = file_get_contents($item['file']);
$sql = $sql === false ? '' : $sql;
fwrite(STDOUT, "Applying {$item['name']} ...\n");
$pdo->beginTransaction();
try {
// 简单处理:按分号执行(对于包含存储过程等复杂语法的脚本,需拆分策略升级)
$statements = array_filter(array_map('trim', preg_split('/;\s*\n/', $sql)));
foreach ($statements as $stmt) {
if ($stmt === '' || str_starts_with(ltrim($stmt), '--')) {
continue;
}
$pdo->exec($stmt);
}
$desc = preg_replace('/^V\d+__/', '', $item['name']);
$desc = preg_replace('/\.sql$/', '', $desc);
$stmt = $pdo->prepare('INSERT INTO schema_sql_migrations(version, description, applied_at) VALUES(?, ?, ?)');
$stmt->execute([$item['version'], $desc, date('Y-m-d H:i:s')]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
fwrite(STDERR, "Failed {$item['name']}: {$e->getMessage()}\n");
exit(3);
}
}
fwrite(STDOUT, "SQL migrations applied: " . count($pending) . "\n");