2026-06-14 14:00:24 +08:00
|
|
|
<?php
|
|
|
|
|
require_once __DIR__ . '/DomainRepository.php';
|
|
|
|
|
require_once __DIR__ . '/CloudflareConfig.php';
|
|
|
|
|
require_once __DIR__ . '/CloudflareClient.php';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 域名添加 / NS 检测 / DNS+SSL 配置 / 删除编排
|
|
|
|
|
*/
|
|
|
|
|
class DomainProvisioningService
|
|
|
|
|
{
|
|
|
|
|
/** @var CloudflareClient|null CLI 回归注入 Mock 客户端 */
|
|
|
|
|
private static $clientOverride = null;
|
|
|
|
|
|
|
|
|
|
public static function setClientOverride($client)
|
|
|
|
|
{
|
|
|
|
|
self::$clientOverride = $client;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function clearClientOverride()
|
|
|
|
|
{
|
|
|
|
|
self::$clientOverride = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function newClient()
|
|
|
|
|
{
|
|
|
|
|
return self::$clientOverride !== null ? self::$clientOverride : new CloudflareClient();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 添加域名:本地入库 + 可选创建 Cloudflare Zone
|
|
|
|
|
*
|
2026-07-06 10:34:34 +08:00
|
|
|
* @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}
|
2026-06-14 14:00:24 +08:00
|
|
|
*/
|
2026-07-06 10:34:34 +08:00
|
|
|
public static function provisionOnAdd(PDO $pdo, $hostname, $dnsMode = 'auto')
|
2026-06-14 14:00:24 +08:00
|
|
|
{
|
|
|
|
|
$add = DomainRepository::add($pdo, $hostname);
|
|
|
|
|
if (!$add['ok']) {
|
|
|
|
|
return $add;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 10:34:34 +08:00
|
|
|
$id = (int) $add['id'];
|
|
|
|
|
$host = $add['hostname'];
|
|
|
|
|
$dnsMode = strtolower(trim((string) $dnsMode)) === 'manual' ? 'manual' : 'auto';
|
2026-06-14 14:00:24 +08:00
|
|
|
|
2026-07-06 10:34:34 +08:00
|
|
|
if ($dnsMode === 'manual' || !CloudflareConfig::isEnabled()) {
|
|
|
|
|
$status = $dnsMode === 'manual' ? 'manual' : 'local';
|
2026-06-14 14:00:24 +08:00
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $id, [
|
2026-07-06 10:34:34 +08:00
|
|
|
'cf_status' => $status,
|
|
|
|
|
'cf_error' => '',
|
2026-06-14 14:00:24 +08:00
|
|
|
]);
|
2026-07-06 10:34:34 +08:00
|
|
|
|
2026-06-14 14:00:24 +08:00
|
|
|
return [
|
2026-07-06 10:34:34 +08:00
|
|
|
'ok' => true,
|
|
|
|
|
'id' => $id,
|
|
|
|
|
'hostname' => $host,
|
|
|
|
|
'cf_status' => $status,
|
|
|
|
|
'dns_mode' => $dnsMode,
|
2026-06-14 14:00:24 +08:00
|
|
|
'nameservers' => [],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$client = self::newClient();
|
|
|
|
|
$zone = $client->createZone($host);
|
|
|
|
|
if (!$zone['ok']) {
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $id, [
|
|
|
|
|
'cf_status' => 'error',
|
|
|
|
|
'cf_error' => $zone['error'] ?? '创建 Zone 失败',
|
|
|
|
|
]);
|
|
|
|
|
return [
|
|
|
|
|
'ok' => false,
|
|
|
|
|
'error' => $zone['error'] ?? 'Cloudflare 创建 Zone 失败',
|
|
|
|
|
'id' => $id,
|
|
|
|
|
'hostname' => $host,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result = $zone['result'];
|
|
|
|
|
$ns = is_array($result['name_servers'] ?? null) ? $result['name_servers'] : [];
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $id, [
|
|
|
|
|
'cf_zone_id' => (string) ($result['id'] ?? ''),
|
|
|
|
|
'cf_nameservers' => json_encode($ns, JSON_UNESCAPED_UNICODE),
|
|
|
|
|
'cf_status' => 'pending_ns',
|
|
|
|
|
'cf_error' => '',
|
|
|
|
|
'cf_checked_at' => date('Y-m-d H:i:s'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'id' => $id,
|
|
|
|
|
'hostname' => $host,
|
|
|
|
|
'cf_status' => 'pending_ns',
|
2026-07-06 10:34:34 +08:00
|
|
|
'dns_mode' => 'auto',
|
2026-06-14 14:00:24 +08:00
|
|
|
'nameservers' => $ns,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 10:34:34 +08:00
|
|
|
/**
|
|
|
|
|
* 批量添加域名
|
|
|
|
|
*
|
|
|
|
|
* @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,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 14:00:24 +08:00
|
|
|
/**
|
|
|
|
|
* 手动检测 NS 是否生效,生效后配置 A 记录与 SSL
|
|
|
|
|
*
|
|
|
|
|
* @return array{ok:bool,status?:string,message?:string,nameservers?:array}
|
|
|
|
|
*/
|
|
|
|
|
public static function checkAndProvision(PDO $pdo, $domainId)
|
|
|
|
|
{
|
|
|
|
|
$row = DomainRepository::findById($pdo, $domainId);
|
|
|
|
|
if (!$row) {
|
|
|
|
|
return ['ok' => false, 'message' => '域名不存在'];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 10:34:34 +08:00
|
|
|
if (($row['cf_status'] ?? '') === 'manual') {
|
|
|
|
|
return ['ok' => false, 'message' => '该域名为手动解析模式,无需 Cloudflare 检测'];
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 14:00:24 +08:00
|
|
|
if (!CloudflareConfig::isEnabled()) {
|
|
|
|
|
return ['ok' => false, 'message' => '未配置 Cloudflare API,无法检测'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$client = self::newClient();
|
|
|
|
|
$zoneId = trim((string) ($row['cf_zone_id'] ?? ''));
|
|
|
|
|
|
|
|
|
|
if ($zoneId === '') {
|
|
|
|
|
$zone = $client->createZone($row['hostname']);
|
|
|
|
|
if (!$zone['ok']) {
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
|
|
|
|
'cf_status' => 'error',
|
|
|
|
|
'cf_error' => $zone['error'] ?? '补创建 Zone 失败',
|
|
|
|
|
'cf_checked_at' => date('Y-m-d H:i:s'),
|
|
|
|
|
]);
|
|
|
|
|
return ['ok' => false, 'message' => $zone['error'] ?? '补创建 Zone 失败'];
|
|
|
|
|
}
|
|
|
|
|
$result = $zone['result'];
|
|
|
|
|
$zoneId = (string) ($result['id'] ?? '');
|
|
|
|
|
$ns = is_array($result['name_servers'] ?? null) ? $result['name_servers'] : [];
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
|
|
|
|
'cf_zone_id' => $zoneId,
|
|
|
|
|
'cf_nameservers' => json_encode($ns, JSON_UNESCAPED_UNICODE),
|
|
|
|
|
'cf_status' => 'pending_ns',
|
|
|
|
|
'cf_error' => '',
|
|
|
|
|
'cf_checked_at' => date('Y-m-d H:i:s'),
|
|
|
|
|
]);
|
|
|
|
|
$row = DomainRepository::findById($pdo, $domainId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$zoneRes = $client->getZone($zoneId);
|
|
|
|
|
if (!$zoneRes['ok']) {
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
|
|
|
|
'cf_status' => 'error',
|
|
|
|
|
'cf_error' => $zoneRes['error'] ?? '读取 Zone 失败',
|
|
|
|
|
'cf_checked_at' => date('Y-m-d H:i:s'),
|
|
|
|
|
]);
|
|
|
|
|
return ['ok' => false, 'message' => $zoneRes['error'] ?? '读取 Zone 失败'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$zoneData = $zoneRes['result'];
|
|
|
|
|
$status = strtolower((string) ($zoneData['status'] ?? 'pending'));
|
|
|
|
|
$ns = CloudflareConfig::parseNameservers($row['cf_nameservers'] ?? '');
|
|
|
|
|
if (empty($ns) && is_array($zoneData['name_servers'] ?? null)) {
|
|
|
|
|
$ns = $zoneData['name_servers'];
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
|
|
|
|
'cf_nameservers' => json_encode($ns, JSON_UNESCAPED_UNICODE),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($status !== 'active') {
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
|
|
|
|
'cf_status' => 'pending_ns',
|
|
|
|
|
'cf_checked_at' => date('Y-m-d H:i:s'),
|
|
|
|
|
'cf_error' => '',
|
|
|
|
|
]);
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'status' => 'pending_ns',
|
|
|
|
|
'message' => 'Nameserver 尚未生效,请到注册商修改为:' . implode('、', $ns),
|
|
|
|
|
'nameservers' => $ns,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$serverIp = CloudflareConfig::serverIp();
|
|
|
|
|
if ($serverIp === '') {
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
|
|
|
|
'cf_status' => 'error',
|
|
|
|
|
'cf_error' => 'SERVER_IP 未配置,无法添加 DNS 记录',
|
|
|
|
|
'cf_checked_at' => date('Y-m-d H:i:s'),
|
|
|
|
|
]);
|
|
|
|
|
return ['ok' => false, 'message' => 'SERVER_IP 未配置,请在 cong.php 中填写服务器公网 IP'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
|
|
|
|
'cf_status' => 'provisioning',
|
|
|
|
|
'cf_error' => '',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$host = $row['hostname'];
|
|
|
|
|
foreach ([$host, 'www.' . $host] as $fqdn) {
|
|
|
|
|
$dns = $client->upsertARecord($zoneId, $fqdn, $serverIp, true);
|
|
|
|
|
if (!$dns['ok']) {
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
|
|
|
|
'cf_status' => 'error',
|
|
|
|
|
'cf_error' => 'DNS 记录失败(' . $fqdn . '): ' . ($dns['error'] ?? ''),
|
|
|
|
|
'cf_checked_at' => date('Y-m-d H:i:s'),
|
|
|
|
|
]);
|
|
|
|
|
return ['ok' => false, 'message' => $dns['error'] ?? 'DNS 配置失败'];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$ssl = $client->setSslFlexible($zoneId);
|
|
|
|
|
if (!$ssl['ok']) {
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
|
|
|
|
'cf_status' => 'error',
|
|
|
|
|
'cf_error' => 'SSL Flexible 失败: ' . ($ssl['error'] ?? ''),
|
|
|
|
|
'cf_checked_at' => date('Y-m-d H:i:s'),
|
|
|
|
|
]);
|
|
|
|
|
return ['ok' => false, 'message' => $ssl['error'] ?? 'SSL 配置失败'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$https = $client->setAlwaysHttps($zoneId);
|
|
|
|
|
if (!$https['ok']) {
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
|
|
|
|
'cf_status' => 'error',
|
|
|
|
|
'cf_error' => 'Always HTTPS 失败: ' . ($https['error'] ?? ''),
|
|
|
|
|
'cf_checked_at' => date('Y-m-d H:i:s'),
|
|
|
|
|
]);
|
|
|
|
|
return ['ok' => false, 'message' => $https['error'] ?? 'Always HTTPS 配置失败'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
|
|
|
|
'cf_status' => 'ready',
|
|
|
|
|
'cf_error' => '',
|
|
|
|
|
'cf_checked_at' => date('Y-m-d H:i:s'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'status' => 'ready',
|
|
|
|
|
'message' => '域名已生效:A 记录(@/www) 已指向 ' . $serverIp . ',已开启 Flexible SSL 与 Always HTTPS',
|
|
|
|
|
'nameservers' => $ns,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 删除本地记录并尝试删除 Cloudflare Zone
|
|
|
|
|
*
|
|
|
|
|
* @return array{ok:bool,message?:string,warning?:string}
|
|
|
|
|
*/
|
|
|
|
|
public static function deleteWithCloudflare(PDO $pdo, $domainId)
|
|
|
|
|
{
|
|
|
|
|
$row = DomainRepository::findById($pdo, $domainId);
|
|
|
|
|
if (!$row) {
|
|
|
|
|
return ['ok' => false, 'message' => '域名不存在'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$warning = '';
|
|
|
|
|
$zoneId = trim((string) ($row['cf_zone_id'] ?? ''));
|
|
|
|
|
if ($zoneId !== '' && CloudflareConfig::isEnabled()) {
|
|
|
|
|
$del = self::newClient()->deleteZone($zoneId);
|
|
|
|
|
if (!$del['ok']) {
|
|
|
|
|
$warning = 'Cloudflare Zone 删除失败:' . ($del['error'] ?? '未知错误');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DomainRepository::deleteById($pdo, $domainId);
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'message' => '域名已删除',
|
|
|
|
|
'warning' => $warning,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|