PDO::ERRMODE_EXCEPTION] ); } catch (Throwable $e) { fwrite(STDERR, 'migrate-dev-schema: ' . $e->getMessage() . PHP_EOL); exit(1); } function columnExists(PDO $pdo, string $table, string $column): bool { $stmt = $pdo->prepare( 'SELECT COUNT(*) FROM information_schema.COLUMNS ' . 'WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t AND COLUMN_NAME = :c' ); $stmt->execute(['t' => $table, 'c' => $column]); return (int)$stmt->fetchColumn() > 0; } $alters = []; if (!columnExists($pdo, 'subscriptions', 'source_order_id')) { $alters[] = 'ALTER TABLE subscriptions ADD COLUMN source_order_id BIGINT UNSIGNED DEFAULT NULL AFTER plan_id'; $alters[] = 'ALTER TABLE subscriptions ADD KEY source_order (source_order_id)'; } if (!columnExists($pdo, 'pay_orders', 'refund_status')) { $alters[] = "ALTER TABLE pay_orders ADD COLUMN refund_status ENUM('none','pending','approved','rejected') NOT NULL DEFAULT 'none' AFTER status"; $alters[] = 'ALTER TABLE pay_orders ADD COLUMN refund_reason TEXT DEFAULT NULL AFTER refund_status'; $alters[] = 'ALTER TABLE pay_orders ADD COLUMN refund_note TEXT DEFAULT NULL AFTER refund_reason'; $alters[] = 'ALTER TABLE pay_orders ADD COLUMN cancelled_at DATETIME DEFAULT NULL AFTER paid_at'; $alters[] = 'ALTER TABLE pay_orders ADD COLUMN refunded_at DATETIME DEFAULT NULL AFTER cancelled_at'; $alters[] = 'ALTER TABLE pay_orders ADD KEY refund_status (refund_status)'; } if (!columnExists($pdo, 'plans', 'description')) { $alters[] = 'ALTER TABLE plans ADD COLUMN description VARCHAR(255) DEFAULT NULL AFTER name'; $alters[] = 'ALTER TABLE plans ADD COLUMN max_files INT UNSIGNED NOT NULL DEFAULT 0 AFTER quota_mb'; $alters[] = 'ALTER TABLE plans ADD COLUMN features TEXT DEFAULT NULL AFTER duration_days'; $alters[] = 'ALTER TABLE plans ADD COLUMN sort_order INT NOT NULL DEFAULT 0 AFTER features'; $alters[] = 'ALTER TABLE plans ADD COLUMN is_recommended TINYINT(1) NOT NULL DEFAULT 0 AFTER sort_order'; } if (!columnExists($pdo, 'users', 'admin_level')) { $alters[] = "ALTER TABLE users ADD COLUMN admin_level ENUM('full','ops') DEFAULT NULL AFTER role"; } if (!columnExists($pdo, 'soon_files', 'thumb')) { $alters[] = 'ALTER TABLE soon_files ADD COLUMN thumb MEDIUMTEXT DEFAULT NULL AFTER version'; } foreach ($alters as $sql) { $pdo->exec($sql); fwrite(STDOUT, "migrate-dev-schema: {$sql}" . PHP_EOL); } if (columnExists($pdo, 'users', 'admin_level')) { $pdo->exec( "UPDATE users SET admin_level = 'full' WHERE role = 'admin' AND (admin_level IS NULL OR admin_level = '')" ); fwrite(STDOUT, 'migrate-dev-schema: admin_level backfill for existing admins' . PHP_EOL); } if (columnExists($pdo, 'plans', 'description')) { $legacyCodes = ['pro_monthly' => 'member_monthly', 'pro_yearly' => 'member_yearly']; foreach ($legacyCodes as $oldCode => $newCode) { $stmt = $pdo->prepare('SELECT id FROM plans WHERE code = :c LIMIT 1'); $stmt->execute(['c' => $oldCode]); if ($stmt->fetch()) { $conflict = $pdo->prepare('SELECT id FROM plans WHERE code = :c LIMIT 1'); $conflict->execute(['c' => $newCode]); if ($conflict->fetch()) { $pdo->prepare('UPDATE plans SET is_active = 0 WHERE code = :c')->execute(['c' => $oldCode]); } else { $pdo->prepare('UPDATE plans SET code = :new WHERE code = :old')->execute(['new' => $newCode, 'old' => $oldCode]); } } } $quota = 2048; $maxFiles = 200; $seed = [ ['free', '免费版', '免费体验设计与编辑', 0, $quota, $maxFiles, 0, '["设计与编辑工具免费使用"]', 0, 0, 1], ['member_lifetime', '永久会员', '一次激活,永久使用预览交付能力', 100, $quota, $maxFiles, 0, '["高清预览","成品打印","云端保存","导出设计文件","永久有效"]', 10, 1, 1], ['member_monthly', '月度订阅', '按月灵活使用,随时续订', 1999, $quota, $maxFiles, 30, '["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 个月"]', 20, 0, 0], ['member_quarterly', '季度订阅', '连续三个月,更省心', 5299, $quota, $maxFiles, 90, '["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 季"]', 25, 0, 0], ['member_yearly', '年度订阅', '全年畅享,性价比更高', 19999, $quota, $maxFiles, 365, '["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 年"]', 30, 0, 0], ]; $upsert = $pdo->prepare( 'INSERT INTO plans (code, name, description, price_cents, quota_mb, max_files, duration_days, features, sort_order, is_recommended, is_active) ' . 'VALUES (:c, :n, :d, :p, :q, :mf, :dd, :f, :so, :ir, :a) ' . 'ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), price_cents=VALUES(price_cents), ' . 'quota_mb=VALUES(quota_mb), max_files=VALUES(max_files), duration_days=VALUES(duration_days), ' . 'features=VALUES(features), sort_order=VALUES(sort_order), is_recommended=VALUES(is_recommended), is_active=VALUES(is_active)' ); foreach ($seed as $row) { $upsert->execute([ 'c' => $row[0], 'n' => $row[1], 'd' => $row[2], 'p' => $row[3], 'q' => $row[4], 'mf' => $row[5], 'dd' => $row[6], 'f' => $row[7], 'so' => $row[8], 'ir' => $row[9], 'a' => $row[10], ]); } fwrite(STDOUT, 'migrate-dev-schema: membership plans catalog synced' . PHP_EOL); $legacyFeatStmt = $pdo->query( 'SELECT code, duration_days FROM plans WHERE features LIKE \'%预览成品效果%\' ' . 'OR features LIKE \'%到期后恢复为非会员%\' OR features LIKE \'%会员权益一致%\' ' . 'OR name LIKE \'%会员 ·%\'' ); $featUpd = $pdo->prepare('UPDATE plans SET features = :f WHERE code = :c'); $coreFeatures = ['高清预览', '成品打印', '云端保存', '导出设计文件']; foreach ($legacyFeatStmt->fetchAll() as $legacyRow) { $code = (string)$legacyRow['code']; $days = (int)$legacyRow['duration_days']; if ($code === 'free') { $featUpd->execute([ 'f' => '["设计与编辑工具免费使用"]', 'c' => $code, ]); continue; } $lines = $coreFeatures; if ($days > 0) { $period = $days >= 365 ? '1 年' : ($days === 90 ? '1 季' : ($days >= 30 && $days % 30 === 0 ? ((int)($days / 30)) . ' 个月' : $days . ' 天')); $lines[] = '订阅周期:' . $period; } $featUpd->execute([ 'f' => json_encode($lines, JSON_UNESCAPED_UNICODE), 'c' => $code, ]); } fwrite(STDOUT, 'migrate-dev-schema: legacy plan copy refreshed' . PHP_EOL); } $pdo->exec( 'CREATE TABLE IF NOT EXISTS `soon_templates` (' . '`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,' . '`name` VARCHAR(120) NOT NULL,' . '`type` TINYINT UNSIGNED NOT NULL DEFAULT 1,' . '`thumb` MEDIUMTEXT DEFAULT NULL,' . '`file_size` INT UNSIGNED NOT NULL DEFAULT 0,' . '`sort_order` INT NOT NULL DEFAULT 0,' . '`is_active` TINYINT(1) NOT NULL DEFAULT 1,' . '`created_at` DATETIME NOT NULL,' . '`updated_at` DATETIME NOT NULL,' . 'PRIMARY KEY (`id`),' . 'KEY `active_sort` (`is_active`, `sort_order`, `id`)' . ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' ); fwrite(STDOUT, 'migrate-dev-schema: soon_templates ensured' . PHP_EOL);