Compare commits

..

21 Commits
2.8.0 ... 2.8.2

Author SHA1 Message Date
net909
0863d02cc9 fix 2025-07-28 20:37:10 +08:00
net909
9032ea0405 整合计划任务,新增访问URL方式的计划任务 2025-07-28 20:30:09 +08:00
net909
e3749ecb6c Merge branch 'main' of ssh://ssh.github.com:443/netcccyun/dnsmgr 2025-07-18 14:47:30 +08:00
net909
e1e90c3c71 修复阿里云ESA部署失败 2025-07-18 14:47:12 +08:00
dependabot[bot]
a171a5b9b0 Bump topthink/think-trace from 1.6 to 2.0 (#276)
---
updated-dependencies:
- dependency-name: topthink/think-trace
  dependency-version: '2.0'
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-16 17:08:08 +08:00
dependabot[bot]
f608b2fceb Bump topthink/framework from 8.1.2 to 8.1.3 (#277)
---
updated-dependencies:
- dependency-name: topthink/framework
  dependency-version: 8.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-16 17:08:01 +08:00
Hanada
0837ac9be1 Merge pull request #274 from HanadaLee/multipartpostfix
重构POST内容判断逻辑,去除X-Content-Type标头
2025-07-12 09:44:14 +08:00
net909
c31e0eaf41 修复部署失败 2025-07-11 09:56:08 +08:00
net909
987deda95d 修复报错 2025-07-10 16:41:11 +08:00
消失的彩虹海
654151ce5b Merge pull request #270 from HanadaLee/ssldeployfix
修复部署证书到阿里云和群晖失败的问题
2025-07-08 23:25:03 +08:00
Hanada
2fedee1e93 修复使用guzzle库后部署证书到群晖失败的问题 2025-07-08 23:12:40 +08:00
Hanada
ba97ac3685 修复部署阿里云时证书序列号可能存在前置0的问题 2025-07-08 21:42:40 +08:00
net909
f2f1a0d01e 修复upyun部署 2025-07-07 21:47:15 +08:00
net909
933f98a909 2.8.1 2025-07-06 12:21:04 +08:00
net909
3beb0ec637 修复阿里云WAF部署 2025-07-06 12:19:02 +08:00
net909
7a92ad7094 Merge branch 'main' of ssh://ssh.github.com:443/netcccyun/dnsmgr 2025-07-06 11:17:06 +08:00
net909
5dc4dbfabd 增加Edgeone IP优选,修复EO海外版部署失败 2025-07-06 11:16:44 +08:00
DearTanker
291f715e91 优化记录值复制功能 (#266)
1、修复无法复制包含双引号的 TXT 记录值
2、调整 MX 记录值的复制按钮到右侧,因为复制不需要复制优先级。
2025-07-04 09:43:55 +08:00
net909
4f15d4d7a2 调整快速打开按钮位置 2025-07-03 19:48:34 +08:00
DearTanker
11e83860b7 添加 自定义样式功能、主机记录增加快速打开链接 (#263)
* 添加支持自定义样式功能。

* 在主机记录列中增加一个新窗口打开链接的小按钮,方便快速访问。

* 记录值增加快速复制按钮,并用 padding 来调整了图标的间距避免空格
2025-07-03 19:37:34 +08:00
net909
479af4fe5f 修复批量操作证书和部署任务 2025-06-30 12:44:31 +08:00
34 changed files with 455 additions and 149 deletions

View File

@@ -12,7 +12,9 @@ use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Config;
use app\service\OptimizeService;
use app\service\CertTaskService;
use app\service\ExpireNoticeService;
class Certtask extends Command
{
@@ -20,7 +22,7 @@ class Certtask extends Command
{
// 指令配置
$this->setName('certtask')
->setDescription('证书申请与部署任务');
->setDescription('SSL证书续签与部署、域名到期提醒、CF优选IP更新');
}
protected function execute(Input $input, Output $output)
@@ -28,6 +30,11 @@ class Certtask extends Command
$res = Db::name('config')->cache('configs', 0)->column('value', 'key');
Config::set($res, 'sys');
(new CertTaskService())->execute();
$res = (new OptimizeService())->execute();
if (!$res) {
(new CertTaskService())->execute();
(new ExpireNoticeService())->task();
}
echo 'done'.PHP_EOL;
}
}

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace app\command;
use Exception;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Config;
use app\service\OptimizeService;
class Opiptask extends Command
{
protected function configure()
{
// 指令配置
$this->setName('opiptask')
->setDescription('CF优选IP任务');
}
protected function execute(Input $input, Output $output)
{
$res = Db::name('config')->cache('configs', 0)->column('value', 'key');
Config::set($res, 'sys');
(new OptimizeService())->execute();
}
}

View File

@@ -457,6 +457,10 @@ function http_request($url, $data = null, $referer = null, $cookie = null, $head
if ($options['headers']['Content-Type'] == 'application/x-www-form-urlencoded') {
// 表单
$options['form_params'] = $data;
} else if ($options['headers']['Content-Type'] == 'multipart/form-data') {
// 表单文件
$options['multipart'] = $data;
unset($options['headers']['Content-Type']); // 由GuzzleHttp重新生成Content-Type头部
} else if ($options['headers']['Content-Type'] == 'application/json') {
// json
$options['json'] = $data;

View File

@@ -458,7 +458,7 @@ class Cert extends BaseController
$ids = input('post.ids');
$success = 0;
foreach ($ids as $id) {
if (input('post.action') == 'delete') {
if (input('post.act') == 'delete') {
$dcount = DB::name('cert_deploy')->where('oid', $id)->count();
if ($dcount > 0) continue;
try {
@@ -468,7 +468,7 @@ class Cert extends BaseController
Db::name('cert_order')->where('id', $id)->delete();
Db::name('cert_domain')->where('oid', $id)->delete();
$success++;
} elseif (input('post.action') == 'reset') {
} elseif (input('post.act') == 'reset') {
try {
$service = new CertOrderService($id);
$service->cancel();
@@ -476,8 +476,8 @@ class Cert extends BaseController
$success++;
} catch (Exception $e) {
}
} elseif (input('post.action') == 'open' || input('post.action') == 'close') {
$isauto = input('post.action') == 'open' ? 1 : 0;
} elseif (input('post.act') == 'open' || input('post.act') == 'close') {
$isauto = input('post.act') == 'open' ? 1 : 0;
Db::name('cert_order')->where('id', $id)->update(['isauto' => $isauto]);
$success++;
}
@@ -754,21 +754,21 @@ class Cert extends BaseController
if (!$cert) return json(['code' => -1, 'msg' => '证书订单不存在']);
}
foreach ($ids as $id) {
if (input('post.action') == 'delete') {
if (input('post.act') == 'delete') {
Db::name('cert_deploy')->where('id', $id)->delete();
$success++;
} elseif (input('post.action') == 'reset') {
} elseif (input('post.act') == 'reset') {
try {
$service = new CertDeployService($id);
$service->reset();
$success++;
} catch (Exception $e) {
}
} elseif (input('post.action') == 'open' || input('post.action') == 'close') {
$active = input('post.action') == 'open' ? 1 : 0;
} elseif (input('post.act') == 'open' || input('post.act') == 'close') {
$active = input('post.act') == 'open' ? 1 : 0;
Db::name('cert_deploy')->where('id', $id)->update(['active' => $active]);
$success++;
} elseif (input('post.action') == 'cert') {
} elseif (input('post.act') == 'cert') {
Db::name('cert_deploy')->where('id', $id)->update(['oid' => $certid]);
$success++;
}

View File

@@ -20,6 +20,9 @@ class Optimizeip extends BaseController
if (empty($key)) {
continue;
}
if ($key == 'optimize_ip_min' && intval($value) < 10) {
return json(['code' => -1, 'msg' => '自动更新时间间隔不能小于10分钟']);
}
config_set($key, $value);
Cache::delete('configs');
}

View File

@@ -7,6 +7,9 @@ use Exception;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
use app\service\OptimizeService;
use app\service\CertTaskService;
use app\service\ExpireNoticeService;
class System extends BaseController
{
@@ -107,4 +110,38 @@ class System extends BaseController
}
return json(['code' => 0]);
}
public function cronset()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
if (config_get('cron_key') === null) {
config_set('cron_key', random(10));
Cache::delete('configs');
}
View::assign('is_user_www', isset($_SERVER['USER']) && $_SERVER['USER'] == 'www');
View::assign('siteurl', request()->root(true));
return View::fetch();
}
public function cron()
{
if (function_exists("set_time_limit")) {
@set_time_limit(0);
}
if (function_exists("ignore_user_abort")) {
@ignore_user_abort(true);
}
if (isset($_SERVER['HTTP_USER_AGENT']) && str_contains($_SERVER['HTTP_USER_AGENT'], 'Baiduspider')) exit;
$key = input('get.key', '');
$cron_key = config_get('cron_key');
if (config_get('cron_type', '0') != '1' || empty($cron_key)) exit('未开启当前方式');
if ($key != $cron_key) exit('访问密钥错误');
$res = (new OptimizeService())->execute();
if (!$res) {
(new CertTaskService())->execute();
(new ExpireNoticeService())->task();
}
echo 'success!';
}
}

View File

@@ -998,6 +998,24 @@ class DeployHelper
'show' => 'product==\'lighthouse\'||product==\'ddos\'',
'required' => true,
],
'site_type' => [
'name' => '站点类型',
'type' => 'select',
'options' => [
['value'=>'cn', 'label'=>'国内站'],
['value'=>'intl', 'label'=>'国际站'],
],
'value' => 'cn',
'show' => 'product==\'teo\'',
'required' => true,
],
'site_id' => [
'name' => '站点ID',
'type' => 'input',
'placeholder' => '类似于zone-xxxx在站点列表或概览页面查看',
'show' => 'product==\'teo\'',
'required' => true,
],
'domain' => [
'name' => '绑定的域名',
'type' => 'input',
@@ -1866,6 +1884,12 @@ class DeployHelper
'show' => 'format==\'pfx\'',
'required' => true,
],
'cmd_pre' => [
'name' => '上传前执行命令',
'type' => 'textarea',
'show' => 'format==\'pem\'||uptype==0',
'placeholder' => '可留空,上传前执行脚本命令',
],
'cmd' => [
'name' => '上传完执行命令',
'type' => 'textarea',

View File

@@ -101,7 +101,7 @@ class aliyun implements DeployInterface
$cert_id = null;
if ($data['TotalCount'] > 0 && !empty($data['CertificateOrderList'])) {
foreach ($data['CertificateOrderList'] as $cert) {
if (strtolower($cert['SerialNo']) == $serial_no) {
if (strtolower($cert['SerialNo']) == $serial_no || strpos(strtolower($cert['SerialNo']), $serial_no) !== false) {
$cert_id = $cert['CertificateId'];
$cert_name = $cert['Name'];
break;
@@ -216,7 +216,7 @@ class aliyun implements DeployInterface
if ($flag) {
$exist_cert_id = $cert['Id'];
$exist_cert_name = $cert['Name'];
$exist_cert_casid = $cert['CasId'];
$exist_cert_casid = isset($cert['CasId']) ? $cert['CasId'] : null;
break;
}
}
@@ -264,6 +264,12 @@ class aliyun implements DeployInterface
$domain = $config['domain'];
if (empty($domain)) throw new Exception('WAF绑定域名不能为空');
if ($config['region'] == 'ap-southeast-1') {
$cert_id .= '-ap-southeast-1';
} else {
$cert_id .= '-cn-hangzhou';
}
$endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com';
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2021-10-01', $this->proxy);
@@ -292,17 +298,19 @@ class aliyun implements DeployInterface
} catch (Exception $e) {
throw new Exception('查询CNAME接入详情失败' . $e->getMessage());
}
if (!isset($data['Listen'])) {
throw new Exception('没有找到' . $domain . '监听器');
}
if (isset($data['Listen']['CertId'])) {
$old_cert_id = $data['Listen']['CertId'];
if (strpos($old_cert_id, '-')) $old_cert_id = substr($old_cert_id, 0, strpos($old_cert_id, '-'));
if (!empty($old_cert_id) && $old_cert_id == $cert_id) {
$this->log('WAF域名 ' . $domain . ' 证书已配置,无需重复操作');
return;
}
}
$data['Listen']['CertId'] = $cert_id . '-cn-hangzhou';
$data['Listen']['CertId'] = $cert_id;
if (empty($data['Listen']['HttpsPorts'])) $data['Listen']['HttpsPorts'] = [443];
$data['Redirect']['Backends'] = $data['Redirect']['AllBackends'];
$param = [

View File

@@ -23,6 +23,14 @@ class ssh implements DeployInterface
public function deploy($fullchain, $privatekey, $config, &$info)
{
$connection = $this->connect();
if (isset($config['cmd_pre']) && !empty($config['cmd_pre'])) {
$cmds = explode("\n", $config['cmd_pre']);
foreach ($cmds as $cmd) {
$cmd = trim($cmd);
if (empty($cmd)) continue;
$this->exec($connection, $cmd);
}
}
$sftp = ssh2_sftp($connection);
if ($config['format'] == 'pem') {
$stream = fopen("ssh2.sftp://$sftp{$config['pem_cert_file']}", 'w');

View File

@@ -109,19 +109,30 @@ class synology implements DeployInterface
'_sid' => $this->token['sid'],
'SynoToken' => $this->token['synotoken'],
];
$privatekey_file = tempnam(sys_get_temp_dir(), 'privatekey');
file_put_contents($privatekey_file, $privatekey);
$fullchain_file = tempnam(sys_get_temp_dir(), 'fullchain');
file_put_contents($fullchain_file, $fullchain);
$post = [
'key' => new \CURLFile($privatekey_file),
'cert' => new \CURLFile($fullchain_file),
'id' => $id,
'desc' => $config['desc'],
$headers = [
'Content-Type' => 'multipart/form-data'
];
$response = http_request($url . '?' . http_build_query($params), $post, null, null, null, $this->proxy, null, 15);
unlink($privatekey_file);
unlink($fullchain_file);
$post = [
[
'name' => 'key',
'filename' => 'key.pem',
'contents' => $privatekey
],
[
'name' => 'cert',
'filename' => 'cert.pem',
'contents' => $fullchain
],
[
'name' => 'id',
'contents' => $id
],
[
'name' => 'desc',
'contents' => $config['desc']
]
];
$response = http_request($url . '?' . http_build_query($params), $post, null, null, $headers, $this->proxy, null, 15);
$result = json_decode($response['body'], true);
if ($id) {
if (isset($result['success']) && $result['success']) {

View File

@@ -60,6 +60,8 @@ class tencent implements DeployInterface
return $this->deploy_clb($cert_id, $config);
} elseif ($config['product'] == 'scf') {
return $this->deploy_scf($cert_id, $config);
} elseif ($config['product'] == 'teo' && isset($config['site_id'])) {
return $this->deploy_teo($cert_id, $config);
} else {
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
if ($config['product'] == 'waf') {
@@ -122,11 +124,17 @@ class tencent implements DeployInterface
} else {
$instance_ids = [$instance_id];
}
if ($product == 'cdn') {
$instance_ids = array_map(function ($id) {
return $id . '|on';
}, $instance_ids);
}
$param = [
'CertificateId' => $cert_id,
'InstanceIdList' => $instance_ids,
'ResourceType' => $product,
];
if ($product == 'live') $param['Status'] = 1;
$data = $this->client->request('DeployCertificateInstance', $param);
$this->log(json_encode($data));
$this->log(strtoupper($product) . '实例 ' . $instance_id . ' 部署证书成功!');
@@ -253,6 +261,26 @@ class tencent implements DeployInterface
$this->log('云函数自定义域名 ' . $config['domain'] . ' 部署证书成功!');
}
private function deploy_teo($cert_id, $config)
{
if (empty($config['site_id'])) throw new Exception('站点ID不能为空');
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
$endpoint = isset($config['site_type']) && $config['site_type'] == 'intl' ? 'teo.intl.tencentcloudapi.com' : 'teo.tencentcloudapi.com';
$client = new TencentCloud($this->SecretId, $this->SecretKey, $endpoint, 'teo', '2022-09-01', null, $this->proxy);
$hosts = explode(',', $config['domain']);
$param = [
'ZoneId' => $config['site_id'],
'Hosts' => $hosts,
'Mode' => 'sslcert',
'ServerCertInfo' => [[
'CertId' => $cert_id
]]
];
$data = $client->request('ModifyHostsCertificate', $param);
$this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!');
}
public function setLogger($func)
{
$this->logger = $func;

View File

@@ -101,8 +101,8 @@ class upyun implements DeployInterface
$result = json_decode($response['body'], true);
if (isset($result['data']['result']) && $result['data']['result'] == true) {
$cookie = '';
if (isset($response['headers']['Set-Cookie'])) {
foreach ($response['headers']['Set-Cookie'] as $val) {
if (isset($response['headers']['set-cookie'])) {
foreach ($response['headers']['set-cookie'] as $val) {
$arr = explode('=', $val);
if ($arr[1] == '' || $arr[1] == 'deleted') continue;
$cookie .= $val . '; ';

View File

@@ -11,15 +11,17 @@ class CertTaskService
public function execute()
{
$this->execute_deploy();
$this->execute_order();
(new ExpireNoticeService())->task();
config_set('certtask_time', date("Y-m-d H:i:s"));
echo 'done'.PHP_EOL;
if ($this->execute_deploy()) {
config_set('certdeploy_time', date("Y-m-d H:i:s"));
}
if ($this->execute_order()) {
config_set('certtask_time', date("Y-m-d H:i:s"));
}
}
private function execute_order()
{
echo '开始执行SSL证书签发任务...'.PHP_EOL;
$days = config_get('cert_renewdays', 7);
$list = Db::name('cert_order')->field('id,aid,status,issend')->whereRaw('status NOT IN (3,4) AND (retrytime IS NULL OR retrytime<NOW()) OR status=3 AND isauto=1 AND expiretime<:expiretime', ['expiretime' => date('Y-m-d H:i:s', time() + $days * 86400)])->select();
//print_r($list);exit;
@@ -55,6 +57,7 @@ class CertTaskService
if ($failcount >= 3) break;
sleep(1);
}
return true;
}
private function execute_deploy()
@@ -64,14 +67,15 @@ class CertTaskService
$hour = date('H');
if($start <= $end){
if($hour < $start || $hour > $end){
echo '不在部署任务运行时间范围内'.PHP_EOL; return;
echo '不在部署任务运行时间范围内'.PHP_EOL; return false;
}
}else{
if($hour < $start && $hour > $end){
echo '不在部署任务运行时间范围内'.PHP_EOL; return;
echo '不在部署任务运行时间范围内'.PHP_EOL; return false;
}
}
echo '开始执行SSL证书部署任务...'.PHP_EOL;
$list = Db::name('cert_deploy')->field('id,status,issend')->whereRaw('active=1 AND status IN (0,-1) AND (retrytime IS NULL OR retrytime<NOW())')->select();
//print_r($list);exit;
$count = 0;
@@ -95,5 +99,6 @@ class CertTaskService
if ($count >= 3) break;
sleep(1);
}
return true;
}
}

View File

@@ -26,6 +26,8 @@ class ExpireNoticeService
public function task()
{
echo '开始执行域名到期提醒任务...' . PHP_EOL;
config_set('domain_expire_time', date("Y-m-d H:i:s"));
$count = $this->refreshDomainList();
if ($count > 0) return;

View File

@@ -39,9 +39,7 @@ class OptimizeService
public function get_ip_address($cdn_type = 1, $ip_type = 'v4')
{
$api = config_get('optimize_ip_api', 0);
if ($api == 2) {
$url = 'https://api.345673.xyz/get_data';
} elseif ($api == 1) {
if ($api == 1) {
$url = 'https://api.hostmonit.com/get_optimization_ip';
} else {
$url = 'https://www.wetest.vip/api/cf2dns/';
@@ -51,6 +49,8 @@ class OptimizeService
$url .= 'get_cloudfront_ip';
} elseif ($cdn_type == 3) {
$url .= 'get_gcore_ip';
} elseif ($cdn_type == 4) {
$url .= 'get_edgeone_ip';
}
}
$params = [
@@ -96,7 +96,15 @@ class OptimizeService
//批量执行优选任务
public function execute()
{
$minute = config_get('optimize_ip_min', '30');
$last = config_get('optimize_ip_time', null, true);
if ($last && strtotime($last) > time() - $minute * 60) {
return false;
}
$list = Db::name('optimizeip')->where('active', 1)->select();
if (count($list) == 0) {
return false;
}
echo '开始执行IP优选任务共获取到'.count($list).'个待执行任务'."\n";
foreach ($list as $row) {
try {
@@ -108,6 +116,8 @@ class OptimizeService
echo '优选任务'.$row['id'].'执行失败:'.$e->getMessage()."\n";
}
}
config_set('optimize_ip_time', date("Y-m-d H:i:s"));
return true;
}
//执行单个优选任务

View File

@@ -126,7 +126,11 @@ class CheckUtils
return ['status' => false, 'errmsg' => 'Invalid IP address', 'usetime' => 0];
}
$timeout = 1;
exec('ping -c 1 -w '.$timeout.' '.$target, $output, $return_var);
if (str_contains($target, ':')) {
exec('ping -6 -c 1 -w '.$timeout.' '.$target, $output, $return_var);
} else {
exec('ping -c 1 -w '.$timeout.' '.$target, $output, $return_var);
}
if (!empty($output[1])) {
if (strpos($output[1], '毫秒') !== false) {
$usetime = getSubstr($output[1], '时间=', ' 毫秒');

View File

@@ -454,7 +454,7 @@ function operation(action){
$.ajax({
type : 'POST',
url : '/cert/order/operation',
data : {action: action, ids: ids},
data : {act: action, ids: ids},
dataType : 'json',
success : function(data) {
layer.close(ii);

View File

@@ -3,14 +3,6 @@
{block name="main"}
<div class="row">
<div class="col-xs-12 col-sm-8 col-lg-6 center-block" style="float: none;">
<div class="panel panel-warning">
<div class="panel-heading"><h3 class="panel-title">计划任务说明</h3></div>
<div class="panel-body">
<p><li>计划任务将以下命令添加到计划任务1分钟1次</li></p>
<p><code>cd {:app()->getRootPath()} && php think certtask</code></p>
<p><li>上次运行时间:<font color="green">{:config_get('certtask_time', '未运行', true)}</font></li></p>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">自动续签设置</h3></div>
@@ -28,7 +20,7 @@
</form>
</div>
<div class="panel-footer">
<li>提示:只有已开启自动续签的证书,才会自动续签。</li>
<li>提示:只有已开启自动续签的证书,并添加<a href="/system/cronset">计划任务</a>才会自动续签。</li>
</div>
</div>

View File

@@ -317,7 +317,7 @@ function operation(action){
$.ajax({
type : 'POST',
url : '/cert/deploy/operation',
data : {action: action, ids: ids},
data : {act: action, ids: ids},
dataType : 'json',
success : function(data) {
layer.close(ii);
@@ -337,7 +337,7 @@ function batch_set_cert(ids){
$.ajax({
type : 'POST',
url : '/cert/deploy/operation',
data : {action: 'cert', ids: ids, certid: text},
data : {act: 'cert', ids: ids, certid: text},
dataType : 'json',
success : function(data) {
layer.close(ii);

View File

@@ -12,6 +12,9 @@
<link href="/static/css/app.min.css" rel="stylesheet">
<link href="/static/css/skins/{$skin}.css" rel="stylesheet">
<link href="/static/css/bootstrap-table.css?v=2" rel="stylesheet"/>
{if file_exists(public_path() . 'static/css/custom.css')}
<link href="/static/css/custom.css" rel="stylesheet">
{/if}
<script src="{$cdnpublic}jquery/3.6.4/jquery.min.js"></script>
<!--[if lt IE 9]>
<script src="{$cdnpublic}html5shiv/3.7.3/html5shiv.min.js"></script>
@@ -150,10 +153,10 @@
<li class="{:checkIfActive('deployaccount')}"><a href="/cert/deployaccount"><i class="fa fa-circle-o"></i> 自动部署账户</a></li>
<li class="{:checkIfActive('deploytask,deploy_form')}"><a href="/cert/deploytask"><i class="fa fa-circle-o"></i> 自动部署任务</a></li>
<li class="{:checkIfActive('cname')}"><a href="/cert/cname"><i class="fa fa-circle-o"></i> CNAME代理</a></li>
<li class="{:checkIfActive('certset')}"><a href="/cert/certset"><i class="fa fa-circle-o"></i> 计划任务设置</a></li>
<li class="{:checkIfActive('certset')}"><a href="/cert/certset"><i class="fa fa-circle-o"></i> 自动续签设置</a></li>
</ul>
</li>
<li class="treeview {:checkIfActive('loginset,noticeset,proxyset')}">
<li class="treeview {:checkIfActive('cronset,loginset,noticeset,proxyset')}">
<a href="javascript:;">
<i class="fa fa-cogs fa-fw"></i>
<span>系统设置</span>
@@ -162,6 +165,7 @@
</span>
</a>
<ul class="treeview-menu">
<li class="{:checkIfActive('cronset')}"><a href="/system/cronset"><i class="fa fa-circle-o"></i> 计划任务</a></li>
<li class="{:checkIfActive('loginset')}"><a href="/system/loginset"><i class="fa fa-circle-o"></i> 登录设置</a></li>
<li class="{:checkIfActive('noticeset')}"><a href="/system/noticeset"><i class="fa fa-circle-o"></i> 通知设置</a></li>
<li class="{:checkIfActive('proxyset')}"><a href="/system/proxyset"><i class="fa fa-circle-o"></i> 代理设置</a></li>

View File

@@ -160,7 +160,7 @@
<p>1、php需要安装swoole组件</p>
<p>2、在命令行执行以下命令启动进程</p>
<p><code>cd {:app()->getRootPath()} && php think dmtask</code></p>
<p>3、也可以使用进程守护管理器添加守护进程,运行目录:{:app()->getRootPath()},启动命令:php think dmtask</p>
<p>3、也可以使用进程守护管理器添加守护进程<br/>运行目录:<code>{:app()->getRootPath()}</code><br/>启动命令:<code>php think dmtask</code></p>
</div>
</div>
</div>

View File

@@ -170,7 +170,7 @@
var userLevel = "{:request()->user['level']}";
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 15;
const defaultPageSize = getCookie('domain_pagesize') ? getCookie('domain_pagesize') : 15;
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
@@ -298,7 +298,13 @@ $(document).ready(function(){
],
onLoadSuccess: function(data) {
$('[data-toggle="tooltip"]').tooltip()
}
},
onPageChange: function(number, size){
if(size != defaultPageSize){
defaultPageSize = size;
setCookie('domain_pagesize', size);
}
},
})
$("#form-store select[name=aid]").change(function(){

View File

@@ -35,12 +35,8 @@
</div>
</form>
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading"><h3 class="panel-title">计划任务说明</h3></div>
<div class="panel-body">
<p>支持域名到期提醒+域名列表到期时间自动刷新。与SSL证书共用计划任务不需要单独添加计划任务。</p><p><a href="/cert/certset">查看计划任务说明</a></p>
<div class="panel-footer">
<p>需添加<a href="/system/cronset">计划任务</a>,支持域名到期提醒+域名列表到期时间自动刷新。</p>
</div>
</div>

View File

@@ -175,7 +175,7 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
<div id="searchbox1">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="keyword" placeholder="输入关键字">
<input type="text" class="form-control" name="keyword" placeholder="输入关键字">
</div>
<div class="form-group">
<select name="status" class="form-control"><option value="">所有状态</option><option value="1">启用</option><option value="0">暂停</option></select>
@@ -213,13 +213,13 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
</select>
</div>
<div class="form-group">
<input type="text" class="form-control" name="subdomain" placeholder="输入主机记录">
<input type="text" class="form-control" name="subdomain" placeholder="输入主机记录">
</div>
<div class="form-group">
<select name="line" class="form-control"><option value="">全部线路</option></select>
</div>
<div class="form-group">
<input type="text" class="form-control" name="value" placeholder="输入记录值">
<input type="text" class="form-control" name="value" placeholder="输入记录值">
</div>
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
<a href="javascript:searchClear()" class="btn btn-default" title="刷新解析记录列表"><i class="fa fa-refresh"></i> 刷新</a>
@@ -227,9 +227,9 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
</div>
</form>
<table id="listTable">
<table id="listTable">
</table>
</div>
</div>
</div>
</div>
</div>
@@ -269,10 +269,10 @@ $(document).ready(function(){
visible: false,
title: '记录ID'
},
{
field: 'Name',
title: '主机记录'
},
{
field: 'Name',
title: '主机记录'
},
{
field: 'Type',
title: '记录类型',
@@ -282,18 +282,26 @@ $(document).ready(function(){
return value;
}
},
{
{
field: 'LineName',
title: '线路类型'
},
{
field: 'Value',
title: '记录值',
formatter: function(value, row, index) {
if(row.Type == 'MX') return value + ' | '+row.MX;
return value;
}
},
field: 'Value',
title: '记录值',
formatter: function(value, row, index) {
var copyId = 'copy-value-' + row.RecordId;
if(row.Type == 'MX') {
// 只复制 mx.yandex.net按钮在其右侧优先级单独显示
return '<span id="'+copyId+'" data-value="'+htmlEscape(value)+'">'+value+'</span>'
+ '<a href="javascript:void(0);" title="复制记录值" onclick="copyToClipboard(null, \'#'+copyId+'\')" style="padding-left:6px;"><i class=\"fa fa-copy\"></i></a>'
+ '<span class="mx-priority"> | '+row.MX+'</span>';
} else {
return '<span id="'+copyId+'" data-value="'+htmlEscape(value)+'">'+value+'</span>'
+ '<a href="javascript:void(0);" title="复制记录值" onclick="copyToClipboard(null, \'#'+copyId+'\')" style="padding-left:6px;"><i class=\"fa fa-copy\"></i></a>';
}
}
},
{
field: 'TTL',
title: 'TTL'
@@ -339,8 +347,12 @@ $(document).ready(function(){
}
html += '<a href="javascript:delItem(\''+row.RecordId+'\')" class="btn btn-danger btn-xs">删除</a>&nbsp;&nbsp;';
if(dnsconfig.remark == 1){
html += '<a href="javascript:setRemark(\''+row.RecordId+'\')" class="btn btn-info btn-xs">备注</a>';
html += '<a href="javascript:setRemark(\''+row.RecordId+'\')" class="btn btn-info btn-xs">备注</a>&nbsp;&nbsp;';
}
if(row.Name === "@") var domain = "{$domainName}";
else var domain = row.Name + ".{$domainName}";
domain = domain.replace(/\*/g, 'www');
html += '<a href="http://' + domain + '" target="_blank" title="访问域名" class="btn btn-default btn-xs"><i class="fa fa-external-link"></i></a>';
return html;
}
},
@@ -719,5 +731,39 @@ function advanceSearch(){
$("#searchbox1").slideDown();
}
}
function copyToClipboard(text, selector) {
if (!text && selector) {
var el = document.querySelector(selector);
if (el) {
text = el.getAttribute('data-value');
}
}
var tempInput = document.createElement('input');
tempInput.style.position = 'absolute';
tempInput.style.left = '-9999px';
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
if(selector){
var icon = document.querySelector(selector + ' + a i');
if(icon){
var oldClass = icon.className;
icon.className = 'fa fa-check';
setTimeout(function(){ icon.className = oldClass; }, 1000);
}
}
layer.msg('已复制到剪贴板', {icon: 1, time: 600});
}
// 工具函数HTML转义防止XSS
function htmlEscape(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
</script>
{/block}

View File

@@ -129,7 +129,7 @@ new Vue({
cdntypeList: {
1:'CloudFlare',
2:"CloudFront",
3:'Gcore'
4:'EdgeOne'
},
},
watch: {

View File

@@ -75,6 +75,8 @@ $(document).ready(function(){
return 'CloudFront';
}else if(value == 3){
return 'Gcore';
}else if(value == 4){
return 'EdgeOne';
}else{
return '未知';
}

View File

@@ -14,10 +14,9 @@
<div class="panel-heading"><h3 class="panel-title">使用说明</h3></div>
<div class="panel-body">
<p><li>不支持对CloudFlare里的域名添加优选必须使用其他DNS服务商。需开通Cloudflare for SaaS且域名使用CNAME的方式解析到CloudFlare。</li></p>
<p><li>数据接口:<a href="https://www.wetest.vip/" target="_blank" rel="noreferrer">wetest.vip</a> 数据接口支持CloudFlare、CloudFront、Gcore<a href="https://stock.hostmonit.com/" target="_blank" rel="noreferrer">HostMonit</a> 只支持CloudFlare。</li></p>
<p><li>数据接口:<a href="https://www.wetest.vip/" target="_blank" rel="noreferrer">wetest.vip</a> 数据接口支持CloudFlare、CloudFront、EdgeOne<a href="https://stock.hostmonit.com/" target="_blank" rel="noreferrer">HostMonit</a> 只支持CloudFlare。</li></p>
<p><li>接口密钥默认o1zrmHAF为免费KEY可永久免费使用。</li></p>
<p><li>计划任务将以下命令添加到计划任务周期设置为15分钟以上</li></p>
<p><code>cd {:app()->getRootPath()} && php think opiptask</code></p>
<p><li>自动更新:可查看<a href="/system/cronset">计划任务设置</a></p>
</div>
</div>
</div>
@@ -43,6 +42,22 @@
</form>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">自动更新设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-3 control-label">自动更新时间间隔(分钟)</label>
<div class="col-sm-9"><input type="text" name="optimize_ip_min" value="{:config_get('optimize_ip_min', '30')}" class="form-control" placeholder="单位:分钟"/></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
</div>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,126 @@
{extend name="common/layout" /}
{block name="title"}计划任务{/block}
{block name="main"}
<div class="row">
<div class="col-xs-12 col-sm-8 col-lg-6 center-block" style="float: none;">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">计划任务说明</h3></div>
<div class="panel-body">
{if config_get('cron_type', '0') == '1'}
<p><li>需定时访问以下URL频率1分钟1次</li></p>
<p><code>{$siteurl}/cron?key={:config_get('cron_key')}</code></p>
{else}
<p><li>将以下Shell命令添加到计划任务频率1分钟1次</li></p>
<p><code>cd {:app()->getRootPath()} && php think certtask</code></p>
{if $is_user_www}<p><li><b>计划任务执行用户必须选择www用户</b></li></p>{/if}
<p><li>采用Docker镜像部署的会自动添加计划任务无需手动添加。</li></p>
{/if}
</div>
</div>
<div class="panel panel-intro">
<div class="panel-heading"><h3 class="panel-title">计划任务设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-3 control-label">计划任务执行方式</label>
<div class="col-sm-9"><select class="form-control" name="cron_type" default="{:config_get('cron_type', '0')}"><option value="0">Shell命令推荐</option><option value="1">访问URL</option></select></div>
</div>
<div class="form-group" id="cron_url" {:config_get('cron_type', '0') == 0 ? 'style="display: none"' : ''}>
<label class="col-sm-3 control-label">访问密钥</label>
<div class="col-sm-9"><input type="text" name="cron_key" value="{:config_get('cron_key')}" class="form-control" requ/></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
</div>
</div>
</form>
</div>
<div class="panel-footer">
<p>优先推荐使用Shell命令方式执行计划任务访问URL方式可能会请求超时导致执行失败。</p><p>如果是虚拟主机环境无法执行命令则可以使用访问URL方式。</p>
</div>
</div>
<div class="panel panel-success mt-3">
<div class="panel-heading"><h3 class="panel-title">计划任务运行状态</h3></div>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>任务名称</th>
<th>上次运行时间</th>
</tr>
</thead>
<tbody>
<tr>
<td>SSL证书续签</td>
<td><font color="green">{:config_get('certtask_time', '未运行', true)}</font></td>
</tr>
<tr>
<td>SSL证书部署</td>
<td><font color="green">{:config_get('certdeploy_time', '未运行', true)}</font></td>
</tr>
<tr>
<td>域名到期提醒</td>
<td><font color="green">{:config_get('domain_expire_time', '未运行', true)}</font></td>
</tr>
<tr>
<td>CF优选IP更新</td>
<td><font color="green">{:config_get('optimize_ip_time', '未运行', true)}</font></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script>
var items = $("select[default]");
for (i = 0; i < items.length; i++) {
$(items[i]).val($(items[i]).attr("default")||0);
}
function saveSetting(obj){
var cron_type = $("select[name='cron_type']").val();
var cron_key = $("input[name='cron_key']").val();
if(cron_type == 1 && cron_key == ''){
layer.alert('访问密钥不能为空!', {icon: 2});
return false;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/system/set',
data : {cron_type:cron_type, cron_key:cron_key},
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert('设置保存成功!', {
icon: 1,
closeBtn: false
}, function(){
window.location.reload()
});
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
return false;
}
$("select[name='cron_type']").change(function(){
if($(this).val() == 0){
$("#cron_url").hide();
}else{
$("#cron_url").show();
}
});
</script>
{/block}

View File

@@ -64,7 +64,7 @@
},
"require-dev": {
"symfony/var-dumper": "^7.3",
"topthink/think-trace":"^1.0",
"topthink/think-trace":"^2.0",
"swoole/ide-helper": "^6.0"
},
"autoload": {

56
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "39f34360e80abbce3e603a056ae6211a",
"content-hash": "876ad9987c672e0fa8d90dbe55321dc7",
"packages": [
{
"name": "cccyun/php-whois",
@@ -1348,16 +1348,16 @@
},
{
"name": "topthink/framework",
"version": "v8.1.2",
"version": "v8.1.3",
"source": {
"type": "git",
"url": "https://github.com/top-think/framework.git",
"reference": "8faec5c9b7a7f2a66ca3140a57e81bd6cd37567c"
"reference": "e4207e98b66f92d26097ed6efd535930cba90e8f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/framework/zipball/8faec5c9b7a7f2a66ca3140a57e81bd6cd37567c",
"reference": "8faec5c9b7a7f2a66ca3140a57e81bd6cd37567c",
"url": "https://api.github.com/repos/top-think/framework/zipball/e4207e98b66f92d26097ed6efd535930cba90e8f",
"reference": "e4207e98b66f92d26097ed6efd535930cba90e8f",
"shasum": ""
},
"require": {
@@ -1409,9 +1409,9 @@
],
"support": {
"issues": "https://github.com/top-think/framework/issues",
"source": "https://github.com/top-think/framework/tree/v8.1.2"
"source": "https://github.com/top-think/framework/tree/v8.1.3"
},
"time": "2025-01-14T08:04:03+00:00"
"time": "2025-07-14T03:48:44+00:00"
},
{
"name": "topthink/think-container",
@@ -1507,16 +1507,16 @@
},
{
"name": "topthink/think-orm",
"version": "v4.0.45",
"version": "v4.0.47",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-orm.git",
"reference": "ea2e627af7306f7714e4baafc70943cad954cc77"
"reference": "04b920632c943e8f4f88336ac558203bab7a8758"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/think-orm/zipball/ea2e627af7306f7714e4baafc70943cad954cc77",
"reference": "ea2e627af7306f7714e4baafc70943cad954cc77",
"url": "https://api.github.com/repos/top-think/think-orm/zipball/04b920632c943e8f4f88336ac558203bab7a8758",
"reference": "04b920632c943e8f4f88336ac558203bab7a8758",
"shasum": ""
},
"require": {
@@ -1524,7 +1524,7 @@
"ext-pdo": "*",
"php": ">=8.0.0",
"psr/log": ">=1.0",
"psr/simple-cache": ">=1.0",
"psr/simple-cache": "^3.0",
"topthink/think-helper": "^3.1",
"topthink/think-validate": "^3.0"
},
@@ -1561,9 +1561,9 @@
],
"support": {
"issues": "https://github.com/top-think/think-orm/issues",
"source": "https://github.com/top-think/think-orm/tree/v4.0.45"
"source": "https://github.com/top-think/think-orm/tree/v4.0.47"
},
"time": "2025-06-24T00:00:32+00:00"
"time": "2025-07-24T09:41:24+00:00"
},
{
"name": "topthink/think-template",
@@ -1727,16 +1727,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e"
"reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e",
"reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42",
"reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42",
"shasum": ""
},
"require": {
@@ -1791,7 +1791,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.3.0"
"source": "https://github.com/symfony/var-dumper/tree/v7.3.1"
},
"funding": [
{
@@ -1807,25 +1807,25 @@
"type": "tidelift"
}
],
"time": "2025-04-27T18:39:23+00:00"
"time": "2025-06-27T19:55:54+00:00"
},
{
"name": "topthink/think-trace",
"version": "v1.6",
"version": "v2.0",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-trace.git",
"reference": "136cd5d97e8bdb780e4b5c1637c588ed7ca3e142"
"reference": "4ba6da2945b37931d61900a6e55dc02b05e5a63f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/think-trace/zipball/136cd5d97e8bdb780e4b5c1637c588ed7ca3e142",
"reference": "136cd5d97e8bdb780e4b5c1637c588ed7ca3e142",
"url": "https://api.github.com/repos/top-think/think-trace/zipball/4ba6da2945b37931d61900a6e55dc02b05e5a63f",
"reference": "4ba6da2945b37931d61900a6e55dc02b05e5a63f",
"shasum": ""
},
"require": {
"php": ">=7.1.0",
"topthink/framework": "^6.0|^8.0"
"php": ">=8.0",
"topthink/framework": "^8.1"
},
"type": "library",
"extra": {
@@ -1856,9 +1856,9 @@
"description": "thinkphp debug trace",
"support": {
"issues": "https://github.com/top-think/think-trace/issues",
"source": "https://github.com/top-think/think-trace/tree/v1.6"
"source": "https://github.com/top-think/think-trace/tree/v2.0"
},
"time": "2023-02-07T08:36:32+00:00"
"time": "2025-06-12T09:18:19+00:00"
}
],
"aliases": [],

View File

@@ -31,7 +31,7 @@ return [
'show_error_msg' => true,
'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl',
'version' => '1037',
'version' => '1039',
'dbversion' => '1033'
];

View File

@@ -6,7 +6,6 @@ return [
// 指令定义
'commands' => [
'dmtask' => 'app\command\Dmtask',
'opiptask' => 'app\command\Opiptask',
'certtask' => 'app\command\Certtask',
'reset' => 'app\command\Reset',
],

View File

View File

@@ -29,6 +29,7 @@ Route::get('/logout', 'auth/logout');
Route::any('/quicklogin', 'auth/quicklogin');
Route::any('/dmtask/status', 'dmonitor/status');
Route::any('/optimizeip/status', 'optimizeip/status');
Route::get('/cron', 'system/cron');
Route::group(function () {
Route::any('/', 'index/index');
@@ -120,6 +121,7 @@ Route::group(function () {
Route::get('/system/tgbottest', 'system/tgbottest');
Route::get('/system/webhooktest', 'system/webhooktest');
Route::post('/system/proxytest', 'system/proxytest');
Route::get('/system/cronset', 'system/cronset');
})->middleware(CheckLogin::class)
->middleware(ViewOutput::class);