域名管理
This commit is contained in:
Executable
+26
@@ -0,0 +1,26 @@
|
||||
-- 域名管理模块表结构
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
DROP TABLE IF EXISTS `fa_domain`;
|
||||
CREATE TABLE `fa_domain` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID',
|
||||
`domain` varchar(255) NOT NULL DEFAULT '' COMMENT '根域名',
|
||||
`full_url` varchar(512) NOT NULL DEFAULT '' COMMENT '完整链接',
|
||||
`zone_id` varchar(64) NOT NULL DEFAULT '' COMMENT 'Cloudflare Zone ID',
|
||||
`nameservers` text COMMENT 'CF分配的NS(JSON)',
|
||||
`zone_status` enum('pending','active','failed') NOT NULL DEFAULT 'pending' COMMENT 'Zone状态',
|
||||
`ns_status` enum('pending','verified','failed') NOT NULL DEFAULT 'pending' COMMENT 'NS状态',
|
||||
`dns_status` enum('pending','created','failed') NOT NULL DEFAULT 'pending' COMMENT 'DNS状态',
|
||||
`check_time` bigint(16) DEFAULT NULL COMMENT '检测时间',
|
||||
`check_result` varchar(2000) DEFAULT '' COMMENT '最近检测结果',
|
||||
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `domain` (`domain`),
|
||||
KEY `admin_id` (`admin_id`),
|
||||
KEY `zone_status` (`zone_status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='域名管理表';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
Regular → Executable
Executable
+247
@@ -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);
|
||||
}
|
||||
}
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'Domain' => '域名',
|
||||
'Full_url' => '完整链接',
|
||||
'Zone_status' => 'Zone状态',
|
||||
'Ns_status' => 'NameServer状态',
|
||||
'Dns_status' => 'DNS状态',
|
||||
'Nameservers' => 'NameServer',
|
||||
'Check_time' => '检测时间',
|
||||
'Check_result' => '最近检测结果',
|
||||
'Createtime' => '创建时间',
|
||||
'Zone pending' => '待激活',
|
||||
'Zone active' => '已激活',
|
||||
'Zone failed' => '异常',
|
||||
'NS pending' => '待验证',
|
||||
'NS verified' => '已验证',
|
||||
'NS failed' => '验证失败',
|
||||
'DNS pending' => '待创建',
|
||||
'DNS created' => '已创建',
|
||||
'DNS failed' => '异常',
|
||||
'Detect' => '检测',
|
||||
'Detection completed' => '检测完成',
|
||||
'Domain input tips title' => '填写说明',
|
||||
'Domain input tips line1' => '请输入根域名,比如:nihao.com,不要填写 www,且不支持二级子域名。',
|
||||
'Domain input tips line2' => '不支持中国域名注册商注册的域名,比如阿里云、腾讯云等注册商;也不支持中国域名后缀,包括 cn、com.cn、net.cn、org.cn 等。',
|
||||
'Domain guide title' => '接入指引',
|
||||
'Domain guide content' => '提交后,请在详情中查看 Cloudflare 实际分配的 NS,并到域名注册商处修改。',
|
||||
'Domain submitted, please check NS in detail page and update at registrar' => '域名已提交,请在详情中查看 Cloudflare 分配的 NS,并到注册商处修改',
|
||||
'Domain cannot be edited after creation' => '域名创建后不可编辑',
|
||||
'Domain already exists' => '该域名已存在',
|
||||
'Please enter the full domain to confirm deletion' => '请输入完整域名以确认删除',
|
||||
'Domain confirmation does not match' => '输入的域名与记录不一致,删除已取消',
|
||||
'Delete domain confirm prompt' => '请输入完整域名以确认删除',
|
||||
'Delete domain confirm mismatch' => '域名输入不一致,请重新确认',
|
||||
];
|
||||
Executable
+189
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 域名管理模型
|
||||
*/
|
||||
class Domain extends Model
|
||||
{
|
||||
/** @var string 中国域名后缀黑名单 */
|
||||
public const CHINA_SUFFIX_BLACKLIST = [
|
||||
'cn', 'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn', 'ac.cn', 'mil.cn',
|
||||
];
|
||||
|
||||
protected $name = 'domain';
|
||||
protected $autoWriteTimestamp = 'integer';
|
||||
protected $createTime = 'createtime';
|
||||
protected $updateTime = 'updatetime';
|
||||
protected $deleteTime = false;
|
||||
|
||||
protected $append = [
|
||||
'zone_status_text',
|
||||
'ns_status_text',
|
||||
'dns_status_text',
|
||||
];
|
||||
|
||||
public function getZoneStatusList(): array
|
||||
{
|
||||
return [
|
||||
'pending' => __('Zone pending'),
|
||||
'active' => __('Zone active'),
|
||||
'failed' => __('Zone failed'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getNsStatusList(): array
|
||||
{
|
||||
return [
|
||||
'pending' => __('NS pending'),
|
||||
'verified' => __('NS verified'),
|
||||
'failed' => __('NS failed'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getDnsStatusList(): array
|
||||
{
|
||||
return [
|
||||
'pending' => __('DNS pending'),
|
||||
'created' => __('DNS created'),
|
||||
'failed' => __('DNS failed'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getZoneStatusTextAttr($value, $data): string
|
||||
{
|
||||
$value = $value ?: ($data['zone_status'] ?? '');
|
||||
$list = $this->getZoneStatusList();
|
||||
return $list[$value] ?? '';
|
||||
}
|
||||
|
||||
public function getNsStatusTextAttr($value, $data): string
|
||||
{
|
||||
$value = $value ?: ($data['ns_status'] ?? '');
|
||||
$list = $this->getNsStatusList();
|
||||
return $list[$value] ?? '';
|
||||
}
|
||||
|
||||
public function getDnsStatusTextAttr($value, $data): string
|
||||
{
|
||||
$value = $value ?: ($data['dns_status'] ?? '');
|
||||
$list = $this->getDnsStatusList();
|
||||
return $list[$value] ?? '';
|
||||
}
|
||||
|
||||
public function setNameserversAttr($value): string
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return json_encode(array_values($value), JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getNameserversArray(): array
|
||||
{
|
||||
$value = $this->getAttr('nameservers');
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_string($value) && $value !== '') {
|
||||
$decoded = json_decode($value, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function normalizeDomain(string $domain): string
|
||||
{
|
||||
return strtolower(trim($domain));
|
||||
}
|
||||
|
||||
public static function isValidRootDomain(string $domain): bool
|
||||
{
|
||||
$domain = self::normalizeDomain($domain);
|
||||
if ($domain === '' || strlen($domain) > 253) {
|
||||
return false;
|
||||
}
|
||||
if (strpos($domain, 'www.') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (!preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i', $domain)) {
|
||||
return false;
|
||||
}
|
||||
if (self::isChinaSuffix($domain)) {
|
||||
return false;
|
||||
}
|
||||
$label = self::getDomainLabelWithoutSuffix($domain);
|
||||
if ($label === '' || strpos($label, '.') !== false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function isChinaSuffix(string $domain): bool
|
||||
{
|
||||
$domain = self::normalizeDomain($domain);
|
||||
foreach (self::CHINA_SUFFIX_BLACKLIST as $suffix) {
|
||||
if ($domain === $suffix || self::endsWith($domain, '.' . $suffix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getMatchedSuffix(string $domain): string
|
||||
{
|
||||
$domain = self::normalizeDomain($domain);
|
||||
$suffixes = array_merge(self::CHINA_SUFFIX_BLACKLIST, ['co.uk', 'com', 'net', 'org', 'io', 'co', 'me', 'info', 'biz']);
|
||||
usort($suffixes, function ($a, $b) {
|
||||
return strlen($b) - strlen($a);
|
||||
});
|
||||
foreach ($suffixes as $suffix) {
|
||||
if ($domain === $suffix || self::endsWith($domain, '.' . $suffix)) {
|
||||
return $suffix;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public static function getDomainLabelWithoutSuffix(string $domain): string
|
||||
{
|
||||
$domain = self::normalizeDomain($domain);
|
||||
$suffix = self::getMatchedSuffix($domain);
|
||||
if ($suffix === '') {
|
||||
$pos = strrpos($domain, '.');
|
||||
return $pos === false ? $domain : substr($domain, 0, $pos);
|
||||
}
|
||||
$suffixWithDot = '.' . $suffix;
|
||||
if (self::endsWith($domain, $suffixWithDot)) {
|
||||
return substr($domain, 0, -strlen($suffixWithDot));
|
||||
}
|
||||
return $domain;
|
||||
}
|
||||
|
||||
public static function mapCloudflareZoneStatus(string $status): string
|
||||
{
|
||||
if ($status === 'active') {
|
||||
return 'active';
|
||||
}
|
||||
if (in_array($status, ['pending', 'initializing'], true)) {
|
||||
return 'pending';
|
||||
}
|
||||
return 'failed';
|
||||
}
|
||||
|
||||
private static function endsWith(string $haystack, string $needle): bool
|
||||
{
|
||||
if ($needle === '') {
|
||||
return true;
|
||||
}
|
||||
return substr($haystack, -strlen($needle)) === $needle;
|
||||
}
|
||||
}
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\validate;
|
||||
|
||||
use app\admin\model\Domain as DomainModel;
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* 域名验证器
|
||||
*/
|
||||
class Domain extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'domain' => 'require|checkDomain',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'domain.require' => '请输入根域名',
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['domain'],
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return bool|string
|
||||
*/
|
||||
protected function checkDomain($value)
|
||||
{
|
||||
$domain = DomainModel::normalizeDomain((string)$value);
|
||||
if (!DomainModel::isValidRootDomain($domain)) {
|
||||
return '请输入合法根域名,不要填写 www 或二级子域名,且不支持中国域名后缀';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||
{:token()}
|
||||
|
||||
<div class="alert alert-warning-light">
|
||||
<p><strong>{:__('Domain input tips title')}</strong></p>
|
||||
<p>{:__('Domain input tips line1')}</p>
|
||||
<p>{:__('Domain input tips line2')}</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info-light">
|
||||
<p><strong>{:__('Domain guide title')}</strong></p>
|
||||
<p>{:__('Domain guide content')}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="c-domain" class="control-label col-xs-12 col-sm-2">{:__('Domain')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-domain" data-rule="required" class="form-control" name="row[domain]" type="text" value="" placeholder="nihao.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group layer-footer">
|
||||
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||
<button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
<style>
|
||||
.table-domain-detail tr td { word-break: break-all; }
|
||||
.domain-ns-list { margin: 0; padding-left: 18px; }
|
||||
.domain-ns-list li { margin-bottom: 6px; font-family: Menlo, Monaco, Consolas, monospace; }
|
||||
</style>
|
||||
<table class="table table-striped table-domain-detail">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="140">{:__('Domain')}</td>
|
||||
<td>{$row.domain|htmlentities}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{:__('Full_url')}</td>
|
||||
<td><a href="{$row.full_url|htmlentities}" target="_blank" rel="noopener noreferrer">{$row.full_url|htmlentities}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{:__('Zone_status')}</td>
|
||||
<td>{$row.zone_status_text|htmlentities}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{:__('Ns_status')}</td>
|
||||
<td>{$row.ns_status_text|htmlentities}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{:__('Dns_status')}</td>
|
||||
<td>{$row.dns_status_text|htmlentities}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{:__('Nameservers')}</td>
|
||||
<td>
|
||||
{if !empty($row.nameservers_list)}
|
||||
<ul class="domain-ns-list">
|
||||
{volist name="row.nameservers_list" id="ns"}
|
||||
<li>{$ns|htmlentities}</li>
|
||||
{/volist}
|
||||
</ul>
|
||||
<p class="text-muted">{:__('Domain guide content')}</p>
|
||||
{else/}
|
||||
<span class="text-muted">{:__('None')}</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{:__('Check_time')}</td>
|
||||
<td>{if $row.check_time}{$row.check_time|datetime}{else/}{:__('None')}{/if}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{:__('Check_result')}</td>
|
||||
<td>{$row.check_result|default=''|htmlentities}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{:__('Createtime')}</td>
|
||||
<td>{$row.createtime|datetime}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="hide layer-footer">
|
||||
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<button type="reset" class="btn btn-primary btn-embossed btn-close" onclick="Layer.closeAll();">{:__('Close')}</button>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
<div class="panel panel-default panel-intro">
|
||||
{:build_heading()}
|
||||
|
||||
<div class="panel-body">
|
||||
<div id="myTabContent" class="tab-content">
|
||||
<div class="tab-pane fade active in" id="one">
|
||||
<div class="widget-body no-padding">
|
||||
<div id="toolbar" class="toolbar">
|
||||
{:build_toolbar('refresh,add')}
|
||||
</div>
|
||||
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
|
||||
data-operate-edit="false"
|
||||
data-operate-del="false"
|
||||
width="100%">
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+331
@@ -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);
|
||||
}
|
||||
}
|
||||
Executable
+12
@@ -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', ''),
|
||||
];
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
return array (
|
||||
'name' => 'LINK S 多功能打粉平台',
|
||||
'name' => 'LINK-S 多功能打粉',
|
||||
'beian' => '',
|
||||
'cdnurl' => '',
|
||||
'version' => '1.0.1',
|
||||
@@ -28,12 +28,6 @@ return array (
|
||||
'user' => 'User',
|
||||
'example' => 'Example',
|
||||
),
|
||||
'attachmentcategory' =>
|
||||
array (
|
||||
'category1' => 'Category1',
|
||||
'category2' => 'Category2',
|
||||
'custom' => 'Custom',
|
||||
),
|
||||
'mail_type' => '1',
|
||||
'mail_smtp_host' => 'smtp.qq.com',
|
||||
'mail_smtp_port' => '465',
|
||||
@@ -41,4 +35,10 @@ return array (
|
||||
'mail_smtp_pass' => '',
|
||||
'mail_verify_type' => '2',
|
||||
'mail_from' => '',
|
||||
'attachmentcategory' =>
|
||||
array (
|
||||
'category1' => 'Category1',
|
||||
'category2' => 'Category2',
|
||||
'custom' => 'Custom',
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# 部署域名检测频率限制(需 root)
|
||||
set -e
|
||||
BASE="$(cd "$(dirname "$0")" && pwd)"
|
||||
PATCH="$BASE/patches"
|
||||
|
||||
cp -f "$PATCH/application/common/service/DomainDetectRateLimitService.php" "$BASE/application/common/service/"
|
||||
cp -f "$PATCH/application/admin/controller/Domain.php" "$BASE/application/admin/controller/"
|
||||
chown www:www \
|
||||
"$BASE/application/common/service/DomainDetectRateLimitService.php" \
|
||||
"$BASE/application/admin/controller/Domain.php"
|
||||
php -l "$BASE/application/common/service/DomainDetectRateLimitService.php"
|
||||
php -l "$BASE/application/admin/controller/Domain.php"
|
||||
echo "检测频率限制部署完成"
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
# 部署 server_ip DNS 自动配置逻辑(需 root)
|
||||
set -e
|
||||
BASE="$(cd "$(dirname "$0")" && pwd)"
|
||||
PATCH="$BASE/patches"
|
||||
|
||||
cp -f "$PATCH/application/extra/cloudflare.php" "$BASE/application/extra/cloudflare.php"
|
||||
cp -f "$PATCH/application/common/service/CloudflareService.php" "$BASE/application/common/service/CloudflareService.php"
|
||||
chown www:www "$BASE/application/extra/cloudflare.php" "$BASE/application/common/service/CloudflareService.php"
|
||||
php -l "$BASE/application/common/service/CloudflareService.php"
|
||||
echo "部署完成。请确认 .env 中 cloudflare.server_ip 已填写。"
|
||||
@@ -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', ''),
|
||||
];
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||
|
||||
var Controller = {
|
||||
index: function () {
|
||||
Table.api.init({
|
||||
extend: {
|
||||
index_url: 'domain/index' + location.search,
|
||||
add_url: 'domain/add',
|
||||
edit_url: '',
|
||||
del_url: '',
|
||||
multi_url: '',
|
||||
table: 'domain',
|
||||
}
|
||||
});
|
||||
|
||||
var table = $("#table");
|
||||
|
||||
table.bootstrapTable({
|
||||
url: $.fn.bootstrapTable.defaults.extend.index_url,
|
||||
pk: 'id',
|
||||
sortName: 'id',
|
||||
sortOrder: 'desc',
|
||||
fixedColumns: true,
|
||||
fixedRightNumber: 1,
|
||||
columns: [
|
||||
[
|
||||
{checkbox: true},
|
||||
{field: 'domain', title: __('Domain'), operate: 'LIKE'},
|
||||
{field: 'full_url', title: __('Full_url'), operate: false, formatter: Table.api.formatter.url},
|
||||
{field: 'zone_status', title: __('Zone_status'), searchList: Config.zoneStatusList, formatter: Table.api.formatter.status},
|
||||
{field: 'ns_status', title: __('Ns_status'), searchList: Config.nsStatusList, formatter: Table.api.formatter.status},
|
||||
{field: 'dns_status', title: __('Dns_status'), searchList: Config.dnsStatusList, formatter: Table.api.formatter.status},
|
||||
{field: 'check_time', title: __('Check_time'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime, sortable: true},
|
||||
{field: 'check_result', title: __('Check_result'), operate: 'LIKE', class: 'autocontent', formatter: Table.api.formatter.content},
|
||||
{field: 'createtime', title: __('Createtime'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime, sortable: true},
|
||||
{
|
||||
field: 'operate',
|
||||
title: __('Operate'),
|
||||
table: table,
|
||||
events: Table.api.events.operate,
|
||||
buttons: [
|
||||
{
|
||||
name: 'detail',
|
||||
text: __('Detail'),
|
||||
icon: 'fa fa-list',
|
||||
classname: 'btn btn-info btn-xs btn-detail btn-dialog',
|
||||
url: 'domain/detail'
|
||||
},
|
||||
{
|
||||
name: 'detect',
|
||||
text: __('Detect'),
|
||||
icon: 'fa fa-refresh',
|
||||
classname: 'btn btn-success btn-xs btn-detect',
|
||||
url: 'domain/detect',
|
||||
refresh: true
|
||||
},
|
||||
{
|
||||
name: 'delconfirm',
|
||||
text: __('Del'),
|
||||
icon: 'fa fa-trash',
|
||||
classname: 'btn btn-danger btn-xs btn-delconfirm'
|
||||
}
|
||||
],
|
||||
formatter: Table.api.formatter.operate
|
||||
}
|
||||
]
|
||||
]
|
||||
});
|
||||
|
||||
table.on('click', '.btn-detect', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var that = this;
|
||||
var rowIndex = $(that).data('row-index');
|
||||
var row = Table.api.getrowbyindex(table, rowIndex);
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
$(that).addClass('disabled');
|
||||
Fast.api.ajax({
|
||||
url: 'domain/detect/ids/' + row.id,
|
||||
type: 'post'
|
||||
}, function () {
|
||||
table.bootstrapTable('refresh');
|
||||
return false;
|
||||
}, function () {
|
||||
$(that).removeClass('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
table.on('click', '.btn-delconfirm', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
var that = this;
|
||||
var rowIndex = $(that).data('row-index');
|
||||
var row = Table.api.getrowbyindex(table, rowIndex);
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
Layer.prompt({
|
||||
title: __('Delete domain confirm prompt'),
|
||||
formType: 0,
|
||||
value: ''
|
||||
}, function (value, index) {
|
||||
if ($.trim(value) !== row.domain) {
|
||||
Toastr.error(__('Delete domain confirm mismatch'));
|
||||
return false;
|
||||
}
|
||||
Layer.close(index);
|
||||
Fast.api.ajax({
|
||||
url: 'domain/del',
|
||||
data: {ids: row.id, confirm_domain: $.trim(value)}
|
||||
}, function () {
|
||||
table.bootstrapTable('refresh');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Table.api.bindevent(table);
|
||||
},
|
||||
add: function () {
|
||||
Controller.api.bindevent();
|
||||
},
|
||||
api: {
|
||||
bindevent: function () {
|
||||
Form.api.bindevent($("form[role=form]"));
|
||||
}
|
||||
}
|
||||
};
|
||||
return Controller;
|
||||
});
|
||||
Reference in New Issue
Block a user