Files

384 lines
13 KiB
PHP
Raw Permalink Normal View History

2026-06-14 14:00:24 +08:00
<?php
/**
* 站点域名注册表(dashboard 管理,ip_check 按 Host 解析配置)
*/
class DomainRepository
{
public static function ensureTable(PDO $pdo)
{
$pdo->exec(
"CREATE TABLE IF NOT EXISTS `cloak_site_domains` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`hostname` varchar(255) NOT NULL COMMENT '站点域名,不含协议与路径',
`config_name` varchar(64) NOT NULL DEFAULT '' COMMENT '关联 check_config 配置名',
`cf_zone_id` varchar(64) DEFAULT NULL COMMENT 'Cloudflare Zone ID',
`cf_nameservers` text DEFAULT NULL COMMENT 'Cloudflare NS JSON',
`cf_status` varchar(32) NOT NULL DEFAULT 'local' COMMENT 'Cloudflare 状态',
`cf_error` varchar(500) DEFAULT NULL COMMENT '最近错误',
`cf_checked_at` datetime DEFAULT NULL COMMENT '上次检测时间',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_hostname` (`hostname`),
KEY `idx_config_name` (`config_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
);
self::ensureCloudflareColumns($pdo);
2026-07-06 10:34:34 +08:00
self::ensureConfigDomainMapTable($pdo);
}
/**
* 配置 → 域名(多配置可共用同一域名)
*/
public static function ensureConfigDomainMapTable(PDO $pdo)
{
$pdo->exec(
"CREATE TABLE IF NOT EXISTS `cloak_config_domain` (
`config_name` varchar(64) NOT NULL COMMENT 'check_config 配置名',
`domain_id` int(11) UNSIGNED NOT NULL COMMENT 'cloak_site_domains.id',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`config_name`),
KEY `idx_domain_id` (`domain_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
);
self::migrateLegacyDomainBindings($pdo);
}
/**
* 将 cloak_site_domains.config_name 旧数据迁入 cloak_config_domain(幂等)
*/
private static function migrateLegacyDomainBindings(PDO $pdo)
{
$st = $pdo->query(
"SELECT id, config_name FROM cloak_site_domains WHERE config_name IS NOT NULL AND config_name != ''"
);
if (!$st) {
return;
}
$ins = $pdo->prepare(
'INSERT IGNORE INTO cloak_config_domain (config_name, domain_id) VALUES (?, ?)'
);
$clr = $pdo->prepare("UPDATE cloak_site_domains SET config_name = '' WHERE id = ?");
while ($row = $st->fetch()) {
$cfg = trim((string) ($row['config_name'] ?? ''));
$id = (int) ($row['id'] ?? 0);
if ($cfg === '' || $id <= 0) {
continue;
}
$ins->execute([$cfg, $id]);
$clr->execute([$id]);
}
2026-06-14 14:00:24 +08:00
}
private static function ensureCloudflareColumns(PDO $pdo)
{
$columns = [
'cf_zone_id' => "ADD COLUMN `cf_zone_id` varchar(64) DEFAULT NULL COMMENT 'Cloudflare Zone ID'",
'cf_nameservers' => "ADD COLUMN `cf_nameservers` text DEFAULT NULL COMMENT 'Cloudflare NS JSON'",
'cf_status' => "ADD COLUMN `cf_status` varchar(32) NOT NULL DEFAULT 'local' COMMENT 'Cloudflare 状态'",
'cf_error' => "ADD COLUMN `cf_error` varchar(500) DEFAULT NULL COMMENT '最近错误'",
'cf_checked_at' => "ADD COLUMN `cf_checked_at` datetime DEFAULT NULL COMMENT '上次检测时间'",
];
foreach ($columns as $name => $ddl) {
if (!self::columnExists($pdo, 'cloak_site_domains', $name)) {
$pdo->exec('ALTER TABLE `cloak_site_domains` ' . $ddl);
}
}
}
private static function columnExists(PDO $pdo, $table, $column)
{
$st = $pdo->prepare(
'SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?'
);
$st->execute([DB_NAME, $table, $column]);
return (int) $st->fetchColumn() > 0;
}
private static function selectColumns()
{
return 'id, hostname, config_name, cf_zone_id, cf_nameservers, cf_status, cf_error, cf_checked_at, created_at';
}
public static function pdo()
{
return new PDO(
'mysql:host=127.0.0.1;dbname=' . DB_NAME . ';charset=utf8mb4',
DB_USERNAME,
DB_PASSWORD,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
}
public static function normalizeHostname($host)
{
$host = trim((string) $host);
if ($host === '') {
return '';
}
if (strpos($host, '://') !== false) {
$parsed = parse_url($host, PHP_URL_HOST);
$host = $parsed ?: $host;
} else {
$host = preg_replace('#/.*$#', '', $host);
$host = preg_replace('#:\d+$#', '', $host);
}
$host = strtolower(trim($host, '.'));
if (str_starts_with($host, 'www.')) {
$host = substr($host, 4);
}
return $host;
}
2026-07-06 10:34:34 +08:00
/**
* 从多行文本解析域名列表(每行一个,也支持逗号/分号分隔)
*
* @return string[]
*/
public static function parseHostnameBatch($text)
{
$text = str_replace(["\r\n", "\r"], "\n", trim((string) $text));
if ($text === '') {
return [];
}
$parts = preg_split('/[\n,;]+/', $text);
$seen = [];
$out = [];
foreach ($parts as $part) {
$host = self::normalizeHostname(trim($part));
if ($host === '' || isset($seen[$host])) {
continue;
}
if (!preg_match('/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i', $host)) {
continue;
}
$seen[$host] = true;
$out[] = $host;
}
return $out;
}
2026-06-14 14:00:24 +08:00
public static function hostToConfigKey($host)
{
$host = self::normalizeHostname($host);
if ($host === '') {
return 'index';
}
$key = preg_replace('/[^a-z0-9_-]+/i', '_', $host);
$key = trim($key, '_');
return $key !== '' ? $key : 'index';
}
public static function listAll(PDO $pdo = null)
{
$pdo = $pdo ?: self::pdo();
self::ensureTable($pdo);
$st = $pdo->query('SELECT ' . self::selectColumns() . ' FROM cloak_site_domains ORDER BY hostname ASC');
return $st->fetchAll();
}
public static function findById(PDO $pdo, $id)
{
self::ensureTable($pdo);
$st = $pdo->prepare('SELECT ' . self::selectColumns() . ' FROM cloak_site_domains WHERE id = ? LIMIT 1');
$st->execute([(int) $id]);
$row = $st->fetch();
return $row ?: null;
}
public static function updateCloudflareMeta(PDO $pdo, $id, array $fields)
{
self::ensureTable($pdo);
$allowed = ['cf_zone_id', 'cf_nameservers', 'cf_status', 'cf_error', 'cf_checked_at'];
$sets = [];
$vals = [];
foreach ($allowed as $key) {
if (array_key_exists($key, $fields)) {
$sets[] = "`{$key}` = ?";
$vals[] = $fields[$key];
}
}
if ($sets === []) {
return false;
}
$vals[] = (int) $id;
$sql = 'UPDATE cloak_site_domains SET ' . implode(', ', $sets) . ' WHERE id = ?';
$st = $pdo->prepare($sql);
$st->execute($vals);
return $st->rowCount() > 0;
}
public static function findByHostname(PDO $pdo, $hostname)
{
self::ensureTable($pdo);
$host = self::normalizeHostname($hostname);
if ($host === '') {
return null;
}
$st = $pdo->prepare('SELECT ' . self::selectColumns() . ' FROM cloak_site_domains WHERE hostname = ? LIMIT 1');
$st->execute([$host]);
$row = $st->fetch();
return $row ?: null;
}
public static function add(PDO $pdo, $hostname)
{
self::ensureTable($pdo);
$host = self::normalizeHostname($hostname);
if ($host === '' || !preg_match('/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i', $host)) {
return ['ok' => false, 'error' => '域名格式无效'];
}
try {
$st = $pdo->prepare('INSERT INTO cloak_site_domains (hostname, config_name) VALUES (?, ?)');
$st->execute([$host, '']);
return ['ok' => true, 'id' => (int) $pdo->lastInsertId(), 'hostname' => $host];
} catch (PDOException $e) {
if ((int) $e->getCode() === 23000) {
return ['ok' => false, 'error' => '该域名已存在'];
}
throw $e;
}
}
public static function deleteById(PDO $pdo, $id)
{
self::ensureTable($pdo);
2026-07-06 10:34:34 +08:00
$id = (int) $id;
$pdo->prepare('DELETE FROM cloak_config_domain WHERE domain_id = ?')->execute([$id]);
2026-06-14 14:00:24 +08:00
$st = $pdo->prepare('DELETE FROM cloak_site_domains WHERE id = ?');
2026-07-06 10:34:34 +08:00
$st->execute([$id]);
2026-06-14 14:00:24 +08:00
return $st->rowCount() > 0;
}
public static function bindConfigToDomain(PDO $pdo, $configName, $domainId)
{
self::ensureTable($pdo);
$configName = trim($configName);
$domainId = (int) $domainId;
if ($configName === '' || $domainId <= 0) {
return false;
}
2026-07-06 10:34:34 +08:00
if (self::findById($pdo, $domainId) === null) {
return false;
}
$st = $pdo->prepare(
'INSERT INTO cloak_config_domain (config_name, domain_id) VALUES (?, ?)
ON DUPLICATE KEY UPDATE domain_id = VALUES(domain_id)'
);
2026-06-14 14:00:24 +08:00
$st->execute([$configName, $domainId]);
2026-07-06 10:34:34 +08:00
return true;
2026-06-14 14:00:24 +08:00
}
public static function clearConfigBinding(PDO $pdo, $configName)
{
self::ensureTable($pdo);
2026-07-06 10:34:34 +08:00
$st = $pdo->prepare('DELETE FROM cloak_config_domain WHERE config_name = ?');
2026-06-14 14:00:24 +08:00
$st->execute([trim($configName)]);
}
public static function domainIdForConfig(PDO $pdo, $configName)
{
self::ensureTable($pdo);
2026-07-06 10:34:34 +08:00
$st = $pdo->prepare('SELECT domain_id FROM cloak_config_domain WHERE config_name = ? LIMIT 1');
2026-06-14 14:00:24 +08:00
$st->execute([trim($configName)]);
$row = $st->fetch();
2026-07-06 10:34:34 +08:00
return $row ? (int) $row['domain_id'] : 0;
2026-06-14 14:00:24 +08:00
}
public static function primaryHostnameForConfig(PDO $pdo, $configName)
{
self::ensureTable($pdo);
2026-07-06 10:34:34 +08:00
$st = $pdo->prepare(
'SELECT d.hostname FROM cloak_config_domain m
INNER JOIN cloak_site_domains d ON d.id = m.domain_id
WHERE m.config_name = ? LIMIT 1'
);
2026-06-14 14:00:24 +08:00
$st->execute([trim($configName)]);
$row = $st->fetch();
2026-07-06 10:34:34 +08:00
return $row ? (string) $row['hostname'] : '';
}
/**
* @return array<int,string[]>
*/
public static function groupConfigsByDomainId(PDO $pdo)
{
self::ensureTable($pdo);
$st = $pdo->query('SELECT domain_id, config_name FROM cloak_config_domain ORDER BY config_name ASC');
$map = [];
while ($row = $st->fetch()) {
$did = (int) ($row['domain_id'] ?? 0);
if ($did <= 0) {
continue;
}
$map[$did][] = (string) $row['config_name'];
}
return $map;
}
/**
* @return string[]
*/
public static function configNamesForDomainId(PDO $pdo, $domainId)
{
self::ensureTable($pdo);
$st = $pdo->prepare('SELECT config_name FROM cloak_config_domain WHERE domain_id = ? ORDER BY config_name ASC');
$st->execute([(int) $domainId]);
$names = [];
while ($row = $st->fetch()) {
$names[] = (string) $row['config_name'];
}
return $names;
}
/**
* 从全部站点域名中随机选取一个(允许多配置共用同一域名)
*/
public static function pickRandomDomainId(PDO $pdo, $configName = '')
{
self::ensureTable($pdo);
unset($configName);
$all = self::listAll($pdo);
if ($all === []) {
return 0;
}
$pick = $all[array_rand($all)];
return (int) ($pick['id'] ?? 0);
2026-06-14 14:00:24 +08:00
}
public static function resolveConfigNameForHost($host, $baseDir = null)
{
$baseDir = $baseDir ?: dirname(__DIR__, 2);
$norm = self::normalizeHostname($host);
if ($norm !== '') {
try {
$pdo = self::pdo();
2026-07-06 10:34:34 +08:00
self::ensureTable($pdo);
2026-06-14 14:00:24 +08:00
$row = self::findByHostname($pdo, $norm);
2026-07-06 10:34:34 +08:00
if ($row) {
$key = self::hostToConfigKey($norm);
if (file_exists($baseDir . '/check_config/' . $key . '_config.php')) {
return $key;
2026-06-14 14:00:24 +08:00
}
2026-07-06 10:34:34 +08:00
return 'index';
2026-06-14 14:00:24 +08:00
}
} catch (Throwable $e) {
}
$key = self::hostToConfigKey($norm);
if (file_exists($baseDir . '/check_config/' . $key . '_config.php')) {
return $key;
}
}
2026-07-06 10:34:34 +08:00
2026-06-14 14:00:24 +08:00
return 'index';
}
}