域名管理
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\model\Domain as DomainModel;
|
||||
use app\common\controller\Backend;
|
||||
use app\common\service\CloudflareService;
|
||||
use app\common\service\DomainDetectRateLimitService;
|
||||
use think\Db;
|
||||
use think\Exception;
|
||||
use think\exception\PDOException;
|
||||
use think\exception\ValidateException;
|
||||
|
||||
/**
|
||||
* 域名管理
|
||||
*
|
||||
* @icon fa fa-globe
|
||||
* @remark 域名接入Cloudflare,新增后请在详情查看NS并到注册商修改
|
||||
*/
|
||||
class Domain extends Backend
|
||||
{
|
||||
/** @var DomainModel */
|
||||
protected $model = null;
|
||||
|
||||
protected $searchFields = 'domain';
|
||||
protected $dataLimit = 'personal';
|
||||
protected $modelValidate = true;
|
||||
protected $modelSceneValidate = true;
|
||||
|
||||
public function _initialize()
|
||||
{
|
||||
parent::_initialize();
|
||||
$this->model = new DomainModel();
|
||||
$this->view->assign('zoneStatusList', $this->model->getZoneStatusList());
|
||||
$this->view->assign('nsStatusList', $this->model->getNsStatusList());
|
||||
$this->view->assign('dnsStatusList', $this->model->getDnsStatusList());
|
||||
$this->assignconfig('zoneStatusList', $this->model->getZoneStatusList());
|
||||
$this->assignconfig('nsStatusList', $this->model->getNsStatusList());
|
||||
$this->assignconfig('dnsStatusList', $this->model->getDnsStatusList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁止编辑
|
||||
*/
|
||||
public function edit($ids = null)
|
||||
{
|
||||
$this->error(__('Domain cannot be edited after creation'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁止批量操作
|
||||
*/
|
||||
public function multi($ids = '')
|
||||
{
|
||||
$this->error(__('Invalid parameters'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加域名并创建 Cloudflare Zone
|
||||
*/
|
||||
public function add()
|
||||
{
|
||||
if (false === $this->request->isPost()) {
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
$params = $this->request->post('row/a', []);
|
||||
if ($params === []) {
|
||||
$this->error(__('Parameter %s can not be empty', ''));
|
||||
}
|
||||
|
||||
$params = $this->preExcludeFields($params);
|
||||
$domain = DomainModel::normalizeDomain((string)($params['domain'] ?? ''));
|
||||
$params['domain'] = $domain;
|
||||
|
||||
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
|
||||
$params[$this->dataLimitField] = $this->auth->id;
|
||||
}
|
||||
|
||||
$result = false;
|
||||
Db::startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$name = str_replace('\\model\\', '\\validate\\', get_class($this->model));
|
||||
$validate = $this->modelSceneValidate ? $name . '.add' : $name;
|
||||
$this->model->validateFailException()->validate($validate, $params);
|
||||
}
|
||||
|
||||
if (DomainModel::where('domain', $domain)->find()) {
|
||||
throw new ValidateException(__('Domain already exists'));
|
||||
}
|
||||
|
||||
$cloudflare = new CloudflareService();
|
||||
$zone = $cloudflare->createZone($domain);
|
||||
|
||||
$params['full_url'] = 'https://' . $domain;
|
||||
$params['zone_id'] = $zone['zone_id'];
|
||||
$params['nameservers'] = $zone['name_servers'];
|
||||
$params['zone_status'] = DomainModel::mapCloudflareZoneStatus($zone['status']);
|
||||
$params['ns_status'] = 'pending';
|
||||
$params['dns_status'] = 'pending';
|
||||
$params['check_result'] = '';
|
||||
|
||||
$result = $this->model->allowField(true)->save($params);
|
||||
Db::commit();
|
||||
} catch (ValidateException $e) {
|
||||
Db::rollback();
|
||||
$this->error($e->getMessage());
|
||||
} catch (PDOException|Exception $e) {
|
||||
Db::rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
if ($result === false) {
|
||||
$this->error(__('No rows were inserted'));
|
||||
}
|
||||
|
||||
$this->success(__('Domain submitted, please check NS in detail page and update at registrar'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 域名详情
|
||||
*/
|
||||
public function detail($ids = null)
|
||||
{
|
||||
$row = $this->model->get($ids);
|
||||
if (!$row) {
|
||||
$this->error(__('No Results were found'));
|
||||
}
|
||||
if (!$this->checkDataLimit($row)) {
|
||||
$this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
$data = $row->toArray();
|
||||
$data['nameservers_list'] = $row->getNameserversArray();
|
||||
$this->view->assign('row', $data);
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测域名状态
|
||||
*/
|
||||
public function detect($ids = null)
|
||||
{
|
||||
if (!$this->request->isPost()) {
|
||||
$this->error(__('Invalid parameters'));
|
||||
}
|
||||
|
||||
$row = $this->model->get($ids);
|
||||
if (!$row) {
|
||||
$this->error(__('No Results were found'));
|
||||
}
|
||||
if (!$this->checkDataLimit($row)) {
|
||||
$this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
try {
|
||||
(new DomainDetectRateLimitService())->assertCanDetect((int)$this->auth->id, (int)$row['id']);
|
||||
} catch (Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$cloudflare = new CloudflareService();
|
||||
$detect = $cloudflare->detectDomain($row->toArray());
|
||||
$row->save([
|
||||
'zone_status' => $detect['zone_status'],
|
||||
'ns_status' => $detect['ns_status'],
|
||||
'dns_status' => $detect['dns_status'],
|
||||
'check_time' => time(),
|
||||
'check_result' => $detect['check_result'],
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$this->success(__('Detection completed'), null, $row->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除(需输入完整域名二次确认)
|
||||
*/
|
||||
public function del($ids = '')
|
||||
{
|
||||
if (!$this->request->isPost()) {
|
||||
$this->error(__('Invalid parameters'));
|
||||
}
|
||||
|
||||
$ids = $ids ?: $this->request->post('ids', '');
|
||||
$confirmDomain = trim((string)$this->request->post('confirm_domain', ''));
|
||||
if ($ids === '' || $confirmDomain === '') {
|
||||
$this->error(__('Please enter the full domain to confirm deletion'));
|
||||
}
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$list = $this->model->where($pk, 'in', $ids)->select();
|
||||
if (!$list || count($list) === 0) {
|
||||
$this->error(__('No Results were found'));
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
Db::startTrans();
|
||||
try {
|
||||
foreach ($list as $item) {
|
||||
if (!$this->checkDataLimit($item)) {
|
||||
throw new Exception(__('You have no permission'));
|
||||
}
|
||||
if (DomainModel::normalizeDomain($confirmDomain) !== DomainModel::normalizeDomain((string)$item['domain'])) {
|
||||
throw new Exception(__('Domain confirmation does not match'));
|
||||
}
|
||||
|
||||
if (!empty($item['zone_id'])) {
|
||||
$cloudflare = new CloudflareService();
|
||||
$cloudflare->deleteZone((string)$item['zone_id']);
|
||||
}
|
||||
|
||||
$count += $item->delete();
|
||||
}
|
||||
Db::commit();
|
||||
} catch (PDOException|Exception $e) {
|
||||
Db::rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
if ($count > 0) {
|
||||
$this->success();
|
||||
}
|
||||
$this->error(__('No rows were deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DomainModel $row
|
||||
*/
|
||||
protected function checkDataLimit($row): bool
|
||||
{
|
||||
if (!$this->dataLimit) {
|
||||
return true;
|
||||
}
|
||||
$adminIds = $this->getDataLimitAdminIds();
|
||||
if (!is_array($adminIds)) {
|
||||
return true;
|
||||
}
|
||||
return in_array((int)$row[$this->dataLimitField], $adminIds, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use fast\Http;
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* Cloudflare API 服务层
|
||||
*/
|
||||
class CloudflareService
|
||||
{
|
||||
private const API_BASE = 'https://api.cloudflare.com/client/v4';
|
||||
|
||||
private string $apiToken;
|
||||
private string $accountId;
|
||||
private string $serverIp;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$config = config('cloudflare');
|
||||
$this->apiToken = (string)($config['api_token'] ?? '');
|
||||
$this->accountId = (string)($config['account_id'] ?? '');
|
||||
$this->serverIp = trim((string)($config['server_ip'] ?? ''));
|
||||
if ($this->apiToken === '' || $this->accountId === '') {
|
||||
throw new Exception('Cloudflare API 凭证未配置,请在 .env 中设置 cloudflare.api_token 与 cloudflare.account_id');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Cloudflare Zone
|
||||
*
|
||||
* @param string $domain 根域名
|
||||
* @return array{zone_id: string, name_servers: array, status: string}
|
||||
* @throws Exception
|
||||
*/
|
||||
public function createZone(string $domain): array
|
||||
{
|
||||
$body = [
|
||||
'name' => $domain,
|
||||
'account' => ['id' => $this->accountId],
|
||||
'type' => 'full',
|
||||
];
|
||||
$response = $this->request('POST', '/zones', $body);
|
||||
$result = $response['result'] ?? [];
|
||||
return [
|
||||
'zone_id' => (string)($result['id'] ?? ''),
|
||||
'name_servers' => (array)($result['name_servers'] ?? []),
|
||||
'status' => (string)($result['status'] ?? 'pending'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getZone(string $zoneId): array
|
||||
{
|
||||
$response = $this->request('GET', '/zones/' . $zoneId);
|
||||
return (array)($response['result'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function deleteZone(string $zoneId): bool
|
||||
{
|
||||
$this->request('DELETE', '/zones/' . $zoneId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function listDnsRecords(string $zoneId): array
|
||||
{
|
||||
$response = $this->request('GET', '/zones/' . $zoneId . '/dns_records', [], ['per_page' => 100]);
|
||||
return (array)($response['result'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚合检测三项状态
|
||||
* Zone/NS 检测通过后,自动将根域名 A 记录指向 server_ip 并校验
|
||||
*
|
||||
* @param array $row 域名记录
|
||||
* @return array{zone_status: string, ns_status: string, dns_status: string, check_result: string}
|
||||
*/
|
||||
public function detectDomain(array $row): array
|
||||
{
|
||||
$domain = strtolower(trim((string)($row['domain'] ?? '')));
|
||||
$zoneId = (string)($row['zone_id'] ?? '');
|
||||
$expectedNs = $this->decodeNameservers($row['nameservers'] ?? '');
|
||||
|
||||
$zoneStatus = 'failed';
|
||||
$nsStatus = 'pending';
|
||||
$dnsStatus = 'pending';
|
||||
$messages = [];
|
||||
|
||||
try {
|
||||
$zone = $this->getZone($zoneId);
|
||||
$cfStatus = (string)($zone['status'] ?? '');
|
||||
if ($cfStatus === 'active') {
|
||||
$zoneStatus = 'active';
|
||||
$messages[] = 'Zone:已激活';
|
||||
} elseif (in_array($cfStatus, ['pending', 'initializing'], true)) {
|
||||
$zoneStatus = 'pending';
|
||||
$messages[] = 'Zone:待激活';
|
||||
} else {
|
||||
$zoneStatus = 'failed';
|
||||
$messages[] = 'Zone:异常(' . $cfStatus . ')';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$zoneStatus = 'failed';
|
||||
$messages[] = 'Zone:检测失败(' . $e->getMessage() . ')';
|
||||
}
|
||||
|
||||
$currentNs = $this->getDomainNameservers($domain);
|
||||
if ($currentNs === []) {
|
||||
$nsStatus = 'pending';
|
||||
$messages[] = 'NS:待验证(未查询到NS记录)';
|
||||
} elseif ($this->compareNameservers($currentNs, $expectedNs)) {
|
||||
$nsStatus = 'verified';
|
||||
$messages[] = 'NS:已验证';
|
||||
} else {
|
||||
$nsStatus = 'pending';
|
||||
$messages[] = 'NS:待验证(当前NS与Cloudflare不一致)';
|
||||
}
|
||||
|
||||
if ($zoneStatus !== 'active') {
|
||||
$dnsStatus = 'pending';
|
||||
$messages[] = 'DNS:待创建(Zone未激活)';
|
||||
} elseif ($nsStatus !== 'verified') {
|
||||
$dnsStatus = 'pending';
|
||||
$messages[] = 'DNS:待创建(NS未验证)';
|
||||
} elseif ($this->serverIp === '') {
|
||||
$dnsStatus = 'failed';
|
||||
$messages[] = 'DNS:未配置server_ip';
|
||||
} elseif (!filter_var($this->serverIp, FILTER_VALIDATE_IP)) {
|
||||
$dnsStatus = 'failed';
|
||||
$messages[] = 'DNS:server_ip格式无效';
|
||||
} else {
|
||||
try {
|
||||
if ($this->hasRootCnameConflict($zoneId, $domain)) {
|
||||
$dnsStatus = 'failed';
|
||||
$messages[] = 'DNS:根域名存在CNAME记录,无法创建A记录';
|
||||
} else {
|
||||
$this->upsertRootARecord($zoneId, $domain, $this->serverIp);
|
||||
if ($this->verifyRootARecord($zoneId, $domain, $this->serverIp)) {
|
||||
$dnsStatus = 'created';
|
||||
$messages[] = 'DNS:已创建(A=' . $this->serverIp . ')';
|
||||
} else {
|
||||
$dnsStatus = 'pending';
|
||||
$messages[] = 'DNS:待创建(A记录未指向' . $this->serverIp . ')';
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$dnsStatus = 'failed';
|
||||
$messages[] = 'DNS:操作失败(' . $e->getMessage() . ')';
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'zone_status' => $zoneStatus,
|
||||
'ns_status' => $nsStatus,
|
||||
'dns_status' => $dnsStatus,
|
||||
'check_result' => implode('; ', $messages),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function listRootARecords(string $zoneId, string $domain): array
|
||||
{
|
||||
$response = $this->request('GET', '/zones/' . $zoneId . '/dns_records', [], [
|
||||
'type' => 'A',
|
||||
'name' => $domain,
|
||||
]);
|
||||
return (array)($response['result'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function hasRootCnameConflict(string $zoneId, string $domain): bool
|
||||
{
|
||||
$response = $this->request('GET', '/zones/' . $zoneId . '/dns_records', [], [
|
||||
'type' => 'CNAME',
|
||||
'name' => $domain,
|
||||
]);
|
||||
return count((array)($response['result'] ?? [])) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function upsertRootARecord(string $zoneId, string $domain, string $ip): void
|
||||
{
|
||||
$records = $this->listRootARecords($zoneId, $domain);
|
||||
if ($records === []) {
|
||||
$this->request('POST', '/zones/' . $zoneId . '/dns_records', [
|
||||
'type' => 'A',
|
||||
'name' => $domain,
|
||||
'content' => $ip,
|
||||
'ttl' => 1,
|
||||
'proxied' => false,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
$recordId = (string)($record['id'] ?? '');
|
||||
$content = (string)($record['content'] ?? '');
|
||||
if ($recordId === '' || $content === $ip) {
|
||||
continue;
|
||||
}
|
||||
$this->request('PATCH', '/zones/' . $zoneId . '/dns_records/' . $recordId, [
|
||||
'content' => $ip,
|
||||
'ttl' => 1,
|
||||
'proxied' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function verifyRootARecord(string $zoneId, string $domain, string $ip): bool
|
||||
{
|
||||
$records = $this->listRootARecords($zoneId, $domain);
|
||||
foreach ($records as $record) {
|
||||
if ((string)($record['content'] ?? '') === $ip) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function request(string $method, string $path, array $body = [], array $query = []): array
|
||||
{
|
||||
$url = self::API_BASE . $path;
|
||||
if ($query !== []) {
|
||||
$url .= '?' . http_build_query($query);
|
||||
}
|
||||
|
||||
$options = [
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $this->apiToken,
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
];
|
||||
|
||||
$payload = $body !== [] ? json_encode($body, JSON_UNESCAPED_UNICODE) : '';
|
||||
|
||||
if (strtoupper($method) === 'GET') {
|
||||
$result = Http::sendRequest($url, [], 'GET', $options);
|
||||
} elseif (strtoupper($method) === 'DELETE') {
|
||||
$result = Http::sendRequest($url, $payload, 'DELETE', $options);
|
||||
} else {
|
||||
$result = Http::sendRequest($url, $payload, strtoupper($method), $options);
|
||||
}
|
||||
|
||||
if (!$result['ret']) {
|
||||
throw new Exception('Cloudflare API 请求失败: ' . ($result['msg'] ?? '未知错误'));
|
||||
}
|
||||
|
||||
$decoded = json_decode((string)$result['msg'], true);
|
||||
if (!is_array($decoded)) {
|
||||
throw new Exception('Cloudflare API 响应解析失败');
|
||||
}
|
||||
|
||||
if (empty($decoded['success'])) {
|
||||
$errors = $decoded['errors'] ?? [];
|
||||
$message = 'Cloudflare API 错误';
|
||||
if (is_array($errors) && isset($errors[0]['message'])) {
|
||||
$message = (string)$errors[0]['message'];
|
||||
}
|
||||
throw new Exception($message);
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
private function decodeNameservers($nameservers): array
|
||||
{
|
||||
if (is_array($nameservers)) {
|
||||
return $nameservers;
|
||||
}
|
||||
if (is_string($nameservers) && $nameservers !== '') {
|
||||
$decoded = json_decode($nameservers, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private function getDomainNameservers(string $domain): array
|
||||
{
|
||||
$records = @dns_get_record($domain, DNS_NS);
|
||||
if (!is_array($records)) {
|
||||
return [];
|
||||
}
|
||||
$list = [];
|
||||
foreach ($records as $record) {
|
||||
if (!empty($record['target'])) {
|
||||
$list[] = $this->normalizeNs((string)$record['target']);
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($list));
|
||||
}
|
||||
|
||||
private function compareNameservers(array $current, array $expected): bool
|
||||
{
|
||||
if ($expected === []) {
|
||||
return false;
|
||||
}
|
||||
$currentNorm = array_map([$this, 'normalizeNs'], $current);
|
||||
$expectedNorm = array_map([$this, 'normalizeNs'], $expected);
|
||||
sort($currentNorm);
|
||||
sort($expectedNorm);
|
||||
return $currentNorm === $expectedNorm;
|
||||
}
|
||||
|
||||
private function normalizeNs(string $ns): string
|
||||
{
|
||||
return rtrim(strtolower(trim($ns)), '.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use think\Cache;
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* 域名检测频率限制
|
||||
* 每分钟最多 2 次,超限后冷却 120 秒
|
||||
*/
|
||||
class DomainDetectRateLimitService
|
||||
{
|
||||
/** 统计窗口(秒) */
|
||||
private const WINDOW_SECONDS = 60;
|
||||
|
||||
/** 窗口内最大检测次数 */
|
||||
private const MAX_ATTEMPTS = 2;
|
||||
|
||||
/** 超限后冷却时间(秒) */
|
||||
private const COOLDOWN_SECONDS = 120;
|
||||
|
||||
/**
|
||||
* 校验是否允许检测,不允许时抛出异常
|
||||
*
|
||||
* @param int $adminId 管理员 ID
|
||||
* @param int $domainId 域名记录 ID
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertCanDetect(int $adminId, int $domainId): void
|
||||
{
|
||||
$now = time();
|
||||
$baseKey = 'domain_detect:' . $adminId . ':' . $domainId;
|
||||
$cooldownKey = $baseKey . ':cooldown';
|
||||
$attemptsKey = $baseKey . ':attempts';
|
||||
|
||||
$cooldownUntil = (int)Cache::get($cooldownKey, 0);
|
||||
if ($cooldownUntil > $now) {
|
||||
$wait = $cooldownUntil - $now;
|
||||
throw new Exception(sprintf('检测过于频繁,请 %d 秒后再试', $wait));
|
||||
}
|
||||
|
||||
$timestamps = Cache::get($attemptsKey);
|
||||
if (!is_array($timestamps)) {
|
||||
$timestamps = [];
|
||||
}
|
||||
|
||||
$timestamps = array_values(array_filter($timestamps, static function ($ts) use ($now): bool {
|
||||
return is_int($ts) && ($now - $ts) < self::WINDOW_SECONDS;
|
||||
}));
|
||||
|
||||
if (count($timestamps) >= self::MAX_ATTEMPTS) {
|
||||
Cache::set($cooldownKey, $now + self::COOLDOWN_SECONDS, self::COOLDOWN_SECONDS);
|
||||
Cache::set($attemptsKey, $timestamps, self::WINDOW_SECONDS + self::COOLDOWN_SECONDS);
|
||||
throw new Exception(sprintf(
|
||||
'一分钟最多检测 %d 次,请 %d 秒后再试',
|
||||
self::MAX_ATTEMPTS,
|
||||
self::COOLDOWN_SECONDS
|
||||
));
|
||||
}
|
||||
|
||||
$timestamps[] = $now;
|
||||
Cache::set($attemptsKey, $timestamps, self::WINDOW_SECONDS + self::COOLDOWN_SECONDS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
use think\Env;
|
||||
|
||||
/**
|
||||
* Cloudflare 配置,优先读取 .env
|
||||
*/
|
||||
return [
|
||||
'api_token' => Env::get('cloudflare.api_token', ''),
|
||||
'account_id' => Env::get('cloudflare.account_id', ''),
|
||||
'server_ip' => Env::get('cloudflare.server_ip', ''),
|
||||
];
|
||||
Reference in New Issue
Block a user