diff --git a/check_config/indBXSXX_config.php b/check_config/indBXSXX_config.php deleted file mode 100755 index ff31949..0000000 --- a/check_config/indBXSXX_config.php +++ /dev/null @@ -1,40 +0,0 @@ - 'https://www.google.com/', -) ); - define('COSTM_IP_SCORE', '20251108_emoUAVF6S' ); - define('BLACK_LIST', '0' ); - define('AUTO_BLACK', 'OFF' ); - define('IS_VIRTUAL', '2' ); - define('AUTO_BLACK_TIMES', '5' ); - define('WHITE_PARAMS', '' ); - define('IPHONE_MODEL', 0 ); - - define('SHOW_SITE_MOBILE', 0 ); - define('SHOW_SITE_OUTPUT', 1 ); - define('WRITE_LOG', 1 ); - define('CLOAK_ZH_ON', 'ON'); - define('CLOAK_OS_ON', 'OFF'); - define('KEEP_PARAMS', 'OFF'); - define('CLOAK_REDIRECT_METHOD', 'curl'); - define('DB_ZP', 'https://worldhomenest.store'); - define('CLOAK_MOBILE_ON', 'OFF'); - define('CLOAK_V_REFERER', 'OFF'); - define('CLOAK_RISK_NUMBER', '68'); - define('CLOAK_RISK_ENHANCED', 'ON'); - define('CLOAK_URL_ARGS_TIMEOUT', '0'); - - define('CLOAK_AD_FB', 'OFF'); - define('CLOAK_AD_GOOGLE', 'ON'); - define('CLOAK_AD_TIKTOK', 'ON'); - define('CLOAK_AD_STRICT', 'OFF'); - define('CLOAK_AD_SPM_ID', 'Ss4yqlAnuG1V'); - - define('CLOAK_DEBUG_MODE', 'OFF'); - ?> \ No newline at end of file diff --git a/dashboard.php b/dashboard.php index caea975..ed1df95 100755 --- a/dashboard.php +++ b/dashboard.php @@ -191,7 +191,17 @@ if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "submit") { try { $pdoBind = DomainRepository::pdo(); DomainRepository::ensureTable($pdoBind); - $siteDomainId = (int) ($_REQUEST['site_domain_id'] ?? 0); + $siteDomainRaw = $_REQUEST['site_domain_id'] ?? '0'; + if ($siteDomainRaw === 'random') { + $existingId = DomainRepository::domainIdForConfig($pdoBind, $check_name); + if ($existingId > 0) { + $siteDomainId = $existingId; + } else { + $siteDomainId = DomainRepository::pickRandomDomainId($pdoBind); + } + } else { + $siteDomainId = (int) $siteDomainRaw; + } if ($siteDomainId > 0) { DomainRepository::bindConfigToDomain($pdoBind, $check_name, $siteDomainId); } else { @@ -215,10 +225,10 @@ function onoff_radio($name, $val, $label_on='开启', $label_off='关闭') { $on = ($val == 'ON') ? 'active' : ''; $off = ($val != 'ON') ? 'active' : ''; return '
', array_map('htmlspecialchars', $res['nameservers']));
- $cloakDomainMsg .= '。请到注册商将 NS 修改为:' . $nsHtml . ',然后点击「检测/刷新」。';
- } else {
- $cloakDomainMsg .= ',请在编辑配置时关联此域名。';
+if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'add_domain') {
+ $hostText = trim((string) ($_POST['new_hostnames'] ?? $_POST['new_hostname'] ?? ''));
+ if ($hostText !== '') {
+ try {
+ $pdoDom = DomainRepository::pdo();
+ $dnsMode = $_POST['dns_mode'] ?? 'auto';
+ if (!in_array($dnsMode, ['auto', 'manual'], true)) {
+ $dnsMode = 'auto';
}
- } else {
- $cloakDomainErr = $res['error'] ?? '添加失败';
+ if ($dnsMode === 'auto' && !$cloakCfEnabled) {
+ $cloakDomainErr = '自动模式需要先在 cong.php 配置 Cloudflare API Token 与 Account ID,或改选手动模式。';
+ } else {
+ $res = DomainProvisioningService::provisionBatch($pdoDom, $hostText, $dnsMode);
+ if ($res['ok']) {
+ $addedNames = array_map(static function ($row) {
+ return $row['hostname'] ?? '';
+ }, $res['added'] ?? []);
+ $addedNames = array_filter($addedNames);
+ $cloakDomainMsg = '已成功添加 ' . count($addedNames) . ' 个域名:'
+ . htmlspecialchars(implode('、', $addedNames));
+ if (($res['dns_mode'] ?? '') === 'auto' && $cloakCfEnabled) {
+ $cloakDomainMsg .= '。请到注册商修改 Nameserver 后,在列表中点击「检测/刷新」。';
+ } elseif (($res['dns_mode'] ?? '') === 'manual') {
+ $cloakDomainMsg .= '(手动解析模式,无需 Cloudflare 检测)。';
+ } else {
+ $cloakDomainMsg .= ',请在编辑配置时关联此域名。';
+ }
+ if (!empty($res['failed'])) {
+ $failParts = [];
+ foreach ($res['failed'] as $f) {
+ $failParts[] = ($f['hostname'] ?? '?') . '(' . ($f['error'] ?? '失败') . ')';
+ }
+ $cloakDomainErr = count($res['failed']) . ' 个域名添加失败:'
+ . htmlspecialchars(implode(';', $failParts));
+ }
+ } else {
+ $cloakDomainErr = $res['error'] ?? '添加失败';
+ if (!empty($res['failed'])) {
+ $failParts = [];
+ foreach ($res['failed'] as $f) {
+ $failParts[] = ($f['hostname'] ?? '?') . '(' . ($f['error'] ?? '失败') . ')';
+ }
+ $cloakDomainErr .= ':' . htmlspecialchars(implode(';', $failParts));
+ }
+ }
+ }
+ } catch (Throwable $e) {
+ $cloakDomainErr = $e->getMessage();
}
- } catch (Throwable $e) {
- $cloakDomainErr = $e->getMessage();
+ } else {
+ $cloakDomainErr = '请至少输入一个域名';
}
}
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'check_domain') {
@@ -535,20 +596,163 @@ function dash_build_ad_links($pf, $protocol, $host)
return [['label' => '落地链接', 'url' => $url]];
}
-// 遍历配置列表
-$products_file_ary = [];
-$dd = opendir("check_config");
-while (false !== ($f = readdir($dd))) {
- if ($f == "." || $f == "..") continue;
- if (filetype("check_config/$f") == "dir" || $f == "index.php" || $f == "a.txt") continue;
- $fname = str_replace("_config.php", "", $f);
- $products_file_ary[] = $fname;
- if (!file_exists($fname . ".php")) {
- copy("tem_link.php", $fname . ".php");
+function dash_build_config_index()
+{
+ $items = [];
+ $dir = __DIR__ . '/check_config';
+ $dh = @opendir($dir);
+ if (!$dh) {
+ return $items;
}
+ while (false !== ($f = readdir($dh))) {
+ if ($f === '.' || $f === '..') {
+ continue;
+ }
+ $path = $dir . '/' . $f;
+ if (is_dir($path) || $f === 'index.php' || $f === 'a.txt') {
+ continue;
+ }
+ if (substr($f, -11) !== '_config.php') {
+ continue;
+ }
+ $name = str_replace('_config.php', '', $f);
+ if ($name === '' || !preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
+ continue;
+ }
+ $entryPhp = __DIR__ . '/' . $name . '.php';
+ if (!file_exists($entryPhp)) {
+ copy(__DIR__ . '/tem_link.php', $entryPhp);
+ }
+ $items[] = [
+ 'name' => $name,
+ 'mtime' => (int) @filemtime($path),
+ 'path' => $path,
+ ];
+ }
+ closedir($dh);
+ usort($items, function ($a, $b) {
+ if ($a['mtime'] === $b['mtime']) {
+ return strcmp($a['name'], $b['name']);
+ }
+ return $b['mtime'] <=> $a['mtime'];
+ });
+
+ return $items;
+}
+
+function dash_paginate_configs(array $all, $q, $page, $perPage)
+{
+ $q = trim((string) $q);
+ $page = max(1, (int) $page);
+ $perPage = max(1, (int) $perPage);
+
+ if ($q !== '') {
+ $qLower = function_exists('mb_strtolower') ? mb_strtolower($q, 'UTF-8') : strtolower($q);
+ $all = array_values(array_filter($all, function ($row) use ($qLower) {
+ $name = (string) ($row['name'] ?? '');
+ $hay = function_exists('mb_strtolower') ? mb_strtolower($name, 'UTF-8') : strtolower($name);
+
+ return strpos($hay, $qLower) !== false;
+ }));
+ }
+
+ $total = count($all);
+ $totalPages = $total > 0 ? (int) ceil($total / $perPage) : 1;
+ if ($page > $totalPages) {
+ $page = $totalPages;
+ }
+ $offset = ($page - 1) * $perPage;
+
+ return [
+ 'items' => array_slice($all, $offset, $perPage),
+ 'total' => $total,
+ 'page' => $page,
+ 'per_page' => $perPage,
+ 'total_pages' => $totalPages,
+ ];
+}
+
+function dash_delete_config($name)
+{
+ $name = dash_sanitize_config_name($name);
+ if ($name === '' || $name === 'index') {
+ return false;
+ }
+ try {
+ DomainRepository::clearConfigBinding(DomainRepository::pdo(), $name);
+ } catch (Throwable $e) {
+ }
+ @unlink(__DIR__ . "/check_config/{$name}_config.php");
+ @unlink(__DIR__ . "/{$name}.php");
+
+ return true;
+}
+
+function dash_list_query_params($q, $page)
+{
+ $params = [];
+ if (trim((string) $q) !== '') {
+ $params['q'] = trim((string) $q);
+ }
+ if ((int) $page > 1) {
+ $params['page'] = (int) $page;
+ }
+
+ return $params;
+}
+
+function dash_list_url($currentName, $q, $page)
+{
+ $params = dash_list_query_params($q, $page);
+ $qs = $params !== [] ? '?' . http_build_query($params) : '';
+
+ return $currentName . $qs;
+}
+
+// 配置列表索引(按文件修改时间倒序)
+$configListAll = dash_build_config_index();
+$listSearchQ = trim((string) ($_GET['q'] ?? ''));
+$listPage = max(1, (int) ($_GET['page'] ?? 1));
+$listPerPage = 10;
+$configPage = dash_paginate_configs($configListAll, $listSearchQ, $listPage, $listPerPage);
+$products_file_ary = array_map(function ($row) {
+ return $row['name'];
+}, $configListAll);
+
+// 删除 / 批量删除配置
+if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "delete_config" && !empty($_REQUEST["name"])) {
+ $name = trim($_REQUEST["name"]);
+ if ($name == "index") {
+ echo("");
+ exit();
+ }
+ dash_delete_config($name);
+ echo("");
+ exit();
+}
+
+if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'batch_delete_config') {
+ $names = $_REQUEST['names'] ?? [];
+ if (!is_array($names)) {
+ $names = [$names];
+ }
+ $deleted = 0;
+ $seen = [];
+ foreach ($names as $rawName) {
+ $name = dash_sanitize_config_name($rawName);
+ if ($name === '' || $name === 'index' || isset($seen[$name])) {
+ continue;
+ }
+ $seen[$name] = true;
+ if (dash_delete_config($name)) {
+ $deleted++;
+ }
+ }
+ $redirect = dash_list_url($current_name, $_POST['q'] ?? '', 1);
+ $msg = $deleted > 0 ? "已删除 {$deleted} 个配置" : '未删除任何配置';
+ echo("");
+ exit();
}
-closedir($dd);
-sort($products_file_ary);
// 加载 IP 组(编辑页用)
$blackGroups = $whiteGroups = [];
@@ -566,17 +770,20 @@ $action = $_REQUEST['action'] ?? '';
$isEdit = in_array($action, ['edit_config', 'submit'], true);
$cloakSiteDomains = [];
+$domainConfigMap = [];
$selectedDomainId = 0;
$dashboardDefaultHost = DomainRepository::normalizeHostname($_SERVER['HTTP_HOST'] ?? '');
try {
$pdoDash = $pdoDash ?? DomainRepository::pdo();
DomainRepository::ensureTable($pdoDash);
$cloakSiteDomains = DomainRepository::listAll($pdoDash);
+ $domainConfigMap = DomainRepository::groupConfigsByDomainId($pdoDash);
if ($isEdit) {
$selectedDomainId = DomainRepository::domainIdForConfig($pdoDash, $check_name);
}
} catch (Throwable $e) {
$cloakSiteDomains = [];
+ $domainConfigMap = [];
}
// 视图判断
@@ -594,7 +801,7 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
-
+
@@ -616,7 +823,7 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
echo '● Redis ' . ($redisOk ? '已连接' : '连接失败') . '';
}
?>
- ● 配置数:
+ ● 配置数:(搜索「」)
● 已登记域名:
@@ -635,22 +842,40 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
+
+
@@ -732,19 +991,37 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
- Cloudflare 已启用:添加域名后将自动创建 Zone 并显示 Nameserver;请到注册商修改 NS 后点击「检测/刷新」。NS 生效后将自动配置 proxied A 记录、Flexible SSL 与 Always HTTPS。
- 源站仍需在宝塔添加站点并监听 (Flexible:访客↔CF 为 HTTPS,CF↔源站为 HTTP)。
+ Cloudflare 已启用:选择「自动」时将创建 Zone 并配置 DNS/SSL;选择「手动」时仅本地登记(您已自行配置 DNS)。
+ 源站仍需在宝塔添加站点并监听 。
- 使用前请先在宝塔完成:在宝塔面板 → 网站 → 添加站点(填写下方相同域名)→ 申请并开启 SSL 证书。完成后再在此添加域名并关联配置。
- 若在 cong.php 配置 Cloudflare API Token 与 Account ID,可自动创建 Zone 并管理 DNS/SSL。
+ 提示:自动模式需先在 cong.php 配置 Cloudflare API Token 与 Account ID。若已自行完成 DNS 解析,请选择「手动」模式。
+ 使用前请先在宝塔添加站点并开启 SSL。
-
尚未添加域名。未添加时,广告链接将使用当前后台访问域名:
@@ -763,6 +1040,7 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
$cfLabel = CloudflareConfig::statusLabel($cfStatus);
$badgeCls = 'secondary';
if ($cfStatus === 'pending_ns') { $badgeCls = 'warning'; }
+ elseif ($cfStatus === 'manual') { $badgeCls = 'secondary'; }
elseif ($cfStatus === 'provisioning') { $badgeCls = 'info'; }
elseif ($cfStatus === 'ready') { $badgeCls = 'success'; }
elseif ($cfStatus === 'error') { $badgeCls = 'danger'; }
@@ -775,10 +1053,13 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
' . htmlspecialchars($dom['config_name']) . '';
- } else {
+ $boundConfigs = $domainConfigMap[(int) $dom['id']] ?? [];
+ if ($boundConfigs === []) {
echo '未关联';
+ } else {
+ foreach ($boundConfigs as $cfgName) {
+ echo '' . htmlspecialchars($cfgName) . '';
+ }
}
?>
@@ -863,11 +1144,17 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
-
+ = 2 && ($isNewConfigDraft || $selectedDomainId === 0)): ?>
+
+
+
@@ -1009,10 +1296,10 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
-
+
> 开启
-
+
> 关闭
@@ -1200,10 +1487,10 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
记录访客日志
-
+
> 开启
-
+
> 关闭
@@ -1292,8 +1579,26 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
}
});
+ $('#checkAllConfigs').on('change', function() {
+ $('.config-row-check').prop('checked', this.checked);
+ });
+ $(document).on('change', '.config-row-check', function() {
+ var all = $('.config-row-check');
+ var checked = all.filter(':checked').length;
+ $('#checkAllConfigs').prop('checked', all.length > 0 && checked === all.length);
+ });
+
});
+function dashBatchDeleteConfirm() {
+ var checked = document.querySelectorAll('.config-row-check:checked');
+ if (checked.length === 0) {
+ alert('请至少选择一个配置');
+ return false;
+ }
+ return confirm('确定删除选中的 ' + checked.length + ' 个配置?此操作不可恢复。');
+}
+
function delUrlArgs() {
if (!confirm('确认重置临时链接参数计时?')) return;
window.location.href = '?action=delUrlArgs&check_name=';
diff --git a/indBXSXX.php b/indBXSXX.php
deleted file mode 100755
index 0c861a7..0000000
--- a/indBXSXX.php
+++ /dev/null
@@ -1,4 +0,0 @@
- '本地登记',
+ 'manual' => '手动解析',
'pending_ns' => '待检测',
'provisioning' => '配置中',
'ready' => '已生效',
diff --git a/lib/Cloak/DomainProvisioningService.php b/lib/Cloak/DomainProvisioningService.php
index f0aae5c..1b167e1 100755
--- a/lib/Cloak/DomainProvisioningService.php
+++ b/lib/Cloak/DomainProvisioningService.php
@@ -29,27 +29,33 @@ class DomainProvisioningService
/**
* 添加域名:本地入库 + 可选创建 Cloudflare Zone
*
- * @return array{ok:bool,error?:string,hostname?:string,id?:int,nameservers?:array,cf_status?:string}
+ * @param string $dnsMode auto|manual — manual 时不调用 Cloudflare
+ * @return array{ok:bool,error?:string,hostname?:string,id?:int,nameservers?:array,cf_status?:string,dns_mode?:string}
*/
- public static function provisionOnAdd(PDO $pdo, $hostname)
+ public static function provisionOnAdd(PDO $pdo, $hostname, $dnsMode = 'auto')
{
$add = DomainRepository::add($pdo, $hostname);
if (!$add['ok']) {
return $add;
}
- $id = (int) $add['id'];
- $host = $add['hostname'];
+ $id = (int) $add['id'];
+ $host = $add['hostname'];
+ $dnsMode = strtolower(trim((string) $dnsMode)) === 'manual' ? 'manual' : 'auto';
- if (!CloudflareConfig::isEnabled()) {
+ if ($dnsMode === 'manual' || !CloudflareConfig::isEnabled()) {
+ $status = $dnsMode === 'manual' ? 'manual' : 'local';
DomainRepository::updateCloudflareMeta($pdo, $id, [
- 'cf_status' => 'local',
+ 'cf_status' => $status,
+ 'cf_error' => '',
]);
+
return [
- 'ok' => true,
- 'id' => $id,
- 'hostname' => $host,
- 'cf_status' => 'local',
+ 'ok' => true,
+ 'id' => $id,
+ 'hostname' => $host,
+ 'cf_status' => $status,
+ 'dns_mode' => $dnsMode,
'nameservers' => [],
];
}
@@ -84,10 +90,52 @@ class DomainProvisioningService
'id' => $id,
'hostname' => $host,
'cf_status' => 'pending_ns',
+ 'dns_mode' => 'auto',
'nameservers' => $ns,
];
}
+ /**
+ * 批量添加域名
+ *
+ * @return array{ok:bool,error?:string,added?:array,failed?:array,dns_mode?:string}
+ */
+ public static function provisionBatch(PDO $pdo, $hostnameText, $dnsMode = 'auto')
+ {
+ $hosts = DomainRepository::parseHostnameBatch($hostnameText);
+ if ($hosts === []) {
+ return ['ok' => false, 'error' => '请至少输入一个有效域名(每行一个)'];
+ }
+
+ $dnsMode = strtolower(trim((string) $dnsMode)) === 'manual' ? 'manual' : 'auto';
+ $added = [];
+ $failed = [];
+ foreach ($hosts as $host) {
+ $res = self::provisionOnAdd($pdo, $host, $dnsMode);
+ if (!empty($res['ok'])) {
+ $added[] = $res;
+ } else {
+ $failed[] = [
+ 'hostname' => $host,
+ 'error' => $res['error'] ?? '添加失败',
+ ];
+ }
+ }
+
+ if ($added === []) {
+ $firstErr = $failed[0]['error'] ?? '添加失败';
+
+ return ['ok' => false, 'error' => $firstErr, 'added' => [], 'failed' => $failed, 'dns_mode' => $dnsMode];
+ }
+
+ return [
+ 'ok' => true,
+ 'added' => $added,
+ 'failed' => $failed,
+ 'dns_mode' => $dnsMode,
+ ];
+ }
+
/**
* 手动检测 NS 是否生效,生效后配置 A 记录与 SSL
*
@@ -100,6 +148,10 @@ class DomainProvisioningService
return ['ok' => false, 'message' => '域名不存在'];
}
+ if (($row['cf_status'] ?? '') === 'manual') {
+ return ['ok' => false, 'message' => '该域名为手动解析模式,无需 Cloudflare 检测'];
+ }
+
if (!CloudflareConfig::isEnabled()) {
return ['ok' => false, 'message' => '未配置 Cloudflare API,无法检测'];
}
diff --git a/lib/Cloak/DomainRepository.php b/lib/Cloak/DomainRepository.php
index e94e4b3..872fd20 100755
--- a/lib/Cloak/DomainRepository.php
+++ b/lib/Cloak/DomainRepository.php
@@ -23,6 +23,50 @@ class DomainRepository
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
);
self::ensureCloudflareColumns($pdo);
+ 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]);
+ }
}
private static function ensureCloudflareColumns(PDO $pdo)
@@ -86,6 +130,35 @@ class DomainRepository
return $host;
}
+ /**
+ * 从多行文本解析域名列表(每行一个,也支持逗号/分号分隔)
+ *
+ * @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;
+ }
+
public static function hostToConfigKey($host)
{
$host = self::normalizeHostname($host);
@@ -171,8 +244,10 @@ class DomainRepository
public static function deleteById(PDO $pdo, $id)
{
self::ensureTable($pdo);
+ $id = (int) $id;
+ $pdo->prepare('DELETE FROM cloak_config_domain WHERE domain_id = ?')->execute([$id]);
$st = $pdo->prepare('DELETE FROM cloak_site_domains WHERE id = ?');
- $st->execute([(int) $id]);
+ $st->execute([$id]);
return $st->rowCount() > 0;
}
@@ -184,35 +259,98 @@ class DomainRepository
if ($configName === '' || $domainId <= 0) {
return false;
}
- $pdo->prepare("UPDATE cloak_site_domains SET config_name = '' WHERE config_name = ?")->execute([$configName]);
- $st = $pdo->prepare('UPDATE cloak_site_domains SET config_name = ? WHERE id = ?');
+ 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)'
+ );
$st->execute([$configName, $domainId]);
- return $st->rowCount() > 0;
+
+ return true;
}
public static function clearConfigBinding(PDO $pdo, $configName)
{
self::ensureTable($pdo);
- $st = $pdo->prepare("UPDATE cloak_site_domains SET config_name = '' WHERE config_name = ?");
+ $st = $pdo->prepare('DELETE FROM cloak_config_domain WHERE config_name = ?');
$st->execute([trim($configName)]);
}
public static function domainIdForConfig(PDO $pdo, $configName)
{
self::ensureTable($pdo);
- $st = $pdo->prepare('SELECT id FROM cloak_site_domains WHERE config_name = ? ORDER BY id ASC LIMIT 1');
+ $st = $pdo->prepare('SELECT domain_id FROM cloak_config_domain WHERE config_name = ? LIMIT 1');
$st->execute([trim($configName)]);
$row = $st->fetch();
- return $row ? (int) $row['id'] : 0;
+
+ return $row ? (int) $row['domain_id'] : 0;
}
public static function primaryHostnameForConfig(PDO $pdo, $configName)
{
self::ensureTable($pdo);
- $st = $pdo->prepare('SELECT hostname FROM cloak_site_domains WHERE config_name = ? ORDER BY id ASC LIMIT 1');
+ $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'
+ );
$st->execute([trim($configName)]);
$row = $st->fetch();
- return $row ? $row['hostname'] : '';
+
+ return $row ? (string) $row['hostname'] : '';
+ }
+
+ /**
+ * @return array
+ */
+ 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);
}
public static function resolveConfigNameForHost($host, $baseDir = null)
@@ -222,12 +360,15 @@ class DomainRepository
if ($norm !== '') {
try {
$pdo = self::pdo();
+ self::ensureTable($pdo);
$row = self::findByHostname($pdo, $norm);
- if ($row && !empty($row['config_name'])) {
- $name = trim($row['config_name']);
- if (file_exists($baseDir . '/check_config/' . $name . '_config.php')) {
- return $name;
+ if ($row) {
+ $key = self::hostToConfigKey($norm);
+ if (file_exists($baseDir . '/check_config/' . $key . '_config.php')) {
+ return $key;
}
+
+ return 'index';
}
} catch (Throwable $e) {
}
@@ -236,6 +377,7 @@ class DomainRepository
return $key;
}
}
+
return 'index';
}
}
diff --git a/lib/Cloak/VisitorSimulationRunner.php b/lib/Cloak/VisitorSimulationRunner.php
index 65c547d..d23bacf 100755
--- a/lib/Cloak/VisitorSimulationRunner.php
+++ b/lib/Cloak/VisitorSimulationRunner.php
@@ -157,8 +157,11 @@ class VisitorSimulationRunner
$pdo = DomainRepository::pdo();
DomainRepository::ensureTable($pdo);
$domRow = DomainRepository::findByHostname($pdo, $domain);
- if ($domRow && !empty($domRow['config_name'])) {
- $config = trim($domRow['config_name']);
+ if ($domRow) {
+ $names = DomainRepository::configNamesForDomainId($pdo, (int) ($domRow['id'] ?? 0));
+ if (count($names) === 1) {
+ $config = $names[0];
+ }
}
} catch (Throwable $e) {
}
diff --git a/lib/InstallSchema.php b/lib/InstallSchema.php
index 37d7427..e663082 100755
--- a/lib/InstallSchema.php
+++ b/lib/InstallSchema.php
@@ -40,6 +40,14 @@ class InstallSchema
KEY `idx_config_name` (`config_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
+ "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",
+
"CREATE TABLE IF NOT EXISTS `visitor_logs` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`campagin_id` varchar(20) NOT NULL COMMENT '策略ID',
@@ -101,7 +109,7 @@ class InstallSchema
return [
'ok' => true,
- 'tables' => ['ip_groups', 'ip_list', 'cloak_site_domains', 'visitor_logs'],
+ 'tables' => ['ip_groups', 'ip_list', 'cloak_site_domains', 'cloak_config_domain', 'visitor_logs'],
];
}
}
diff --git a/tools/verify_cloudflare_domain.php b/tools/verify_cloudflare_domain.php
index 4b6dbe4..a258e63 100755
--- a/tools/verify_cloudflare_domain.php
+++ b/tools/verify_cloudflare_domain.php
@@ -108,6 +108,29 @@ try {
$del = DomainProvisioningService::deleteWithCloudflare($pdo, $domainId);
$tests['delete_calls_zone'] = ($del['ok'] ?? false) === true && $mock->deleteCalled === true;
$tests['delete_row_gone'] = DomainRepository::findById($pdo, $domainId) === null;
+
+ // 手动模式:不创建 Zone
+ $hostManual = 'cf-manual-' . substr(md5((string) microtime(true)), 0, 8) . '.test';
+ $addManual = DomainProvisioningService::provisionOnAdd($pdo, $hostManual, 'manual');
+ $rowManual = DomainRepository::findById($pdo, (int) ($addManual['id'] ?? 0));
+ $tests['add_manual_cf_status'] = ($addManual['ok'] ?? false) === true
+ && ($addManual['cf_status'] ?? '') === 'manual'
+ && ($rowManual['cf_status'] ?? '') === 'manual'
+ && trim((string) ($rowManual['cf_zone_id'] ?? '')) === '';
+ $checkManual = DomainProvisioningService::checkAndProvision($pdo, (int) ($addManual['id'] ?? 0));
+ $tests['check_manual_skipped'] = ($checkManual['ok'] ?? false) === false;
+ DomainRepository::deleteById($pdo, (int) ($addManual['id'] ?? 0));
+
+ // 批量添加
+ $batchText = "batch-a.test\nbatch-b.test, batch-c.test";
+ $batch = DomainProvisioningService::provisionBatch($pdo, $batchText, 'manual');
+ $tests['batch_add_count'] = ($batch['ok'] ?? false) === true && count($batch['added'] ?? []) === 3;
+ foreach ($batch['added'] ?? [] as $item) {
+ DomainRepository::deleteById($pdo, (int) ($item['id'] ?? 0));
+ }
+
+ $parsed = DomainRepository::parseHostnameBatch("a.test\nb.test\nc.test,d.test");
+ $tests['parse_hostname_batch'] = $parsed === ['a.test', 'b.test', 'c.test', 'd.test'];
} catch (Throwable $e) {
$tests['exception'] = false;
$tests['error'] = $e->getMessage();
diff --git a/tools/verify_domain_random.php b/tools/verify_domain_random.php
new file mode 100644
index 0000000..b3ca5e5
--- /dev/null
+++ b/tools/verify_domain_random.php
@@ -0,0 +1,90 @@
+#!/usr/bin/env php
+= 3) {
+ DomainRepository::bindConfigToDomain($pdo, 'cfg_one', $ids[2]);
+ DomainRepository::bindConfigToDomain($pdo, 'cfg_two', $ids[2]);
+ }
+
+ $map = DomainRepository::groupConfigsByDomainId($pdo);
+ vassert('多配置可共用同一域名', count($map[$ids[2]] ?? []) === 2, json_encode($map[$ids[2]] ?? []));
+
+ $picked = DomainRepository::pickRandomDomainId($pdo);
+ vassert('pickRandom 返回有效 id', $picked > 0, (string) $picked);
+ vassert('pickRandom 从全部域名中选取', in_array($picked, $ids, true), 'picked=' . $picked);
+
+ DomainRepository::bindConfigToDomain($pdo, 'cfg_three', $ids[0]);
+ DomainRepository::bindConfigToDomain($pdo, 'cfg_four', $ids[0]);
+ vassert('domainIdForConfig 读取映射', DomainRepository::domainIdForConfig($pdo, 'cfg_three') === $ids[0]);
+ vassert('primaryHostnameForConfig', DomainRepository::primaryHostnameForConfig($pdo, 'cfg_three') === $hostA);
+
+ DomainRepository::clearConfigBinding($pdo, 'cfg_three');
+ vassert('clearConfigBinding 仅清除指定配置', DomainRepository::domainIdForConfig($pdo, 'cfg_three') === 0);
+ vassert('clearConfigBinding 不影响同域其他配置', DomainRepository::domainIdForConfig($pdo, 'cfg_four') === $ids[0]);
+
+ foreach ($ids as $id) {
+ DomainRepository::deleteById($pdo, $id);
+ }
+ vassert('deleteById 级联清理映射', DomainRepository::domainIdForConfig($pdo, 'cfg_two') === 0);
+ vassert('cleanup', DomainRepository::findById($pdo, $ids[0]) === null);
+} catch (Throwable $e) {
+ echo "WARN: DB test skipped — " . $e->getMessage() . "\n";
+}
+
+$all = [
+ ['id' => 1],
+ ['id' => 2],
+ ['id' => 3],
+];
+vassert('offline 全部域名均可随机', count($all) === 3);
+
+$result = ['ok' => $failed === 0, 'passed' => $passed, 'failed' => $failed];
+echo json_encode($result, JSON_UNESCAPED_UNICODE) . "\n";
+exit($failed > 0 ? 1 : 0);