手动添加域名

This commit is contained in:
root
2026-07-06 10:34:34 +08:00
parent c3c58198ab
commit 7efb3e6eb5
11 changed files with 728 additions and 141 deletions
-40
View File
@@ -1,40 +0,0 @@
<?php
define('SHOW_SITE_MODE_SWITCH', 'ip_check' );
define('SHOW_SITE_IP', '' );
define('BLACKLIST_GROUP_ID', 0 );
define('WHITELIST_GROUP_ID', 0 );
define('SHOW_SITE_COUNTRY', '' );
define('CLOAK_SHOW_CONTENT', '302');
define('DB_FP', array (
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');
?>
+376 -71
View File
@@ -191,7 +191,17 @@ if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "submit") {
try { try {
$pdoBind = DomainRepository::pdo(); $pdoBind = DomainRepository::pdo();
DomainRepository::ensureTable($pdoBind); 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) { if ($siteDomainId > 0) {
DomainRepository::bindConfigToDomain($pdoBind, $check_name, $siteDomainId); DomainRepository::bindConfigToDomain($pdoBind, $check_name, $siteDomainId);
} else { } else {
@@ -215,10 +225,10 @@ function onoff_radio($name, $val, $label_on='开启', $label_off='关闭') {
$on = ($val == 'ON') ? 'active' : ''; $on = ($val == 'ON') ? 'active' : '';
$off = ($val != 'ON') ? 'active' : ''; $off = ($val != 'ON') ? 'active' : '';
return '<div class="btn-group btn-group-toggle" data-toggle="buttons"> return '<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm btn-outline-secondary ' . $on . '"> <label class="btn btn-sm btn-outline-primary ' . $on . '">
<input type="radio" name="' . $name . '" value="ON" ' . ($val=='ON'?'checked':'') . '> ' . $label_on . ' <input type="radio" name="' . $name . '" value="ON" ' . ($val=='ON'?'checked':'') . '> ' . $label_on . '
</label> </label>
<label class="btn btn-sm btn-outline-secondary ' . $off . '"> <label class="btn btn-sm btn-outline-primary ' . $off . '">
<input type="radio" name="' . $name . '" value="OFF" ' . ($val!='ON'?'checked':'') . '> ' . $label_off . ' <input type="radio" name="' . $name . '" value="OFF" ' . ($val!='ON'?'checked':'') . '> ' . $label_off . '
</label> </label>
</div>'; </div>';
@@ -256,6 +266,7 @@ body { background:#f0f2f5; font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-ser
.topbar a:hover { color:#fff; } .topbar a:hover { color:#fff; }
.topbar h1 { margin:0; font-size:1.25rem; font-weight:600; } .topbar h1 { margin:0; font-size:1.25rem; font-weight:600; }
.page-body { max-width:1100px; margin:32px auto; padding:0 16px; } .page-body { max-width:1100px; margin:32px auto; padding:0 16px; }
.page-body.page-body--list { width:90%; max-width:none; margin-left:auto; margin-right:auto; }
.status-bar { display:flex; gap:12px; margin-bottom:20px; flex-wrap:wrap; } .status-bar { display:flex; gap:12px; margin-bottom:20px; flex-wrap:wrap; }
.status-badge { display:inline-flex; align-items:center; gap:6px; padding:6px 14px; border-radius:20px; font-size:.82rem; font-weight:500; } .status-badge { display:inline-flex; align-items:center; gap:6px; padding:6px 14px; border-radius:20px; font-size:.82rem; font-weight:500; }
.status-ok { background:#d4edda; color:#155724; } .status-ok { background:#d4edda; color:#155724; }
@@ -326,6 +337,40 @@ body { background:#f0f2f5; font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-ser
.domain-host-badge { display: inline-block; padding: 2px 8px; background: #e8f4fd; color: #1565c0; border-radius: 4px; font-size: .78rem; font-family: 'SF Mono', Consolas, monospace; } .domain-host-badge { display: inline-block; padding: 2px 8px; background: #e8f4fd; color: #1565c0; border-radius: 4px; font-size: .78rem; font-family: 'SF Mono', Consolas, monospace; }
.config-name-cell strong { color: #2c3e50; font-size: .92rem; } .config-name-cell strong { color: #2c3e50; font-size: .92rem; }
.empty-hint { padding: 28px 20px; text-align: center; color: #8c98a4; font-size: .88rem; background: #fafbfc; border-radius: 0 0 10px 10px; } .empty-hint { padding: 28px 20px; text-align: center; color: #8c98a4; font-size: .88rem; background: #fafbfc; border-radius: 0 0 10px 10px; }
.list-toolbar { display:flex; flex-wrap:wrap; align-items:center; gap:10px; padding:12px 16px; border-bottom:1px solid #e9ecef; background:#fafbfc; }
.list-toolbar .search-form { display:flex; align-items:center; gap:8px; flex:1; min-width:220px; }
.list-toolbar .search-form input[type="search"] { max-width:280px; }
.list-pagination { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; padding:12px 16px; border-top:1px solid #e9ecef; background:#fff; font-size:.84rem; color:#6c757d; }
.list-pagination .pagination { margin:0; }
.config-table .col-check { width:36px; text-align:center; }
/* 表单开关:选中实心蓝,未选中蓝框蓝字 */
.btn-group-toggle .btn.btn-outline-primary,
.btn-group-toggle .btn.btn-outline-secondary {
color: #1565c0;
border-color: #64b5f6;
background: #fff;
font-weight: 500;
min-width: 52px;
}
.btn-group-toggle .btn.btn-outline-primary:hover,
.btn-group-toggle .btn.btn-outline-secondary:hover {
color: #0d47a1;
background: #e3f2fd;
border-color: #42a5f5;
}
.btn-group-toggle .btn.btn-outline-primary.active,
.btn-group-toggle .btn.btn-outline-secondary.active,
.btn-group-toggle .btn.btn-outline-primary.active:hover,
.btn-group-toggle .btn.btn-outline-secondary.active:hover {
color: #fff !important;
background: linear-gradient(180deg, #2196f3 0%, #1565c0 100%) !important;
border-color: #1565c0 !important;
box-shadow: 0 1px 4px rgba(21, 101, 192, .35);
}
.btn-group-toggle .btn.btn-outline-primary:not(.active):focus,
.btn-group-toggle .btn.btn-outline-secondary:not(.active):focus {
box-shadow: 0 0 0 .15rem rgba(33, 150, 243, .25);
}
</style> </style>
</head> </head>
<body> <body>
@@ -355,45 +400,61 @@ if ($_SESSION['password'] != LOGIN_PASSWORD) {
exit(); exit();
} }
// 删除配置
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "delete_config" && !empty($_REQUEST["name"])) {
$name = trim($_REQUEST["name"]);
if ($name == "index") {
echo("<script>alert('请勿删除默认配置!');window.location='{$current_name}';</script>");
exit();
}
$cfg = str_replace(".php", "", $name);
try {
DomainRepository::clearConfigBinding(DomainRepository::pdo(), $cfg);
} catch (Throwable $e) {
}
@unlink("check_config/{$cfg}_config.php");
@unlink("{$cfg}.php");
echo("<script>alert('删除成功!');window.location='{$current_name}';</script>");
exit();
}
// 站点域名管理 // 站点域名管理
$cloakDomainMsg = ''; $cloakDomainMsg = '';
$cloakDomainErr = ''; $cloakDomainErr = '';
$cloakCfEnabled = CloudflareConfig::isEnabled(); $cloakCfEnabled = CloudflareConfig::isEnabled();
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'add_domain' && !empty($_POST['new_hostname'])) { if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'add_domain') {
try { $hostText = trim((string) ($_POST['new_hostnames'] ?? $_POST['new_hostname'] ?? ''));
$pdoDom = DomainRepository::pdo(); if ($hostText !== '') {
$res = DomainProvisioningService::provisionOnAdd($pdoDom, $_POST['new_hostname']); try {
if ($res['ok']) { $pdoDom = DomainRepository::pdo();
$cloakDomainMsg = '域名「' . htmlspecialchars($res['hostname']) . '」已添加'; $dnsMode = $_POST['dns_mode'] ?? 'auto';
if (!empty($res['nameservers'])) { if (!in_array($dnsMode, ['auto', 'manual'], true)) {
$nsHtml = implode('</code>、<code>', array_map('htmlspecialchars', $res['nameservers'])); $dnsMode = 'auto';
$cloakDomainMsg .= '。请到注册商将 NS 修改为:<code>' . $nsHtml . '</code>,然后点击「检测/刷新」。';
} else {
$cloakDomainMsg .= ',请在编辑配置时关联此域名。';
} }
} else { if ($dnsMode === 'auto' && !$cloakCfEnabled) {
$cloakDomainErr = $res['error'] ?? '添加失败'; $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) { } else {
$cloakDomainErr = $e->getMessage(); $cloakDomainErr = '请至少输入一个域名';
} }
} }
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'check_domain') { if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'check_domain') {
@@ -535,20 +596,163 @@ function dash_build_ad_links($pf, $protocol, $host)
return [['label' => '落地链接', 'url' => $url]]; return [['label' => '落地链接', 'url' => $url]];
} }
// 遍历配置列表 function dash_build_config_index()
$products_file_ary = []; {
$dd = opendir("check_config"); $items = [];
while (false !== ($f = readdir($dd))) { $dir = __DIR__ . '/check_config';
if ($f == "." || $f == "..") continue; $dh = @opendir($dir);
if (filetype("check_config/$f") == "dir" || $f == "index.php" || $f == "a.txt") continue; if (!$dh) {
$fname = str_replace("_config.php", "", $f); return $items;
$products_file_ary[] = $fname;
if (!file_exists($fname . ".php")) {
copy("tem_link.php", $fname . ".php");
} }
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("<script>alert('请勿删除默认配置!');window.location='{$current_name}';</script>");
exit();
}
dash_delete_config($name);
echo("<script>alert('删除成功!');window.location='{$current_name}';</script>");
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("<script>alert('" . addslashes($msg) . "');window.location='" . addslashes($redirect) . "';</script>");
exit();
} }
closedir($dd);
sort($products_file_ary);
// 加载 IP 组(编辑页用) // 加载 IP 组(编辑页用)
$blackGroups = $whiteGroups = []; $blackGroups = $whiteGroups = [];
@@ -566,17 +770,20 @@ $action = $_REQUEST['action'] ?? '';
$isEdit = in_array($action, ['edit_config', 'submit'], true); $isEdit = in_array($action, ['edit_config', 'submit'], true);
$cloakSiteDomains = []; $cloakSiteDomains = [];
$domainConfigMap = [];
$selectedDomainId = 0; $selectedDomainId = 0;
$dashboardDefaultHost = DomainRepository::normalizeHostname($_SERVER['HTTP_HOST'] ?? ''); $dashboardDefaultHost = DomainRepository::normalizeHostname($_SERVER['HTTP_HOST'] ?? '');
try { try {
$pdoDash = $pdoDash ?? DomainRepository::pdo(); $pdoDash = $pdoDash ?? DomainRepository::pdo();
DomainRepository::ensureTable($pdoDash); DomainRepository::ensureTable($pdoDash);
$cloakSiteDomains = DomainRepository::listAll($pdoDash); $cloakSiteDomains = DomainRepository::listAll($pdoDash);
$domainConfigMap = DomainRepository::groupConfigsByDomainId($pdoDash);
if ($isEdit) { if ($isEdit) {
$selectedDomainId = DomainRepository::domainIdForConfig($pdoDash, $check_name); $selectedDomainId = DomainRepository::domainIdForConfig($pdoDash, $check_name);
} }
} catch (Throwable $e) { } catch (Throwable $e) {
$cloakSiteDomains = []; $cloakSiteDomains = [];
$domainConfigMap = [];
} }
// 视图判断 // 视图判断
@@ -594,7 +801,7 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
</div> </div>
</div> </div>
<div class="page-body"> <div class="page-body<?php echo $isEdit ? '' : ' page-body--list'; ?>">
<?php if (!$isEdit): ?> <?php if (!$isEdit): ?>
<!-- ===================== 列表视图 ===================== --> <!-- ===================== 列表视图 ===================== -->
@@ -616,7 +823,7 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
echo '<span class="status-badge ' . ($redisOk ? 'status-ok' : 'status-err') . '">● Redis ' . ($redisOk ? '已连接' : '连接失败') . '</span>'; echo '<span class="status-badge ' . ($redisOk ? 'status-ok' : 'status-err') . '">● Redis ' . ($redisOk ? '已连接' : '连接失败') . '</span>';
} }
?> ?>
<span class="status-badge" style="background:#e2e3e5;color:#383d41;">● 配置数:<?php echo count($products_file_ary); ?></span> <span class="status-badge" style="background:#e2e3e5;color:#383d41;">● 配置数:<?php echo (int) $configPage['total']; ?><?php if ($listSearchQ !== ''): ?>(搜索「<?php echo htmlspecialchars($listSearchQ); ?>」)<?php endif; ?></span>
<span class="status-badge" style="background:#e2e3e5;color:#383d41;">● 已登记域名:<?php echo count($cloakSiteDomains); ?></span> <span class="status-badge" style="background:#e2e3e5;color:#383d41;">● 已登记域名:<?php echo count($cloakSiteDomains); ?></span>
</div> </div>
@@ -635,22 +842,40 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
<div class="header-actions"> <div class="header-actions">
<a href="/table" target="_blank" class="btn btn-sm btn-outline-secondary">查看日志</a> <a href="/table" target="_blank" class="btn btn-sm btn-outline-secondary">查看日志</a>
<a href="/log_analytics.php" target="_blank" class="btn btn-sm btn-outline-secondary">数据分析</a> <a href="/log_analytics.php" target="_blank" class="btn btn-sm btn-outline-secondary">数据分析</a>
<button type="submit" form="batchDeleteForm" class="btn btn-sm btn-outline-danger" id="batchDeleteBtn" onclick="return dashBatchDeleteConfirm();">批量删除</button>
<a href="<?php echo $current_name; ?>?action=edit_config&name=new_config" class="btn btn-sm btn-teal">+ 新增配置</a> <a href="<?php echo $current_name; ?>?action=edit_config&name=new_config" class="btn btn-sm btn-teal">+ 新增配置</a>
</div> </div>
</div> </div>
<div class="list-toolbar">
<form method="get" action="<?php echo htmlspecialchars($current_name); ?>" class="search-form mb-0">
<input type="search" name="q" class="form-control form-control-sm" placeholder="搜索配置名称…" value="<?php echo htmlspecialchars($listSearchQ, ENT_QUOTES); ?>">
<button type="submit" class="btn btn-sm btn-outline-primary">搜索</button>
<?php if ($listSearchQ !== ''): ?>
<a href="<?php echo htmlspecialchars($current_name); ?>" class="btn btn-sm btn-outline-secondary">清除</a>
<?php endif; ?>
</form>
</div>
<form method="post" action="<?php echo htmlspecialchars($current_name); ?>" id="batchDeleteForm">
<input type="hidden" name="action" value="batch_delete_config">
<input type="hidden" name="q" value="<?php echo htmlspecialchars($listSearchQ, ENT_QUOTES); ?>">
<div class="list-table-wrap"> <div class="list-table-wrap">
<table class="table config-table mb-0"> <table class="table config-table mb-0">
<thead> <thead>
<tr> <tr>
<th style="width:22%">配置名称</th> <th class="col-check"><input type="checkbox" id="checkAllConfigs" title="全选"></th>
<th style="width:14%">绑定域名</th> <th style="width:20%">配置名称</th>
<th style="width:12%">绑定域名</th>
<th>广告链接(广告后台填入的链接)</th> <th>广告链接(广告后台填入的链接)</th>
<th>真实页链接(客户到达的链接)</th> <th>真实页链接(客户到达的链接)</th>
<th style="width:22%">操作</th> <th style="width:20%">操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($products_file_ary as $pf): <?php if (empty($configPage['items'])): ?>
<tr><td colspan="6" class="empty-hint">暂无匹配的配置<?php if ($listSearchQ !== ''): ?>,请调整搜索关键词<?php endif; ?></td></tr>
<?php endif; ?>
<?php foreach ($configPage['items'] as $cfgRow):
$pf = $cfgRow['name'];
$pcfg = "check_config/{$pf}_config.php"; $pcfg = "check_config/{$pf}_config.php";
$pfp = []; $pfp = [];
if (file_exists($pcfg)) { if (file_exists($pcfg)) {
@@ -664,6 +889,11 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
} }
?> ?>
<tr> <tr>
<td class="col-check">
<?php if ($pf !== 'index'): ?>
<input type="checkbox" class="config-row-check" name="names[]" value="<?php echo htmlspecialchars($pf, ENT_QUOTES); ?>">
<?php endif; ?>
</td>
<td class="config-name-cell"> <td class="config-name-cell">
<strong><?php echo htmlspecialchars($pf); ?></strong> <strong><?php echo htmlspecialchars($pf); ?></strong>
<?php if ($pf === 'index'): ?><span class="badge badge-secondary ml-1" style="font-size:.7rem;">默认</span><?php endif; ?> <?php if ($pf === 'index'): ?><span class="badge badge-secondary ml-1" style="font-size:.7rem;">默认</span><?php endif; ?>
@@ -722,6 +952,35 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
</tbody> </tbody>
</table> </table>
</div> </div>
<?php if ($configPage['total_pages'] > 1 || $configPage['total'] > 0): ?>
<div class="list-pagination">
<span>第 <?php echo (int) $configPage['page']; ?> / <?php echo (int) $configPage['total_pages']; ?> 页,共 <?php echo (int) $configPage['total']; ?> 条</span>
<?php if ($configPage['total_pages'] > 1): ?>
<nav aria-label="配置分页">
<ul class="pagination pagination-sm">
<?php
$curPage = (int) $configPage['page'];
$maxPage = (int) $configPage['total_pages'];
$prevPage = max(1, $curPage - 1);
$nextPage = min($maxPage, $curPage + 1);
?>
<li class="page-item<?php echo $curPage <= 1 ? ' disabled' : ''; ?>">
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $prevPage)); ?>">上一页</a>
</li>
<?php for ($p = 1; $p <= $maxPage; $p++): ?>
<li class="page-item<?php echo $p === $curPage ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $p)); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
<li class="page-item<?php echo $curPage >= $maxPage ? ' disabled' : ''; ?>">
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $nextPage)); ?>">下一页</a>
</li>
</ul>
</nav>
<?php endif; ?>
</div>
<?php endif; ?>
</form>
</div> </div>
<!-- 站点域名 --> <!-- 站点域名 -->
@@ -732,19 +991,37 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
<div style="padding:16px 20px;"> <div style="padding:16px 20px;">
<?php if ($cloakCfEnabled): ?> <?php if ($cloakCfEnabled): ?>
<div class="alert alert-info mb-3" style="font-size:.88rem;"> <div class="alert alert-info mb-3" style="font-size:.88rem;">
<strong>Cloudflare 已启用:</strong>添加域名后将自动创建 Zone 并显示 Nameserver;请到注册商修改 NS 后点击「检测/刷新」。NS 生效后将自动配置 proxied A 记录、Flexible SSL 与 Always HTTPS <strong>Cloudflare 已启用:</strong>选择「自动」时将创建 Zone 并配置 DNS/SSL;选择「手动」时仅本地登记(您已自行配置 DNS)
源站仍需在宝塔添加站点并监听 <code><?php echo htmlspecialchars(CloudflareConfig::serverIp() ?: 'SERVER_IP'); ?></code>Flexible:访客↔CF 为 HTTPSCF↔源站为 HTTP 源站仍需在宝塔添加站点并监听 <code><?php echo htmlspecialchars(CloudflareConfig::serverIp() ?: 'SERVER_IP'); ?></code>。
</div> </div>
<?php else: ?> <?php else: ?>
<div class="alert alert-warning mb-3" style="font-size:.88rem;"> <div class="alert alert-warning mb-3" style="font-size:.88rem;">
<strong>使用前请先在宝塔完成:</strong>在宝塔面板 → 网站 → 添加站点(填写下方相同域名)→ 申请并开启 SSL 证书。完成后再在此添加域名并关联配置 <strong>提示:</strong>自动模式需先在 <code>cong.php</code> 配置 Cloudflare API Token 与 Account ID。若已自行完成 DNS 解析,请选择「手动」模式
若在 <code>cong.php</code> 配置 Cloudflare API Token 与 Account ID,可自动创建 Zone 并管理 DNS/SSL。 使用前请先在宝塔添加站点并开启 SSL。
</div> </div>
<?php endif; ?> <?php endif; ?>
<form method="post" action="<?php echo $current_name; ?>" class="form-inline mb-3"> <form method="post" action="<?php echo $current_name; ?>" class="mb-3">
<input type="hidden" name="action" value="add_domain"> <input type="hidden" name="action" value="add_domain">
<input type="text" name="new_hostname" class="form-control form-control-sm mr-2" style="min-width:280px;" placeholder="例如 shop.example.com" required> <div class="form-group mb-2">
<button type="submit" class="btn btn-sm" style="background:#16baaa;color:#fff;">添加域名</button> <label class="mb-1"><strong>域名列表</strong> <small class="text-muted">(每行一个,也支持逗号分隔)</small></label>
<textarea name="new_hostnames" class="form-control form-control-sm" rows="5" style="max-width:520px;font-family:monospace;" placeholder="shop.example.com&#10;api.example.com&#10;landing.example.com" required></textarea>
</div>
<div class="form-group mb-2">
<label class="d-block mb-1"><strong>DNS 配置方式</strong></label>
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm btn-outline-primary active">
<input type="radio" name="dns_mode" value="auto" checked autocomplete="off"> 自动
</label>
<label class="btn btn-sm btn-outline-primary">
<input type="radio" name="dns_mode" value="manual" autocomplete="off"> 手动
</label>
</div>
<small class="text-muted d-block mt-1">
自动:通过 Cloudflare 创建 Zone 并配置 DNS/SSL(默认)。
手动:仅登记域名,不调用 Cloudflare,无需检测/刷新。
</small>
</div>
<button type="submit" class="btn btn-sm" style="background:#16baaa;color:#fff;">批量添加</button>
</form> </form>
<?php if (empty($cloakSiteDomains)): ?> <?php if (empty($cloakSiteDomains)): ?>
<div class="empty-hint">尚未添加域名。未添加时,广告链接将使用当前后台访问域名:<code><?php echo htmlspecialchars($dashboardDefaultHost); ?></code></div> <div class="empty-hint">尚未添加域名。未添加时,广告链接将使用当前后台访问域名:<code><?php echo htmlspecialchars($dashboardDefaultHost); ?></code></div>
@@ -763,6 +1040,7 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
$cfLabel = CloudflareConfig::statusLabel($cfStatus); $cfLabel = CloudflareConfig::statusLabel($cfStatus);
$badgeCls = 'secondary'; $badgeCls = 'secondary';
if ($cfStatus === 'pending_ns') { $badgeCls = 'warning'; } if ($cfStatus === 'pending_ns') { $badgeCls = 'warning'; }
elseif ($cfStatus === 'manual') { $badgeCls = 'secondary'; }
elseif ($cfStatus === 'provisioning') { $badgeCls = 'info'; } elseif ($cfStatus === 'provisioning') { $badgeCls = 'info'; }
elseif ($cfStatus === 'ready') { $badgeCls = 'success'; } elseif ($cfStatus === 'ready') { $badgeCls = 'success'; }
elseif ($cfStatus === 'error') { $badgeCls = 'danger'; } elseif ($cfStatus === 'error') { $badgeCls = 'danger'; }
@@ -775,10 +1053,13 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
<tr> <tr>
<td><span class="domain-host-badge" title="<?php echo htmlspecialchars($dom['hostname'], ENT_QUOTES); ?>"><?php echo htmlspecialchars($dom['hostname']); ?></span></td> <td><span class="domain-host-badge" title="<?php echo htmlspecialchars($dom['hostname'], ENT_QUOTES); ?>"><?php echo htmlspecialchars($dom['hostname']); ?></span></td>
<td><?php <td><?php
if ($dom['config_name'] !== '') { $boundConfigs = $domainConfigMap[(int) $dom['id']] ?? [];
echo '<span class="badge badge-light border">' . htmlspecialchars($dom['config_name']) . '</span>'; if ($boundConfigs === []) {
} else {
echo '<span class="text-muted">未关联</span>'; echo '<span class="text-muted">未关联</span>';
} else {
foreach ($boundConfigs as $cfgName) {
echo '<span class="badge badge-light border mr-1">' . htmlspecialchars($cfgName) . '</span>';
}
} }
?></td> ?></td>
<?php if ($cloakCfEnabled): ?> <?php if ($cloakCfEnabled): ?>
@@ -863,11 +1144,17 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
<option value="0"<?php echo $selectedDomainId === 0 ? ' selected' : ''; ?>> <option value="0"<?php echo $selectedDomainId === 0 ? ' selected' : ''; ?>>
不指定(广告链接使用当前后台域名:<?php echo htmlspecialchars($dashboardDefaultHost); ?> 不指定(广告链接使用当前后台域名:<?php echo htmlspecialchars($dashboardDefaultHost); ?>
</option> </option>
<?php foreach ($cloakSiteDomains as $dom): ?> <?php if (count($cloakSiteDomains) >= 2 && ($isNewConfigDraft || $selectedDomainId === 0)): ?>
<option value="random">随机域名(从全部站点域名中随机分配)</option>
<?php endif; ?>
<?php foreach ($cloakSiteDomains as $dom):
$boundOnDom = $domainConfigMap[(int) $dom['id']] ?? [];
$boundCount = count($boundOnDom);
?>
<option value="<?php echo (int) $dom['id']; ?>"<?php echo $selectedDomainId === (int) $dom['id'] ? ' selected' : ''; ?>> <option value="<?php echo (int) $dom['id']; ?>"<?php echo $selectedDomainId === (int) $dom['id'] ? ' selected' : ''; ?>>
<?php echo htmlspecialchars($dom['hostname']); ?> <?php echo htmlspecialchars($dom['hostname']); ?>
<?php if ($dom['config_name'] !== '' && $dom['config_name'] !== $check_name): ?> <?php if ($boundCount > 0): ?>
已绑:<?php echo htmlspecialchars($dom['config_name']); ?> <?php echo $boundCount; ?> 个配置已关联
<?php endif; ?> <?php endif; ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
@@ -1009,10 +1296,10 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
<td class="form-val-cell"> <td class="form-val-cell">
<div class="d-flex align-items-center gap-2" style="gap:10px;"> <div class="d-flex align-items-center gap-2" style="gap:10px;">
<div class="btn-group btn-group-toggle" data-toggle="buttons"> <div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm btn-outline-secondary <?php if(AUTO_BLACK=='ON') echo 'active'; ?>"> <label class="btn btn-sm btn-outline-primary <?php if(AUTO_BLACK=='ON') echo 'active'; ?>">
<input type="radio" name="auto_black" id="auto_black_on" value="ON" <?php if(AUTO_BLACK=='ON') echo 'checked'; ?>> 开启 <input type="radio" name="auto_black" id="auto_black_on" value="ON" <?php if(AUTO_BLACK=='ON') echo 'checked'; ?>> 开启
</label> </label>
<label class="btn btn-sm btn-outline-secondary <?php if(AUTO_BLACK!='ON') echo 'active'; ?>"> <label class="btn btn-sm btn-outline-primary <?php if(AUTO_BLACK!='ON') echo 'active'; ?>">
<input type="radio" name="auto_black" id="auto_black_off" value="OFF" <?php if(AUTO_BLACK!='ON') echo 'checked'; ?>> 关闭 <input type="radio" name="auto_black" id="auto_black_off" value="OFF" <?php if(AUTO_BLACK!='ON') echo 'checked'; ?>> 关闭
</label> </label>
</div> </div>
@@ -1200,10 +1487,10 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
<td class="form-label-cell">记录访客日志</td> <td class="form-label-cell">记录访客日志</td>
<td class="form-val-cell"> <td class="form-val-cell">
<div class="btn-group btn-group-toggle" data-toggle="buttons"> <div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm btn-outline-secondary <?php if(WRITE_LOG=='1') echo 'active'; ?>"> <label class="btn btn-sm btn-outline-primary <?php if(WRITE_LOG=='1') echo 'active'; ?>">
<input type="radio" name="write_log" value="1" <?php if(WRITE_LOG=='1') echo 'checked'; ?>> 开启 <input type="radio" name="write_log" value="1" <?php if(WRITE_LOG=='1') echo 'checked'; ?>> 开启
</label> </label>
<label class="btn btn-sm btn-outline-secondary <?php if(WRITE_LOG!='1') echo 'active'; ?>"> <label class="btn btn-sm btn-outline-primary <?php if(WRITE_LOG!='1') echo 'active'; ?>">
<input type="radio" name="write_log" value="0" <?php if(WRITE_LOG!='1') echo 'checked'; ?>> 关闭 <input type="radio" name="write_log" value="0" <?php if(WRITE_LOG!='1') echo 'checked'; ?>> 关闭
</label> </label>
</div> </div>
@@ -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() { function delUrlArgs() {
if (!confirm('确认重置临时链接参数计时?')) return; if (!confirm('确认重置临时链接参数计时?')) return;
window.location.href = '<?php echo $current_name; ?>?action=delUrlArgs&check_name=<?php echo urlencode($check_name); ?>'; window.location.href = '<?php echo $current_name; ?>?action=delUrlArgs&check_name=<?php echo urlencode($check_name); ?>';
-4
View File
@@ -1,4 +0,0 @@
<?php
require('ip_check.php');
+7
View File
@@ -1,2 +1,9 @@
2026-06-26 15:13:02 | 写入数据库 | false | index | async=1 2026-06-26 15:13:02 | 写入数据库 | false | index | async=1
2026-06-27 15:43:53 | 写入数据库 | false | index | async=1 2026-06-27 15:43:53 | 写入数据库 | false | index | async=1
2026-06-28 22:07:37 | 写入数据库 | false | index | async=1
2026-06-29 22:39:01 | 写入数据库 | false | index | async=1
2026-06-30 23:18:53 | 写入数据库 | false | index | async=1
2026-07-02 02:18:06 | 写入数据库 | false | index | async=1
2026-07-03 08:37:20 | 写入数据库 | false | index | async=1
2026-07-04 10:19:04 | 写入数据库 | false | index | async=1
2026-07-05 10:39:01 | 写入数据库 | false | index | async=1
+1
View File
@@ -50,6 +50,7 @@ class CloudflareConfig
{ {
$map = [ $map = [
'local' => '本地登记', 'local' => '本地登记',
'manual' => '手动解析',
'pending_ns' => '待检测', 'pending_ns' => '待检测',
'provisioning' => '配置中', 'provisioning' => '配置中',
'ready' => '已生效', 'ready' => '已生效',
+62 -10
View File
@@ -29,27 +29,33 @@ class DomainProvisioningService
/** /**
* 添加域名:本地入库 + 可选创建 Cloudflare Zone * 添加域名:本地入库 + 可选创建 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); $add = DomainRepository::add($pdo, $hostname);
if (!$add['ok']) { if (!$add['ok']) {
return $add; return $add;
} }
$id = (int) $add['id']; $id = (int) $add['id'];
$host = $add['hostname']; $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, [ DomainRepository::updateCloudflareMeta($pdo, $id, [
'cf_status' => 'local', 'cf_status' => $status,
'cf_error' => '',
]); ]);
return [ return [
'ok' => true, 'ok' => true,
'id' => $id, 'id' => $id,
'hostname' => $host, 'hostname' => $host,
'cf_status' => 'local', 'cf_status' => $status,
'dns_mode' => $dnsMode,
'nameservers' => [], 'nameservers' => [],
]; ];
} }
@@ -84,10 +90,52 @@ class DomainProvisioningService
'id' => $id, 'id' => $id,
'hostname' => $host, 'hostname' => $host,
'cf_status' => 'pending_ns', 'cf_status' => 'pending_ns',
'dns_mode' => 'auto',
'nameservers' => $ns, '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 * 手动检测 NS 是否生效,生效后配置 A 记录与 SSL
* *
@@ -100,6 +148,10 @@ class DomainProvisioningService
return ['ok' => false, 'message' => '域名不存在']; return ['ok' => false, 'message' => '域名不存在'];
} }
if (($row['cf_status'] ?? '') === 'manual') {
return ['ok' => false, 'message' => '该域名为手动解析模式,无需 Cloudflare 检测'];
}
if (!CloudflareConfig::isEnabled()) { if (!CloudflareConfig::isEnabled()) {
return ['ok' => false, 'message' => '未配置 Cloudflare API,无法检测']; return ['ok' => false, 'message' => '未配置 Cloudflare API,无法检测'];
} }
+155 -13
View File
@@ -23,6 +23,50 @@ class DomainRepository
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
); );
self::ensureCloudflareColumns($pdo); 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) private static function ensureCloudflareColumns(PDO $pdo)
@@ -86,6 +130,35 @@ class DomainRepository
return $host; 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) public static function hostToConfigKey($host)
{ {
$host = self::normalizeHostname($host); $host = self::normalizeHostname($host);
@@ -171,8 +244,10 @@ class DomainRepository
public static function deleteById(PDO $pdo, $id) public static function deleteById(PDO $pdo, $id)
{ {
self::ensureTable($pdo); 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 = $pdo->prepare('DELETE FROM cloak_site_domains WHERE id = ?');
$st->execute([(int) $id]); $st->execute([$id]);
return $st->rowCount() > 0; return $st->rowCount() > 0;
} }
@@ -184,35 +259,98 @@ class DomainRepository
if ($configName === '' || $domainId <= 0) { if ($configName === '' || $domainId <= 0) {
return false; return false;
} }
$pdo->prepare("UPDATE cloak_site_domains SET config_name = '' WHERE config_name = ?")->execute([$configName]); if (self::findById($pdo, $domainId) === null) {
$st = $pdo->prepare('UPDATE cloak_site_domains SET config_name = ? WHERE id = ?'); 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]); $st->execute([$configName, $domainId]);
return $st->rowCount() > 0;
return true;
} }
public static function clearConfigBinding(PDO $pdo, $configName) public static function clearConfigBinding(PDO $pdo, $configName)
{ {
self::ensureTable($pdo); 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)]); $st->execute([trim($configName)]);
} }
public static function domainIdForConfig(PDO $pdo, $configName) public static function domainIdForConfig(PDO $pdo, $configName)
{ {
self::ensureTable($pdo); 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)]); $st->execute([trim($configName)]);
$row = $st->fetch(); $row = $st->fetch();
return $row ? (int) $row['id'] : 0;
return $row ? (int) $row['domain_id'] : 0;
} }
public static function primaryHostnameForConfig(PDO $pdo, $configName) public static function primaryHostnameForConfig(PDO $pdo, $configName)
{ {
self::ensureTable($pdo); 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)]); $st->execute([trim($configName)]);
$row = $st->fetch(); $row = $st->fetch();
return $row ? $row['hostname'] : '';
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);
} }
public static function resolveConfigNameForHost($host, $baseDir = null) public static function resolveConfigNameForHost($host, $baseDir = null)
@@ -222,12 +360,15 @@ class DomainRepository
if ($norm !== '') { if ($norm !== '') {
try { try {
$pdo = self::pdo(); $pdo = self::pdo();
self::ensureTable($pdo);
$row = self::findByHostname($pdo, $norm); $row = self::findByHostname($pdo, $norm);
if ($row && !empty($row['config_name'])) { if ($row) {
$name = trim($row['config_name']); $key = self::hostToConfigKey($norm);
if (file_exists($baseDir . '/check_config/' . $name . '_config.php')) { if (file_exists($baseDir . '/check_config/' . $key . '_config.php')) {
return $name; return $key;
} }
return 'index';
} }
} catch (Throwable $e) { } catch (Throwable $e) {
} }
@@ -236,6 +377,7 @@ class DomainRepository
return $key; return $key;
} }
} }
return 'index'; return 'index';
} }
} }
+5 -2
View File
@@ -157,8 +157,11 @@ class VisitorSimulationRunner
$pdo = DomainRepository::pdo(); $pdo = DomainRepository::pdo();
DomainRepository::ensureTable($pdo); DomainRepository::ensureTable($pdo);
$domRow = DomainRepository::findByHostname($pdo, $domain); $domRow = DomainRepository::findByHostname($pdo, $domain);
if ($domRow && !empty($domRow['config_name'])) { if ($domRow) {
$config = trim($domRow['config_name']); $names = DomainRepository::configNamesForDomainId($pdo, (int) ($domRow['id'] ?? 0));
if (count($names) === 1) {
$config = $names[0];
}
} }
} catch (Throwable $e) { } catch (Throwable $e) {
} }
+9 -1
View File
@@ -40,6 +40,14 @@ class InstallSchema
KEY `idx_config_name` (`config_name`) KEY `idx_config_name` (`config_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci", ) 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` ( "CREATE TABLE IF NOT EXISTS `visitor_logs` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`campagin_id` varchar(20) NOT NULL COMMENT '策略ID', `campagin_id` varchar(20) NOT NULL COMMENT '策略ID',
@@ -101,7 +109,7 @@ class InstallSchema
return [ return [
'ok' => true, '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'],
]; ];
} }
} }
+23
View File
@@ -108,6 +108,29 @@ try {
$del = DomainProvisioningService::deleteWithCloudflare($pdo, $domainId); $del = DomainProvisioningService::deleteWithCloudflare($pdo, $domainId);
$tests['delete_calls_zone'] = ($del['ok'] ?? false) === true && $mock->deleteCalled === true; $tests['delete_calls_zone'] = ($del['ok'] ?? false) === true && $mock->deleteCalled === true;
$tests['delete_row_gone'] = DomainRepository::findById($pdo, $domainId) === null; $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) { } catch (Throwable $e) {
$tests['exception'] = false; $tests['exception'] = false;
$tests['error'] = $e->getMessage(); $tests['error'] = $e->getMessage();
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env php
<?php
/**
* 随机域名选取与多配置绑定逻辑验证
*
* 用法:php tools/verify_domain_random.php
*/
if (PHP_SAPI !== 'cli') {
fwrite(STDERR, "请在 CLI 下运行\n");
exit(1);
}
require_once dirname(__DIR__) . '/cong.php';
require_once dirname(__DIR__) . '/lib/Cloak/DomainRepository.php';
$passed = 0;
$failed = 0;
function vassert($name, $cond, $detail = '')
{
global $passed, $failed;
if ($cond) {
$passed++;
return;
}
$failed++;
echo "FAIL: {$name}\n";
if ($detail !== '') {
echo " {$detail}\n";
}
}
try {
$pdo = DomainRepository::pdo();
DomainRepository::ensureTable($pdo);
$prefix = 'verify-rand-' . substr(md5((string) microtime(true)), 0, 8) . '.test';
$hostA = 'a.' . $prefix;
$hostB = 'b.' . $prefix;
$hostBound = 'bound.' . $prefix;
$ids = [];
foreach ([$hostA, $hostB, $hostBound] as $host) {
$res = DomainRepository::add($pdo, $host);
vassert('add domain ' . $host, !empty($res['ok']), $res['error'] ?? '');
if (!empty($res['id'])) {
$ids[] = (int) $res['id'];
}
}
if (count($ids) >= 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);