10 Commits
1.0 ... 1.3

Author SHA1 Message Date
net909
f20b132d97 新增CF优选IP 2024-05-01 10:51:34 +08:00
net909
ad85fa7c22 修复 2024-04-26 23:16:39 +08:00
net909
51538495b1 1.2.1 2024-04-22 23:09:09 +08:00
net909
8f257664ae fix 2024-04-22 23:08:21 +08:00
net909
8d653b872a fix 2024-04-20 21:36:11 +08:00
net909
7201476e9c fix 2024-04-20 10:22:22 +08:00
net909
3292f8e9ce 修复运行状态显示 2024-04-19 19:44:52 +08:00
net909
497aabb8e6 readme 2024-04-18 19:38:47 +08:00
net909
923cdc5e90 1.2 2024-04-18 19:35:14 +08:00
net909
d43ac1404a 增加dsnla,修复转发类型显示 2024-04-17 11:16:37 +08:00
51 changed files with 3723 additions and 108 deletions

View File

@@ -6,9 +6,15 @@
- 腾讯云
- 华为云
- 西部数码
- DNSLA
- CloudFlare
本系统支持多用户每个用户可分配不同的域名解析权限支持API接口支持获取域名独立DNS控制面板登录链接方便各种IDC系统对接。
### 功能特性
- 多用户管理,可为每个用户可分配不同的域名解析权限
- 提供API接口可获取域名单独的登录链接方便各种IDC系统对接
- 容灾切换功能支持ping、tcp、http(s)检测协议并自动暂停/修改域名解析,并支持邮件、微信公众号通知
- CF优选IP功能支持获取最新的Cloudflare优选IP并自动更新到解析记录
### 演示截图
@@ -28,6 +34,10 @@
![](https://p0.meituan.net/csc/d1bd90bedca9b6cbc5da40286bdb5cd5228438.png)
CF优选IP功能添加优选IP任务
![](https://p1.meituan.net/csc/da70c76753aee4bce044d16fadd56e5f217660.png)
### 部署方法
* 从[Release](https://github.com/netcccyun/dnsmgr/releases)页面下载安装包

76
app/command/Dmtask.php Normal file
View File

@@ -0,0 +1,76 @@
<?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 app\lib\TaskRunner;
class Dmtask extends Command
{
protected function configure()
{
// 指令配置
$this->setName('dmtask')
->setDescription('容灾切换任务');
}
protected function execute(Input $input, Output $output)
{
config_set('run_error', '');
if(!extension_loaded('swoole')){
$output->writeln('[Error] 未安装Swoole扩展');
config_set('run_error', '未安装Swoole扩展');
return;
}
try{
$output->writeln('进程启动成功.');
$this->runtask();
}catch(Exception $e){
$output->writeln('[Error] '.$e->getMessage());
config_set('run_error', $e->getMessage());
}
}
private function runtask(){
\Co::set(['hook_flags'=> SWOOLE_HOOK_ALL]);
\Co\run(function() {
$date = date("Ymd");
$count = config_get('run_count', null, true) ?? 0;
while(true){
sleep(1);
if($date != date("Ymd")){
$count = 0;
$date = date("Ymd");
}
$rows = Db::name('dmtask')->where('checknexttime', '<=', time())->where('active', 1)->order('id', 'ASC')->select();
foreach($rows as $row){
\go(function () use($row) {
try{
(new TaskRunner())->execute($row);
} catch (\Swoole\ExitException $e) {
echo $e->getStatus()."\n";
} catch (Exception $e) {
echo $e->__toString()."\n";
}
});
Db::name('dmtask')->where('id', $row['id'])->update([
'checktime' => time(),
'checknexttime' => time() + $row['frequency']
]);
$count++;
}
config_set('run_time', date("Y-m-d H:i:s"));
config_set('run_count', $count);
}
});
}
}

28
app/command/Opiptask.php Normal file
View File

@@ -0,0 +1,28 @@
<?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 app\lib\OptimizeService;
class Opiptask extends Command
{
protected function configure()
{
// 指令配置
$this->setName('opiptask')
->setDescription('CF优选IP任务');
}
protected function execute(Input $input, Output $output)
{
(new OptimizeService())->execute();
}
}

View File

@@ -201,10 +201,53 @@ function checkPermission($type, $domain = null){
function getAdminSkin(){
$skin = cookie('admin_skin');
if(empty($skin)){
$skin = cache('admin_skin');
$skin = config_get('admin_skin');
}
if(empty($skin)){
$skin = 'skin-black-blue';
}
return $skin;
}
function config_get($key, $default = null, $force = false)
{
if ($force) {
$value = Db::name('config')->where('key', $key)->value('value');
} else {
$value = config('sys.'.$key);
}
return $value ?: $default;
}
function config_set($key, $value)
{
$res = Db::name('config')->replace()->insert(['key'=>$key, 'value'=>$value]);
return $res!==false;
}
function getMillisecond()
{
list($s1, $s2) = explode(' ', microtime());
return (int)sprintf('%.0f', (floatval($s1) + floatval($s2)) * 1000);
}
function getDnsType($value){
if(filter_var($value, FILTER_VALIDATE_IP))return 'A';
else return 'CNAME';
}
function convert_second($s){
$m = floor($s/60);
if($m == 0){
return $s.'秒';
}else{
$s = $s%60;
$h = floor($m/60);
if($h == 0){
return $m.'分钟'.$s.'秒';
}else{
$m = $m%60;
return $h.'小时'.$m.'分钟'.$s.'秒';
}
}
}

View File

@@ -36,7 +36,7 @@ class Auth extends BaseController
}
if (file_exists($login_limit_file)) {
$login_limit = unserialize(file_get_contents($login_limit_file));
if ($login_limit['count'] >= $login_limit_count && $login_limit['time'] > time() - 86400) {
if ($login_limit['count'] >= $login_limit_count && $login_limit['time'] > time() - 7200) {
exit(json_encode(['code' => -1, 'msg' => '多次登录失败,暂时禁止登录。可删除/runtime/@login.lock文件解除限制', 'vcode'=>1]));
}
}
@@ -49,6 +49,9 @@ class Auth extends BaseController
$expiretime = time()+2562000;
$token = authcode("user\t{$user['id']}\t{$session}\t{$expiretime}", 'ENCODE', env('app.sys_key'));
cookie('user_token', $token, ['expire' => $expiretime, 'httponly' => true]);
if (file_exists($login_limit_file)) {
unlink($login_limit_file);
}
return json(['code'=>0]);
}else{
if($user){

267
app/controller/Dmonitor.php Normal file
View File

@@ -0,0 +1,267 @@
<?php
namespace app\controller;
use app\BaseController;
use Exception;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
use app\lib\DnsHelper;
class Dmonitor extends BaseController
{
public function overview()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
$switch_count = Db::name('dmlog')->where('date', '>=', date("Y-m-d H:i:s",strtotime("-1 days")))->count();
$fail_count = Db::name('dmlog')->where('date', '>=', date("Y-m-d H:i:s",strtotime("-1 days")))->where('action', 1)->count();
$run_time = config_get('run_time', null, true);
$run_state = $run_time ? (time()-strtotime($run_time) > 10 ? 0 : 1) : 0;
View::assign('info', [
'run_count' => config_get('run_count', null, true) ?? 0,
'run_time' => $run_time ?? '无',
'run_state' => $run_state,
'run_error' => config_get('run_error', null, true),
'switch_count' => $switch_count,
'fail_count' => $fail_count,
'swoole' => extension_loaded('swoole') ? '<font color="green">已安装</font>' : '<font color="red">未安装</font>',
]);
return View::fetch();
}
public function task()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
return View::fetch();
}
public function task_data(){
if(!checkPermission(2)) return json(['total'=>0, 'rows'=>[]]);
$type = input('post.type/d', 1);
$kw = input('post.kw', null, 'trim');
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$select = Db::name('dmtask')->alias('A')->join('domain B','A.did = B.id');
if(!empty($kw)){
if($type == 1){
$select->whereLike('rr|B.name', '%'.$kw.'%');
}elseif($type == 2){
$select->where('recordid', $kw);
}elseif($type == 3){
$select->where('main_value', $kw);
}elseif($type == 4){
$select->where('backup_value', $kw);
}elseif($type == 5){
$select->whereLike('remark', '%'.$kw.'%');
}
}
$total = $select->count();
$list = $select->order('A.id','desc')->limit($offset, $limit)->field('A.*,B.name domain')->select()->toArray();
foreach($list as &$row){
$row['checktimestr'] = date('Y-m-d H:i:s', $row['checktime']);
}
return json(['total'=>$total, 'rows'=>$list]);
}
public function taskform()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
$action = input('param.action');
if(request()->isPost()){
if($action == 'add'){
$task = [
'did' => input('post.did/d'),
'rr' => input('post.rr', null, 'trim'),
'recordid' => input('post.recordid', null, 'trim'),
'type' => input('post.type/d'),
'main_value' => input('post.main_value', null, 'trim'),
'backup_value' => input('post.backup_value', null, 'trim'),
'checktype' => input('post.checktype/d'),
'checkurl' => input('post.checkurl', null, 'trim'),
'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null,
'frequency' => input('post.frequency/d'),
'cycle' => input('post.cycle/d'),
'timeout' => input('post.timeout/d'),
'remark' => input('post.remark', null, 'trim'),
'recordinfo' => input('post.recordinfo', null, 'trim'),
'addtime' => time(),
'active' => 1
];
if(empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])){
return json(['code'=>-1, 'msg'=>'必填项不能为空']);
}
if($task['checktype'] > 0 && $task['timeout'] > $task['frequency']){
return json(['code'=>-1, 'msg'=>'为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']);
}
if($task['type'] == 2 && $task['backup_value'] == $task['main_value']){
return json(['code'=>-1, 'msg'=>'主备地址不能相同']);
}
if(Db::name('dmtask')->where('recordid', $task['recordid'])->find()){
return json(['code'=>-1, 'msg'=>'当前容灾切换策略已存在']);
}
Db::name('dmtask')->insert($task);
return json(['code'=>0, 'msg'=>'添加成功']);
}elseif($action == 'edit'){
$id = input('post.id/d');
$task = [
'did' => input('post.did/d'),
'rr' => input('post.rr', null, 'trim'),
'recordid' => input('post.recordid', null, 'trim'),
'type' => input('post.type/d'),
'main_value' => input('post.main_value', null, 'trim'),
'backup_value' => input('post.backup_value', null, 'trim'),
'checktype' => input('post.checktype/d'),
'checkurl' => input('post.checkurl', null, 'trim'),
'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null,
'frequency' => input('post.frequency/d'),
'cycle' => input('post.cycle/d'),
'timeout' => input('post.timeout/d'),
'remark' => input('post.remark', null, 'trim'),
'recordinfo' => input('post.recordinfo', null, 'trim'),
];
if(empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])){
return json(['code'=>-1, 'msg'=>'必填项不能为空']);
}
if($task['checktype'] > 0 && $task['timeout'] > $task['frequency']){
return json(['code'=>-1, 'msg'=>'为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']);
}
if($task['type'] == 2 && $task['backup_value'] == $task['main_value']){
return json(['code'=>-1, 'msg'=>'主备地址不能相同']);
}
if(Db::name('dmtask')->where('recordid', $task['recordid'])->where('id', '<>', $id)->find()){
return json(['code'=>-1, 'msg'=>'当前容灾切换策略已存在']);
}
Db::name('dmtask')->where('id', $id)->update($task);
return json(['code'=>0, 'msg'=>'修改成功']);
}elseif($action == 'setactive'){
$id = input('post.id/d');
$active = input('post.active/d');
Db::name('dmtask')->where('id', $id)->update(['active'=>$active]);
return json(['code'=>0, 'msg'=>'设置成功']);
}elseif($action == 'del'){
$id = input('post.id/d');
Db::name('dmtask')->where('id', $id)->delete();
Db::name('dmlog')->where('taskid', $id)->delete();
return json(['code'=>0, 'msg'=>'删除成功']);
}else{
return json(['code'=>-1, 'msg'=>'参数错误']);
}
}
$task = null;
if($action == 'edit'){
$id = input('get.id/d');
$task = Db::name('dmtask')->where('id', $id)->find();
if(empty($task)) return $this->alert('error', '切换策略不存在');
}
$domains = [];
foreach(Db::name('domain')->select() as $row){
$domains[$row['id']] = $row['name'];
}
View::assign('domains', $domains);
View::assign('info', $task);
View::assign('action', $action);
View::assign('support_ping', function_exists('exec')?'1':'0');
return View::fetch();
}
public function taskinfo()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
$id = input('param.id/d');
$task = Db::name('dmtask')->where('id', $id)->find();
if(empty($task)) return $this->alert('error', '切换策略不存在');
$switch_count = Db::name('dmlog')->where('taskid', $id)->where('date', '>=', date("Y-m-d H:i:s",strtotime("-1 days")))->count();
$fail_count = Db::name('dmlog')->where('taskid', $id)->where('date', '>=', date("Y-m-d H:i:s",strtotime("-1 days")))->where('action', 1)->count();
$task['switch_count'] = $switch_count;
$task['fail_count'] = $fail_count;
if($task['type'] == 3){
$task['action_name'] = ['未知', '<font color="red">开启解析</font>', '<font color="green">暂停解析</font>'];
}elseif($task['type'] == 2){
$task['action_name'] = ['未知', '<font color="red">切换备用解析记录</font>', '<font color="green">恢复主解析记录</font>'];
}else{
$task['action_name'] = ['未知', '<font color="red">暂停解析</font>', '<font color="green">启用解析</font>'];
}
View::assign('info', $task);
return View::fetch();
}
public function tasklog_data(){
if(!checkPermission(2)) return json(['total'=>0, 'rows'=>[]]);
$taskid = input('param.id/d');
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$action = input('post.action/d', 0);
$select = Db::name('dmlog')->where('taskid', $taskid);
if($action > 0){
$select->where('action', $action);
}
$total = $select->count();
$list = $select->order('id','desc')->limit($offset, $limit)->select();
return json(['total'=>$total, 'rows'=>$list]);
}
public function noticeset()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
if(request()->isPost()){
$params = input('post.');
if(isset($params['mail_type']) && isset($params['mail_name2']) && $params['mail_type'] > 0){
$params['mail_name'] = $params['mail_name2'];
unset($params['mail_name2']);
}
foreach ($params as $key=>$value){
if (empty($key)) {
continue;
}
config_set($key, $value);
Cache::delete('configs');
}
return json(['code'=>0, 'msg'=>'succ']);
}
return View::fetch();
}
public function mailtest()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
$mail_name = config_get('mail_recv')?config_get('mail_recv'):config_get('mail_name');
if(empty($mail_name)) return json(['code'=>-1, 'msg'=>'您还未设置邮箱!']);
$result = \app\lib\MsgNotice::send_mail($mail_name,'邮件发送测试。','这是一封测试邮件!<br/><br/>来自:'.request()->root(true));
if($result === true){
return json(['code'=>0, 'msg'=>'邮件发送成功!']);
}else{
return json(['code'=>-1, 'msg'=>'邮件发送失败!'.$result]);
}
}
public function clean()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
if(request()->isPost()){
$days = input('post.days/d');
if(!$days || $days < 0) return json(['code'=>-1, 'msg'=>'参数错误']);
Db::execute("DELETE FROM `".config('database.connections.mysql.prefix')."dmlog` WHERE `date`<'".date("Y-m-d H:i:s",strtotime("-".$days." days"))."'");
Db::execute("OPTIMIZE TABLE `".config('database.connections.mysql.prefix')."dmlog`");
return json(['code'=>0, 'msg'=>'清理成功']);
}
}
public function status()
{
$run_time = config_get('run_time', null, true);
$run_state = $run_time ? (time()-strtotime($run_time) > 10 ? 0 : 1) : 0;
return $run_state == 1 ? 'ok' : 'error';
}
}

View File

@@ -219,6 +219,8 @@ class Domain extends BaseController
if(!checkPermission(2)) return $this->alert('error', '无权限');
$id = input('post.id/d');
Db::name('domain')->where('id', $id)->delete();
Db::name('dmtask')->where('did', $id)->delete();
Db::name('optimizeip')->where('did', $id)->delete();
return json(['code'=>0]);
}
return json(['code'=>-3]);
@@ -231,9 +233,16 @@ class Domain extends BaseController
$page = input('?post.page') ? input('post.page/d') : 1;
$pagesize = input('?post.pagesize') ? input('post.pagesize/d') : 10;
$dns = DnsHelper::getModel($aid);
$list = $dns->getDomainList($kw, $page, $pagesize);
if(!$list) return json(['code'=>-1, 'msg'=>'获取域名列表失败,'.$dns->getError()]);
return json(['code'=>0, 'data'=>$list]);
$result = $dns->getDomainList($kw, $page, $pagesize);
if(!$result) return json(['code'=>-1, 'msg'=>'获取域名列表失败,'.$dns->getError()]);
$newlist = [];
foreach($result['list'] as $row){
if(!Db::name('domain')->where('aid', $aid)->where('name', $row['Domain'])->find()){
$newlist[] = $row;
}
}
return json(['code'=>0, 'data'=>['total'=>$result['total'], 'list'=>$newlist]]);
}
//获取解析线路和最小TTL
@@ -243,10 +252,10 @@ class Domain extends BaseController
if(empty($recordLine)){
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
if(!$dns) return $this->alert('error', 'DNS模块不存在');
$recordLine = $dns->getRecordLine($drow['name']);
$recordLine = $dns->getRecordLine();
if(!$recordLine) return $this->alert('error', '获取解析线路列表失败,'.$dns->getError());
cache('record_line_'.$drow['id'], $recordLine, 604800);
$minTTL = $dns->getMinTTL($drow['name']);
$minTTL = $dns->getMinTTL();
if($minTTL){
cache('min_ttl_'.$drow['id'], $minTTL, 604800);
}
@@ -267,7 +276,7 @@ class Domain extends BaseController
$recordLineArr = [];
foreach($recordLine as $key=>$item){
$recordLineArr[] = ['id'=>$key, 'name'=>$item['name'], 'parent'=>$item['parent']];
$recordLineArr[] = ['id'=>strval($key), 'name'=>$item['name'], 'parent'=>$item['parent']];
}
$dnsconfig = DnsHelper::$dns_config[$dnstype];
@@ -300,7 +309,7 @@ class Domain extends BaseController
$recordLineArr = [];
foreach($recordLine as $key=>$item){
$recordLineArr[] = ['id'=>$key, 'name'=>$item['name'], 'parent'=>$item['parent']];
$recordLineArr[] = ['id'=>strval($key), 'name'=>$item['name'], 'parent'=>$item['parent']];
}
$dnsconfig = DnsHelper::$dns_config[$dnstype];
@@ -345,13 +354,35 @@ class Domain extends BaseController
$recordLine = cache('record_line_'.$id);
$list = [];
foreach($domainRecords['list'] as $row){
foreach($domainRecords['list'] as &$row){
$row['LineName'] = isset($recordLine[$row['Line']]) ? $recordLine[$row['Line']]['name'] : $row['Line'];
$list[] = $row;
}
return json(['total'=>$domainRecords['total'], 'rows'=>$list]);
return json(['total'=>$domainRecords['total'], 'rows'=>$domainRecords['list']]);
}
public function record_list(){
if(!checkPermission(2)) return $this->alert('error', '无权限');
$id = input('post.id/d');
$rr = input('post.rr', null, 'trim');
$drow = Db::name('domain')->where('id', $id)->find();
if(!$drow){
return json(['code'=>-1, 'msg'=>'域名不存在']);
}
if(!checkPermission(0, $drow['name'])) return json(['code'=>-1, 'msg'=>'无权限']);
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
$domainRecords = $dns->getSubDomainRecords($rr, 1, 100);
if(!$domainRecords) return json(['code'=>-1, 'msg'=>'获取记录列表失败,'.$dns->getError()]);
list($recordLine, $minTTL) = $this->get_line_and_ttl($drow);
foreach($domainRecords['list'] as &$row){
$row['LineName'] = isset($recordLine[$row['Line']]) ? $recordLine[$row['Line']]['name'] : $row['Line'];
}
return json(['code'=>0, 'data'=>$domainRecords['list']]);
}
public function record_add(){

View File

@@ -32,6 +32,12 @@ class Index extends BaseController
return json(['code'=>-3]);
}
if(config('app.dbversion') && config_get('version') != config('app.dbversion')){
$this->db_update();
config_set('version', config('app.dbversion'));
Cache::clear();
}
$tmp = 'version()';
$mysqlVersion = Db::query("select version()")[0][$tmp];
$info = [
@@ -47,13 +53,26 @@ class Index extends BaseController
return view();
}
private function db_update(){
$sqls=file_get_contents(app()->getAppPath().'sql/update.sql');
$mysql_prefix = env('database.prefix', 'dnsmgr_');
$sqls=explode(';', $sqls);
foreach ($sqls as $value) {
$value=trim($value);
if(empty($value))continue;
$value = str_replace('dnsmgr_',$mysql_prefix,$value);
Db::execute($value);
}
}
public function changeskin(){
$skin = input('post.skin');
if(request()->user['level'] == 2){
if(cookie('admin_skin')){
cookie('admin_skin', null);
}
cache('admin_skin', $skin);
config_set('admin_skin', $skin);
Cache::delete('configs');
}else{
cookie('admin_skin', $skin);
}
@@ -93,4 +112,7 @@ class Index extends BaseController
return view();
}
public function test(){
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace app\controller;
use app\BaseController;
use Exception;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
use app\lib\OptimizeService;
class Optimizeip extends BaseController
{
public function opipset()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
if(request()->isPost()){
$params = input('post.');
foreach ($params as $key=>$value){
if (empty($key)) {
continue;
}
config_set($key, $value);
Cache::delete('configs');
}
return json(['code'=>0, 'msg'=>'succ']);
}
return View::fetch();
}
public function opiplist()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
return View::fetch();
}
public function opiplist_data(){
if(!checkPermission(2)) return json(['total'=>0, 'rows'=>[]]);
$type = input('post.type/d', 1);
$kw = input('post.kw', null, 'trim');
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$select = Db::name('optimizeip')->alias('A')->join('domain B','A.did = B.id');
if(!empty($kw)){
if($type == 1){
$select->whereLike('rr|B.name', '%'.$kw.'%');
}elseif($type == 2){
$select->whereLike('remark', '%'.$kw.'%');
}
}
$total = $select->count();
$list = $select->order('A.id','desc')->limit($offset, $limit)->field('A.*,B.name domain')->select();
return json(['total'=>$total, 'rows'=>$list]);
}
public function opipform()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
$action = input('param.action');
if(request()->isPost()){
if($action == 'add'){
$task = [
'did' => input('post.did/d'),
'rr' => input('post.rr', null, 'trim'),
'type' => input('post.type/d'),
'ip_type' => input('post.ip_type', null, 'trim'),
'cdn_type' => input('post.cdn_type/d'),
'recordnum' => input('post.recordnum/d'),
'ttl' => input('post.ttl/d'),
'remark' => input('post.remark', null, 'trim'),
'addtime' => time(),
'active' => 1
];
if(empty($task['did']) || empty($task['rr']) || empty($task['ip_type']) || empty($task['recordnum']) || empty($task['ttl'])){
return json(['code'=>-1, 'msg'=>'必填项不能为空']);
}
if($task['recordnum'] > 5){
return json(['code'=>-1, 'msg'=>'解析数量不能超过5个']);
}
if(Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->find()){
return json(['code'=>-1, 'msg'=>'当前域名的优选IP任务已存在']);
}
Db::name('optimizeip')->insert($task);
return json(['code'=>0, 'msg'=>'添加成功']);
}elseif($action == 'edit'){
$id = input('post.id/d');
$task = [
'did' => input('post.did/d'),
'rr' => input('post.rr', null, 'trim'),
'type' => input('post.type/d'),
'ip_type' => input('post.ip_type', null, 'trim'),
'cdn_type' => input('post.cdn_type/d'),
'recordnum' => input('post.recordnum/d'),
'ttl' => input('post.ttl/d'),
'remark' => input('post.remark', null, 'trim'),
];
if(empty($task['did']) || empty($task['rr']) || empty($task['ip_type']) || empty($task['recordnum']) || empty($task['ttl'])){
return json(['code'=>-1, 'msg'=>'必填项不能为空']);
}
if($task['recordnum'] > 5){
return json(['code'=>-1, 'msg'=>'解析数量不能超过5个']);
}
if(Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->where('id', '<>', $id)->find()){
return json(['code'=>-1, 'msg'=>'当前域名的优选IP任务已存在']);
}
Db::name('optimizeip')->where('id', $id)->update($task);
return json(['code'=>0, 'msg'=>'修改成功']);
}elseif($action == 'setactive'){
$id = input('post.id/d');
$active = input('post.active/d');
Db::name('optimizeip')->where('id', $id)->update(['active'=>$active]);
return json(['code'=>0, 'msg'=>'设置成功']);
}elseif($action == 'del'){
$id = input('post.id/d');
Db::name('optimizeip')->where('id', $id)->delete();
return json(['code'=>0, 'msg'=>'删除成功']);
}elseif($action == 'run'){
$id = input('post.id/d');
$task = Db::name('optimizeip')->where('id', $id)->find();
if(empty($task)) return json(['code'=>-1, 'msg'=>'任务不存在']);
try{
$result = (new OptimizeService())->execute_one($task);
Db::name('optimizeip')->where('id', $id)->update(['status' => 1, 'errmsg' => null, 'updatetime' => date('Y-m-d H:i:s')]);
return json(['code'=>0, 'msg'=>'优选任务执行成功:'.$result]);
}catch(Exception $e){
Db::name('optimizeip')->where('id', $id)->update(['status' => 2, 'errmsg' => $e->getMessage(), 'updatetime' => date('Y-m-d H:i:s')]);
return json(['code'=>-1, 'msg'=>'优选任务执行失败:'.$e->getMessage(), 'stack'=>$e->__toString()]);
}
}else{
return json(['code'=>-1, 'msg'=>'参数错误']);
}
}
$task = null;
if($action == 'edit'){
$id = input('get.id/d');
$task = Db::name('optimizeip')->where('id', $id)->find();
if(empty($task)) return $this->alert('error', '任务不存在');
}
$domains = [];
foreach(Db::name('domain')->alias('A')->join('account B','A.aid = B.id')->field('A.*')->where('B.type', '<>', 'cloudflare')->select() as $row){
$domains[$row['id']] = $row['name'];
}
View::assign('domains', $domains);
View::assign('info', $task);
View::assign('action', $action);
return View::fetch();
}
public function queryapi()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
$optimize_ip_api = input('post.optimize_ip_api/d');
$optimize_ip_key = input('post.optimize_ip_key', null, 'trim');
if(empty($optimize_ip_key)) return json(['code'=>-1, 'msg'=>'参数不能为空']);
try{
$result = (new OptimizeService())->get_license($optimize_ip_api, $optimize_ip_key);
return json(['code'=>0, 'msg'=>'当前积分余额:'.$result]);
}catch(Exception $e){
return json(['code'=>-1, 'msg'=>$e->getMessage()]);
}
}
public function status()
{
$run_time = Db::name('optimizeip')->where('active', 1)->order('updatetime', 'desc')->value('updatetime');
$run_state = $run_time ? (time()-strtotime($run_time) > 3600 ? 0 : 1) : 0;
return $run_state == 1 ? 'ok' : 'error';
}
}

90
app/lib/CheckUtils.php Normal file
View File

@@ -0,0 +1,90 @@
<?php
namespace app\lib;
class CheckUtils
{
public static function curl($url, $timeout, $ip = null)
{
$status = true;
$errmsg = null;
$urlarr = parse_url($url);
if (!empty($ip) && !filter_var($urlarr['host'], FILTER_VALIDATE_IP)) {
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
$ip = gethostbyname($ip);
}
if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP)) {
$port = isset($urlarr['port']) ? $urlarr['port'] : ($urlarr['scheme'] == 'https' ? 443 : 80);
$resolve = $urlarr['host'] . ':' . $port . ':' . $ip;
}
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$httpheader[] = "Accept: */*";
$httpheader[] = "Accept-Language: zh-CN,zh;q=0.8";
$httpheader[] = "Connection: close";
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
if(!empty($resolve)){
curl_setopt($ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false);
curl_setopt($ch, CURLOPT_RESOLVE, [$resolve]);
}
curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$status = false;
$errmsg = curl_error($ch);
}
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($status && ($httpcode < 200 || $httpcode >= 400)){
$status = false;
$errmsg = 'http_code='.$httpcode;
}
$usetime = round(curl_getinfo($ch, CURLINFO_TOTAL_TIME) * 1000);
curl_close($ch);
return ['status'=>$status, 'errmsg'=>$errmsg, 'usetime'=>$usetime];
}
public static function tcp($target, $port, $timeout){
if(!filter_var($target,FILTER_VALIDATE_IP) && checkDomain($target)){
$target = gethostbyname($target);
if(!$target)return ['status'=>false, 'error'=>'DNS resolve failed', 'usetime'=>0];
}
$starttime = getMillisecond();
$fp = @fsockopen($target, $port, $errCode, $errStr, $timeout);
if ($fp) {
$status = true;
fclose($fp);
} else {
$status = false;
}
$endtime = getMillisecond();
$usetime = $endtime-$starttime;
return ['status'=>$status, 'errmsg'=>$errStr, 'usetime'=>$usetime];
}
public static function ping($target){
if(!function_exists('exec'))return ['status'=>false, 'error'=>'exec函数不可用', 'usetime'=>0];
if(!filter_var($target,FILTER_VALIDATE_IP) && checkDomain($target)){
$target = gethostbyname($target);
if(!$target)return ['status'=>false, 'error'=>'DNS resolve failed', 'usetime'=>0];
}
if(!filter_var($target,FILTER_VALIDATE_IP)){
return ['status'=>false, 'error'=>'Invalid IP address', 'usetime'=>0];
}
$timeout = 1;
exec('ping -c 1 -w '.$timeout.' '.$target.'', $output, $return_var);
$usetime = !empty($output[1]) ? round(getSubstr($output[1], 'time=', ' ms')) : 0;
$errmsg = null;
if($return_var !== 0){
$usetime = $usetime == 0 ? $timeout*1000 : $usetime;
$errmsg = 'ping timeout';
}
return ['status'=>$return_var===0, 'errmsg'=>$errmsg, 'usetime'=>$usetime];
}
}

View File

@@ -46,10 +46,21 @@ class DnsHelper
'sk' => 'API密码'
],
'remark' => 0,
'status' => false,
'status' => true,
'redirect' => false,
'log' => false,
],
'dnsla' => [
'name' => 'DNSLA',
'config' => [
'ak' => 'APIID',
'sk' => 'API密钥'
],
'remark' => 0,
'status' => true,
'redirect' => true,
'log' => false,
],
'cloudflare' => [
'name' => 'Cloudflare',
'config' => [
@@ -88,4 +99,17 @@ class DnsHelper
}
return false;
}
public static function getModel2($config)
{
$dnstype = $config['type'];
$class = "\\app\\lib\\dns\\{$dnstype}";
if(class_exists($class)){
$config['domain'] = $config['name'];
$config['domainid'] = $config['thirdid'];
$model = new $class($config);
return $model;
}
return false;
}
}

105
app/lib/MsgNotice.php Normal file
View File

@@ -0,0 +1,105 @@
<?php
namespace app\lib;
class MsgNotice
{
private static $sitename = '聚合DNS管理系统';
public static function send($action, $task, $result)
{
if($action == 1){
$mail_title = 'DNS容灾切换-发生告警通知';
$mail_content = '尊敬的系统管理员,您好:<br/>您的域名 <b>'.$task['domain'].'</b> 的 <b>'.$task['main_value'].'</b> 记录发生了异常';
if($task['type'] == 2){
$mail_content .= ',已自动切换为备用解析记录 '.$task['backup_value'].' ';
}elseif($task['type'] == 1){
$mail_content .= ',已自动暂停解析';
}else{
$mail_content .= ',请及时处理';
}
if(!empty($result['errmsg'])){
$mail_content .= '。<br/>异常信息:'.$result['errmsg'];
}
}else{
$mail_title = 'DNS容灾切换-恢复正常通知';
$mail_content = '尊敬的系统管理员,您好:<br/>您的域名 <b>'.$task['domain'].'</b> 的 <b>'.$task['main_value'].'</b> 记录已恢复正常';
if($task['type'] == 2){
$mail_content .= ',已自动切换回当前解析记录';
}elseif($task['type'] == 1){
$mail_content .= ',已自动开启解析';
}
$lasttime = convert_second(time() - $task['switchtime']);
$mail_content .= '。<br/>异常持续时间:'.$lasttime;
}
if(!empty($task['remark'])) $mail_title .= '('.$task['remark'].')';
if(!empty($task['remark'])) $mail_content .= '<br/>备注:'.$task['remark'];
$mail_content .= '<br/>'.self::$sitename.'<br/>'.date('Y-m-d H:i:s');
if(config_get('notice_mail') == 1){
$mail_name = config_get('mail_recv')?config_get('mail_recv'):config_get('mail_name');
self::send_mail($mail_name, $mail_title, $mail_content);
}
if(config_get('notice_wxtpl') == 1){
$mail_content = str_replace(['<br/>', '<b>', '</b>'], ["\n\n", '**', '**'], $mail_content);
self::send_wechat_tplmsg($mail_title, $mail_content);
}
}
public static function send_mail($to, $sub, $msg){
$mail_type = config_get('mail_type');
if($mail_type == 1){
$mail = new \app\lib\mail\Sendcloud(config_get('mail_apiuser'), config_get('mail_apikey'));
return $mail->send($to, $sub, $msg, config_get('mail_name'), self::$sitename);
}elseif($mail_type == 2){
$mail = new \app\lib\mail\Aliyun(config_get('mail_apiuser'), config_get('mail_apikey'));
return $mail->send($to, $sub, $msg, config_get('mail_name'), self::$sitename);
}else{
$mail_name = config_get('mail_name');
$mail_port = intval(config_get('mail_port'));
$mail_smtp = config_get('mail_smtp');
$mail_pwd = config_get('mail_pwd');
if(!$mail_name || !$mail_port || !$mail_smtp || !$mail_pwd)return false;
$mail = new \app\lib\mail\PHPMailer\PHPMailer(true);
try{
$mail->SMTPDebug = 0;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 5;
$mail->isSMTP();
$mail->Host = $mail_smtp;
$mail->SMTPAuth = true;
$mail->Username = $mail_name;
$mail->Password = $mail_pwd;
if($mail_port == 587) $mail->SMTPSecure = 'tls';
else if($mail_port >= 465) $mail->SMTPSecure = 'ssl';
else $mail->SMTPAutoTLS = false;
$mail->Port = $mail_port;
$mail->setFrom($mail_name, self::$sitename);
$mail->addAddress($to);
$mail->addReplyTo($mail_name, self::$sitename);
$mail->isHTML(true);
$mail->Subject = $sub;
$mail->Body = $msg;
$mail->send();
return true;
} catch (\Exception $e) {
return $mail->ErrorInfo;
}
}
}
public static function send_wechat_tplmsg($title, $content){
$wechat_apptoken = config_get('wechat_apptoken');
$wechat_appuid = config_get('wechat_appuid');
if(!$wechat_apptoken||!$wechat_appuid)return false;
$url = 'https://wxpusher.zjiecode.com/api/send/message';
$post = ['appToken'=>$wechat_apptoken, 'content'=>$content, 'summary'=>$title, 'contentType'=>3, 'uids'=>[$wechat_appuid]];
$result = get_curl($url, json_encode($post),0,0,0,0,0,['Content-Type: application/json; charset=UTF-8']);
$arr = json_decode($result, true);
if(isset($arr['success']) && $arr['success']==true){
return true;
}else{
return $arr['msg'];
}
}
}

17
app/lib/NewDb.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
namespace app\lib;
use think\Facade;
class NewDb extends Facade
{
/**
* 获取当前Facade对应类名或者已经绑定的容器对象标识
* @access protected
* @return string
*/
protected static function getFacadeClass()
{
return 'app\lib\NewDbManager';
}
}

25
app/lib/NewDbManager.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
declare (strict_types = 1);
namespace app\lib;
use think\db\ConnectionInterface;
class NewDbManager extends \think\Db
{
/**
* 创建数据库连接实例
* @access protected
* @param string|null $name 连接标识
* @param bool $force 强制重新连接
* @return ConnectionInterface
*/
protected function instance(string $name = null, bool $force = false): ConnectionInterface
{
if (empty($name)) {
$name = $this->getConfig('default', 'mysql');
}
return $this->createConnection($name);
}
}

204
app/lib/OptimizeService.php Normal file
View File

@@ -0,0 +1,204 @@
<?php
namespace app\lib;
use Exception;
use think\facade\Db;
class OptimizeService
{
private static $line_name = [
'aliyun' => ['DEF'=>'default', 'CT'=>'telecom', 'CU'=>'unicom', 'CM'=>'mobile', 'AB'=>'oversea'],
'dnspod' => ['DEF'=>'0', 'CT'=>'10=0', 'CU'=>'10=1', 'CM'=>'10=3', 'AB'=>'3=0'],
'huawei' => ['DEF'=>'default_view', 'CT'=>'Dianxin', 'CU'=>'Liantong', 'CM'=>'Yidong', 'AB'=>'Abroad'],
'west' => ['DEF'=>'', 'CT'=>'LTEL', 'CU'=>'LCNC', 'CM'=>'LMOB', 'AB'=>'LFOR'],
'dnsla' => ['DEF'=>'', 'CT'=>'84613316902921216', 'CU'=>'84613316923892736', 'CM'=>'84613316953252864', 'AB'=>''],
];
private $ip_address = [];
private $add_num = 0;
private $change_num = 0;
private $del_num = 0;
public static function get_license($api, $key){
if($api == 1){
$url = 'https://api.hostmonit.com/get_license?license='.$key;
}else{
$url = 'https://monitor.gacjie.cn/api/client/get_account_integral?license='.$key;
}
$response = get_curl($url);
$arr = json_decode($response, true);
if(isset($arr['code']) && $arr['code'] == 200 && isset($arr['count'])){
return $arr['count'];
}elseif(isset($arr['info'])){
throw new Exception('获取剩余请求次数失败,'.$arr['info']);
}else{
throw new Exception('获取剩余请求次数失败');
}
}
public function get_ip_address($cdn_type = 1, $ip_type = 'v4'){
$api = config_get('optimize_ip_api', 0);
if($api == 1){
$url = 'https://api.hostmonit.com/get_optimization_ip';
}else{
$url = 'https://monitor.gacjie.cn/api/client/get_ip_address';
}
$params = [
'key' => config_get('optimize_ip_key', 'o1zrmHAF'),
'type' => $ip_type,
];
if($api == 0){
$params['cdn_server'] = $cdn_type;
}
$response = get_curl($url, json_encode($params), 0, 0, 0, 0, 0, ['Content-Type: application/json; charset=UTF-8']);
$arr = json_decode($response, true);
if(isset($arr['code']) && $arr['code'] == 200){
return $arr['info'];
}elseif(isset($arr['info'])){
throw new Exception('获取优选IP数据失败'.$arr['info']);
}elseif(isset($arr['msg'])){
throw new Exception('获取优选IP数据失败'.$arr['msg']);
}else{
throw new Exception('获取优选IP数据失败原因未知');
}
}
public function get_ip_address2($cdn_type = 1, $ip_type = 'v4'){
$key = $cdn_type.'_'.$ip_type;
if(!isset($this->ip_address[$key])){
$info = $this->get_ip_address($cdn_type, $ip_type);
$res = [];
if(isset($info['DEF'])) $res['DEF'] = $info['DEF'];
if(isset($info['CT'])) $res['CT'] = $info['CT'];
if(isset($info['CU'])) $res['CU'] = $info['CU'];
if(isset($info['CM'])) $res['CM'] = $info['CM'];
$this->ip_address[$key] = $res;
}
return $this->ip_address[$key];
}
//批量执行优选任务
public function execute(){
$list = Db::name('optimizeip')->where('active', 1)->select();
echo '开始执行IP优选任务共获取到'.count($list).'个待执行任务'."\n";
foreach($list as $row){
try{
$result = $this->execute_one($row);
Db::name('optimizeip')->where('id', $row['id'])->update(['status' => 1, 'errmsg' => null, 'updatetime' => date('Y-m-d H:i:s')]);
echo '优选任务'.$row['id'].'执行成功:'.$result."\n";
}catch(Exception $e){
Db::name('optimizeip')->where('id', $row['id'])->update(['status' => 2, 'errmsg' => $e->getMessage(), 'updatetime' => date('Y-m-d H:i:s')]);
echo '优选任务'.$row['id'].'执行失败:'.$e->getMessage()."\n";
}
}
}
//执行单个优选任务
public function execute_one($row){
$this->add_num = 0;
$this->change_num = 0;
$this->del_num = 0;
$ip_types = explode(',', $row['ip_type']);
foreach($ip_types as $ip_type){
if(empty($ip_type)) continue;
$drow = Db::name('domain')->alias('A')->join('account B','A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.ak,B.sk,B.ext')->find();
if(!$drow){
throw new Exception('域名不存在ID'.$row['did'].'');
}
if(!isset(self::$line_name[$drow['type']])){
throw new Exception('不支持的DNS服务商');
}
$info = $this->get_ip_address2($row['cdn_type'], $ip_type);
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
$domainRecords = $dns->getSubDomainRecords($row['rr'], 1, 100);
if(!$domainRecords){
throw new Exception('获取记录列表失败,'.$dns->getError());
}
if($row['type'] == 1 && isset($info['DEF']) && !empty($info['DEF'])) $row['type'] = 0;
foreach($info as $line=>$iplist){
if(empty($iplist)) continue;
$get_ips = array_column($iplist, 'ip');
if($drow['type']=='huawei') {sort($get_ips); $get_ips = [implode(',',$get_ips)]; $row['recordnum'] = 1;}
if($row['type'] == 1 && $line == 'CT') $line = 'DEF';
$line_name = self::$line_name[$drow['type']][$line];
$this->process_dns_line($dns, $row, $domainRecords['list'], $get_ips, $line_name, $ip_type);
}
}
return '成功添加'.$this->add_num.'条记录,修改'.$this->change_num.'条记录,删除'.$this->del_num.'条记录';
}
//处理单个线路的解析记录
private function process_dns_line($dns, $row, $record_list, $get_ips, $line_name, $ip_type){
$record_num = $row['recordnum'];
$records = array_filter($record_list, function($v) use($line_name){
return $v['Line'] == $line_name;
});
//删除CNAME记录
$cname_records = array_filter($records, function($v){
return $v['Type'] == 'CNAME';
});
if(!empty($cname_records)){
foreach($cname_records as $record){
$dns->deleteDomainRecord($record['RecordId']);
}
}
//处理A/AAAA记录
$ip_records = array_filter($records, function($v) use ($ip_type){
return $v['Type'] == ($ip_type == 'v6' ? 'AAAA' : 'A');
});
if(!empty($ip_records) && is_array($ip_records[array_key_first($ip_records)]['Value'])){ //处理华为云记录
foreach($ip_records as &$ip_record){
sort($ip_record['Value']);
$ip_record['Value'] = implode(',', $ip_record['Value']);
}
}
$exist_ips = array_column($ip_records, 'Value');
$add_ips = array_diff($get_ips, $exist_ips);
$del_ips = array_diff($exist_ips, $get_ips);
$correct_ips = array_diff($exist_ips, $del_ips);
$correct_count = count($correct_ips);
if(!empty($del_ips)){
foreach($ip_records as $record){
if(in_array($record['Value'], $del_ips)){
$add_ip = array_pop($add_ips);
if($add_ip){
$res = $dns->updateDomainRecord($record['RecordId'], $row['rr'], $ip_type == 'v6' ? 'AAAA' : 'A', $add_ip, $line_name, $row['ttl']);
if(!$res){
throw new Exception('修改解析失败,'.$dns->getError());
}
$this->change_num++;
$correct_count++;
}else{
$res = $dns->deleteDomainRecord($record['RecordId']);
if(!$res){
throw new Exception('删除解析失败,'.$dns->getError());
}
$this->del_num++;
}
}
}
}
if($correct_count < $record_num && !empty($add_ips)){
foreach($add_ips as $add_ip){
$res = $dns->addDomainRecord($row['rr'], $ip_type == 'v6' ? 'AAAA' : 'A', $add_ip, $line_name, $row['ttl']);
if(!$res){
throw new Exception('添加解析失败,'.$dns->getError());
}
$this->add_num++;
$correct_count++;
if($correct_count >= $record_num) break;
}
}
}
}

125
app/lib/TaskRunner.php Normal file
View File

@@ -0,0 +1,125 @@
<?php
namespace app\lib;
use app\lib\NewDb;
use app\lib\CheckUtils;
use app\lib\DnsHelper;
use app\lib\MsgNotice;
class TaskRunner
{
private $conn;
private function db()
{
if(!$this->conn){
$this->conn = NewDb::connect();
}
return $this->conn;
}
private function closeDb()
{
if($this->conn){
$this->conn->close();
}
}
public function execute($row)
{
if($row['type'] == 3){ //条件开启解析
$action = 0;
$remain = $this->db()->name('dmtask')->where(['did'=>$row['did'], 'rr'=>$row['rr'], 'type'=>1, 'status'=>0])->count();
if($remain<=$row['cycle'] && $row['status']==0){
$action = 2;
$this->db()->name('dmtask')->where('id', $row['id'])->update(['status'=>1, 'errcount'=>0, 'switchtime'=>time()]);
}elseif($remain>$row['cycle'] && $row['status']==1){
$action = 1;
$this->db()->name('dmtask')->where('id', $row['id'])->update(['status'=>0, 'errcount'=>0, 'switchtime'=>time()]);
}
}else{
if($row['checktype'] == 2){
$result = CheckUtils::curl($row['checkurl'], $row['timeout'], $row['main_value']);
}else if($row['checktype'] == 1){
$result = CheckUtils::tcp($row['main_value'], $row['tcpport'], $row['timeout']);
}else{
$result = CheckUtils::ping($row['main_value']);
}
$action = 0;
if($result['status'] && $row['status']==1){
if($row['cycle'] <= 1 || $row['errcount'] >= $row['cycle']){
$this->db()->name('dmtask')->where('id', $row['id'])->update(['status'=>0, 'errcount'=>0, 'switchtime'=>time()]);
$action = 2;
}else{
$this->db()->name('dmtask')->where('id', $row['id'])->inc('errcount')->update();
}
}elseif(!$result['status'] && $row['status']==0){
if($row['cycle'] <= 1 || $row['errcount'] >= $row['cycle']){
$this->db()->name('dmtask')->where('id', $row['id'])->update(['status'=>1, 'errcount'=>0, 'switchtime'=>time()]);
$action = 1;
}else{
$this->db()->name('dmtask')->where('id', $row['id'])->inc('errcount')->update();
}
}elseif($row['errcount'] > 0){
$this->db()->name('dmtask')->where('id', $row['id'])->update(['errcount'=>0]);
}
}
if($action > 0){
$drow = $this->db()->name('domain')->alias('A')->join('account B','A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.ak,B.sk,B.ext')->find();
if(!$drow){
echo '域名不存在ID'.$row['did'].''."\n";
$this->closeDb();
return;
}
$row['domain'] = $row['rr'] . '.' . $drow['name'];
}
if($action == 1){
if($row['type'] == 2){
$dns = DnsHelper::getModel2($drow);
$recordinfo = json_decode($row['recordinfo'], true);
$res = $dns->updateDomainRecord($row['recordid'], $row['rr'], getDnsType($row['backup_value']), $row['backup_value'], $recordinfo['Line'], $recordinfo['TTL']);
if(!$res){
$this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '修改解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]);
}
}elseif($row['type'] == 1 || $row['type'] == 3){
$dns = DnsHelper::getModel2($drow);
$res = $dns->setDomainRecordStatus($row['recordid'], '0');
if(!$res){
$this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '暂停解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]);
}
}
}elseif($action == 2){
if($row['type'] == 2){
$dns = DnsHelper::getModel2($drow);
$recordinfo = json_decode($row['recordinfo'], true);
$res = $dns->updateDomainRecord($row['recordid'], $row['rr'], getDnsType($row['main_value']), $row['main_value'], $recordinfo['Line'], $recordinfo['TTL']);
if(!$res){
$this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '修改解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]);
}
}elseif($row['type'] == 1 || $row['type'] == 3){
$dns = DnsHelper::getModel2($drow);
$res = $dns->setDomainRecordStatus($row['recordid'], '1');
if(!$res){
$this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '启用解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]);
}
}
}else{
$this->closeDb();
return;
}
$this->db()->name('dmlog')->insert([
'taskid' => $row['id'],
'action' => $action,
'errmsg' => isset($result) ? ($result['status'] ? null : $result['errmsg']) : null,
'date' => date('Y-m-d H:i:s')
]);
$this->closeDb();
if($row['type'] != 3){
MsgNotice::send($action, $row, $result);
}
}
}

View File

@@ -86,7 +86,7 @@ class aliyun implements DnsInterface {
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){
$param = ['Action' => 'DescribeSubDomainRecords', 'SubDomain' => $SubDomain, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'Type' => $Type, 'Line' => $Line];
$param = ['Action' => 'DescribeSubDomainRecords', 'SubDomain' => $SubDomain . '.' . $this->domain, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'Type' => $Type, 'Line' => $Line];
$data = $this->request($param, true);
if($data){
$list = [];

View File

@@ -7,8 +7,6 @@ class baidu implements DnsInterface {
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint = "dns.baidubce.com";
private $service = "dnspod";
private $version = "2021-03-23";
private $error;
private $domain;
private $domainid;
@@ -82,11 +80,8 @@ class baidu implements DnsInterface {
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){
$domain_arr = explode('.', $SubDomain);
$domain = $domain_arr[count($domain_arr)-2].'.'.$domain_arr[count($domain_arr)-1];
$subdomain = rtrim(str_replace($domain,'',$SubDomain),'.');
if($subdomain == '')$subdomain='@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $subdomain, $Type, $Line);
if($SubDomain == '')$SubDomain='@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line);
}
//获取解析记录详细信息

View File

@@ -57,10 +57,11 @@ class cloudflare implements DnsInterface {
if($data){
$list = [];
foreach($data['result'] as $row){
$name = $row['zone_name'] == $row['name'] ? '@' : str_replace('.'.$row['zone_name'], '', $row['name']);
$list[] = [
'RecordId' => $row['id'],
'Domain' => $row['zone_name'],
'Name' => str_replace('.'.$row['zone_name'], '', $row['name']),
'Name' => $name,
'Type' => $row['type'],
'Value' => $row['content'],
'Line' => $row['proxied'] ? '1' : '0',
@@ -79,17 +80,16 @@ class cloudflare implements DnsInterface {
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){
$domain_arr = explode('.', $SubDomain);
$domain = $domain_arr[count($domain_arr)-2].'.'.$domain_arr[count($domain_arr)-1];
$subdomain = rtrim(str_replace($domain,'',$SubDomain),'.');
if($subdomain == '')$subdomain='@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $subdomain, $Type, $Line);
if($SubDomain == '@')$SubDomain=$this->domain;
else $SubDomain .= '.'.$this->domain;
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line);
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId){
$data = $this->send_reuqest('GET', '/zones/'.$this->domainid.'/dns_records/'.$RecordId);
if($data){
$name = $data['result']['zone_name'] == $data['result']['name'] ? '@' : str_replace('.'.$data['result']['zone_name'], '', $data['result']['name']);
return [
'RecordId' => $data['result']['id'],
'Domain' => $data['result']['zone_name'],

251
app/lib/dns/dnsla.php Normal file
View File

@@ -0,0 +1,251 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
class dnsla implements DnsInterface {
private $apiid;
private $apisecret;
private $baseUrl = 'https://api.dns.la';
private $typeList = [1 => 'A', 2 => 'NS', 5 => 'CNAME', 15 => 'MX', 16 => 'TXT', 28 => 'AAAA', 33 => 'SRV', 257 => 'CAA', 256 => 'URL转发'];
private $error;
private $domain;
private $domainid;
function __construct($config){
$this->apiid = $config['ak'];
$this->apisecret = $config['sk'];
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
}
public function getError(){
return $this->error;
}
public function check(){
if($this->getDomainList() != false){
return true;
}
return false;
}
//获取域名列表
public function getDomainList($KeyWord=null, $PageNumber=1, $PageSize=20){
$param = ['pageIndex' => $PageNumber, 'pageSize' => $PageSize];
$data = $this->execute('GET', '/api/domainList', $param);
if($data){
$list = [];
foreach($data['results'] as $row){
$list[] = [
'DomainId' => $row['id'],
'Domain' => rtrim($row['displayDomain'], '.'),
'RecordCount' => 0,
];
}
return ['total' => $data['total'], 'list' => $list];
}
return false;
}
//获取解析记录列表
public function getDomainRecords($PageNumber=1, $PageSize=20, $KeyWord = null, $SubDomain = null, $Type = null, $Line = null, $Status = null){
$param = ['domainId' => $this->domainid, 'pageIndex' => $PageNumber, 'pageSize' => $PageSize];
if(!isNullOrEmpty(($KeyWord))){
$param['host'] = $KeyWord;
}
if(!isNullOrEmpty(($Type))){
$param['type'] = $this->convertType($Type);
}
if(!isNullOrEmpty(($Line))){
$param['lineId'] = $Line;
}
if(!isNullOrEmpty(($SubDomain))){
$param['host'] = $SubDomain;
}
$data = $this->execute('GET', '/api/recordList', $param);
if($data){
$list = [];
foreach($data['results'] as $row){
$list[] = [
'RecordId' => $row['id'],
'Domain' => $this->domain,
'Name' => $row['host'],
'Type' => $this->convertTypeId($row['type'], isset($row['domaint']) ? $row['domaint'] : false),
'Value' => $row['data'],
'Line' => $row['lineId'],
'TTL' => $row['ttl'],
'MX' => isset($row['preference']) ? $row['preference'] : null,
'Status' => $row['disable'] ? '0' : '1',
'Weight' => isset($row['weight']) ? $row['weight'] : null,
'Remark' => null,
'UpdateTime' => date('Y-m-d H:i:s', $row['updatedAt']),
];
}
return ['total' => $data['total'], 'list' => $list];
}
return false;
}
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){
if($SubDomain == '')$SubDomain='@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line);
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId){
return false;
}
//添加解析记录
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Remark = null){
$param = ['domainId' => $this->domainid, 'type' => $this->convertType($Type), 'host' => $Name, 'data' => $Value, 'ttl' => intval($TTL), 'lineId' => $Line];
if($Type == 'MX')$param['preference'] = intval($MX);
if($Type == 'REDIRECT_URL'){$param['type'] = 256;$param['dominant'] = true;}
elseif($Type == 'FORWARD_URL'){$param['type'] = 256;$param['dominant'] = false;}
$data = $this->execute('POST', '/api/record', $param);
return is_array($data) ? $data['id'] : false;
}
//修改解析记录
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Remark = null){
$param = ['id' => $RecordId, 'type' => $this->convertType($Type), 'host' => $Name, 'data' => $Value, 'ttl' => intval($TTL), 'lineId' => $Line];
if($Type == 'MX')$param['preference'] = intval($MX);
if($Type == 'REDIRECT_URL'){$param['type'] = 256;$param['dominant'] = true;}
elseif($Type == 'FORWARD_URL'){$param['type'] = 256;$param['dominant'] = false;}
$data = $this->execute('PUT', '/api/record', $param);
return $data!==false;
}
//修改解析记录备注
public function updateDomainRecordRemark($RecordId, $Remark){
return false;
}
//删除解析记录
public function deleteDomainRecord($RecordId){
$param = ['id' => $RecordId];
$data = $this->execute('DELETE', '/api/record', $param);
return $data!==false;
}
//设置解析记录状态
public function setDomainRecordStatus($RecordId, $Status){
$param = ['id' => $RecordId, 'disable' => $Status == '0' ? true : false];
$data = $this->execute('PUT', '/api/recordDisable', $param);
return $data!==false;
}
//获取解析记录操作日志
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null){
return false;
}
//获取解析线路列表
public function getRecordLine(){
$param = ['domain' => $this->domain];
$data = $this->execute('GET', '/api/availableLine', $param);
if($data){
array_multisort(array_column($data, 'order'), SORT_ASC, $data);
$list = [];
foreach($data as $row){
if($row['id'] == '0') $row['id'] = '';
$list[$row['id']] = ['name'=>$row['value'], 'parent'=>!empty($row['pid']) ? $row['pid'] : null];
}
return $list;
}
return false;
}
//获取域名信息
public function getDomainInfo(){
$param = ['id' => $this->domainid];
$data = $this->execute('GET', '/api/domain', $param);
return $data;
}
//获取域名最低TTL
public function getMinTTL(){
$param = ['id' => $this->domainid];
$data = $this->execute('GET', '/api/dnsMeasures', $param);
if($data && isset($data['minTTL'])){
return $data['minTTL'];
}
return false;
}
private function convertType($type){
$typeList = array_flip($this->typeList);
return $typeList[$type];
}
private function convertTypeId($typeId, $domaint){
if($typeId == 256) return $domaint ? 'REDIRECT_URL' : 'FORWARD_URL';
return $this->typeList[$typeId];
}
private function execute($method, $path, $params = null){
$token = base64_encode($this->apiid.':'.$this->apisecret);
$header = ['Authorization: Basic '.$token, 'Content-Type: application/json; charset=utf-8'];
if($method == 'POST' || $method == 'PUT'){
$response = $this->curl($method, $path, $header, json_encode($params));
}else{
if($params){
$path .= '?'.http_build_query($params);
}
$response = $this->curl($method, $path, $header);
}
if(!$response){
return false;
}
$arr=json_decode($response,true);
if($arr){
if($arr['code'] == 200){
return $arr['data'];
}else{
$this->setError($arr['msg']);
return false;
}
}else{
$this->setError('返回数据解析失败');
return false;
}
}
private function curl($method, $path, $header, $body = null, $isPut = false){
$url = $this->baseUrl . $path;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($body) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$this->setError('Curl error: ' . curl_error($ch));
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($errno) return false;
if($httpCode==200){
return $response;
}elseif($httpCode==401){
$this->setError('认证失败');
return false;
}else{
$this->setError('http code: '.$httpCode);
return false;
}
}
private function setError($message){
$this->error = $message;
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
}
}

View File

@@ -57,11 +57,11 @@ class dnspod implements DnsInterface {
$action = 'DescribeRecordFilterList';
$Status = $Status == '1' ? 'ENABLE' : 'DISABLE';
$param = ['Domain' => $this->domain, 'Subdomain' => $SubDomain, 'Keyword' => $KeyWord, 'Offset' => $offset, 'Limit' => $PageSize, 'RecordStatus' => [$Status]];
if(!isNullOrEmpty($Type)) $param['RecordType'] = [$Type];
if(!isNullOrEmpty($Type)) $param['RecordType'] = [$this->convertType($Type)];
if(!isNullOrEmpty($Line)) $param['RecordLine'] = [$Line];
}else{
$action = 'DescribeRecordList';
$param = ['Domain' => $this->domain, 'Subdomain' => $SubDomain, 'RecordType' => $Type, 'RecordLineId' => $Line, 'Keyword' => $KeyWord, 'Offset' => $offset, 'Limit' => $PageSize];
$param = ['Domain' => $this->domain, 'Subdomain' => $SubDomain, 'RecordType' => $this->convertType($Type), 'RecordLineId' => $Line, 'Keyword' => $KeyWord, 'Offset' => $offset, 'Limit' => $PageSize];
}
$data = $this->send_reuqest($action, $param);
if($data){
@@ -72,7 +72,7 @@ class dnspod implements DnsInterface {
'RecordId' => $row['RecordId'],
'Domain' => $this->domain,
'Name' => $row['Name'],
'Type' => $row['Type'],
'Type' => $this->convertTypeId($row['Type']),
'Value' => $row['Value'],
'Line' => $row['LineId'],
'TTL' => $row['TTL'],
@@ -84,17 +84,16 @@ class dnspod implements DnsInterface {
];
}
return ['total' => $data['RecordCountInfo']['TotalCount'], 'list' => $list];
}elseif($this->error == '记录列表为空。'){
return ['total' => 0, 'list' => []];
}
return false;
}
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){
$domain_arr = explode('.', $SubDomain);
$domain = $domain_arr[count($domain_arr)-2].'.'.$domain_arr[count($domain_arr)-1];
$subdomain = rtrim(str_replace($domain,'',$SubDomain),'.');
if($subdomain == '')$subdomain='@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $subdomain, $Type, $Line);
if($SubDomain == '')$SubDomain='@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line);
}
//获取解析记录详细信息
@@ -107,7 +106,7 @@ class dnspod implements DnsInterface {
'RecordId' => $data['RecordInfo']['Id'],
'Domain' => $this->domain,
'Name' => $data['RecordInfo']['SubDomain'],
'Type' => $data['RecordInfo']['RecordType'],
'Type' => $this->convertTypeId($data['RecordInfo']['RecordType']),
'Value' => $data['RecordInfo']['Value'],
'Line' => $data['RecordInfo']['RecordLineId'],
'TTL' => $data['RecordInfo']['TTL'],
@@ -266,6 +265,14 @@ class dnspod implements DnsInterface {
return $type;
}
private function convertTypeId($type){
$convert_dict = ['显性URL'=>'REDIRECT_URL', '隐性URL'=>'FORWARD_URL'];
if(array_key_exists($type, $convert_dict)){
return $convert_dict[$type];
}
return $type;
}
private function send_reuqest($action, $param){
$param = array_filter($param, function($a){ return $a!==null;});

View File

@@ -7,8 +7,6 @@ class huawei implements DnsInterface {
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint = "dns.myhuaweicloud.com";
private $service = "dnspod";
private $version = "2021-03-23";
private $error;
private $domain;
private $domainid;
@@ -55,7 +53,8 @@ class huawei implements DnsInterface {
$offset = ($PageNumber-1)*$PageSize;
$query = ['type' => $Type, 'line_id' => $Line, 'name' => $KeyWord, 'status' => $Status, 'offset' => $offset, 'limit' => $PageSize];
if(!isNullOrEmpty(($SubDomain))){
$param['name'] = $SubDomain;
$query['name'] = $SubDomain;
$query['search_mode'] = 'equal';
}
$data = $this->send_reuqest('GET', '/v2.1/zones/'.$this->domainid.'/recordsets', $query);
if($data){
@@ -84,17 +83,15 @@ class huawei implements DnsInterface {
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){
$domain_arr = explode('.', $SubDomain);
$domain = $domain_arr[count($domain_arr)-2].'.'.$domain_arr[count($domain_arr)-1];
$subdomain = rtrim(str_replace($domain,'',$SubDomain),'.');
if($subdomain == '')$subdomain='@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $subdomain, $Type, $Line);
$SubDomain = $this->getHost($SubDomain);
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line);
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId){
$data = $this->send_reuqest('GET', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId);
if($data){
if($data['name'] == $data['zone_name']) $data['name'] = '@';
return [
'RecordId' => $data['id'],
'Domain' => rtrim($data['zone_name'], '.'),
@@ -115,10 +112,9 @@ class huawei implements DnsInterface {
//添加解析记录
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Remark = null){
if($Name == '@') $Name = '';
else $Name .= '.';
$Name .= $this->domain . '.';
$params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => [$Value], 'line'=>$Line, 'ttl' => intval($TTL), 'description' => $Remark];
$Name = $this->getHost($Name);
$records = explode(',', $Value);
$params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => $records, 'line'=>$Line, 'ttl' => intval($TTL), 'description' => $Remark];
if($Type == 'MX')$param['weight'] = intval($MX);
$data = $this->send_reuqest('POST', '/v2.1/zones/'.$this->domainid.'/recordsets', null, $params);
return is_array($data) ? $data['id'] : false;
@@ -126,10 +122,9 @@ class huawei implements DnsInterface {
//修改解析记录
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Remark = null){
if($Name == '@') $Name = '';
else $Name .= '.';
$Name .= $this->domain . '.';
$params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => [$Value], 'line'=>$Line, 'ttl' => intval($TTL), 'description' => $Remark];
$Name = $this->getHost($Name);
$records = explode(',', $Value);
$params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => $records, 'line'=>$Line, 'ttl' => intval($TTL), 'description' => $Remark];
if($Type == 'MX')$param['weight'] = intval($MX);
$data = $this->send_reuqest('PUT', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId, null, $params);
return is_array($data);
@@ -204,6 +199,13 @@ class huawei implements DnsInterface {
return $type;
}
private function getHost($Name){
if($Name == '@') $Name = '';
else $Name .= '.';
$Name .= $this->domain . '.';
return $Name;
}
private function send_reuqest($method, $path, $query = null, $params = null){
if(!empty($query)){
$query = array_filter($query, function($a){ return $a!==null;});
@@ -329,6 +331,9 @@ class huawei implements DnsInterface {
if(isset($arr['error_msg'])){
$this->setError($arr['error_msg']);
return false;
}elseif(isset($arr['message'])){
$this->setError($arr['message']);
return false;
}else{
return $arr;
}

View File

@@ -3,6 +3,9 @@ namespace app\lib\dns;
use app\lib\DnsInterface;
/**
* @see http://apipost.west.cn/
*/
class west implements DnsInterface {
private $username;
private $api_password;
@@ -30,8 +33,8 @@ class west implements DnsInterface {
//获取域名列表
public function getDomainList($KeyWord=null, $PageNumber=1, $PageSize=20){
$param = ['page' => $PageNumber, 'limit' => $PageSize, 'domain' => $KeyWord];
$data = $this->execute('/domain/?act=getdomains', $param);
$param = ['act' => 'getdomains', 'page' => $PageNumber, 'limit' => $PageSize, 'domain' => $KeyWord];
$data = $this->execute('/domain/', $param);
if($data){
$list = [];
foreach($data['items'] as $row){
@@ -48,23 +51,23 @@ class west implements DnsInterface {
//获取解析记录列表
public function getDomainRecords($PageNumber=1, $PageSize=20, $KeyWord = null, $SubDomain = null, $Type = null, $Line = null, $Status = null){
$param = ['act' => 'dnsrec.list', 'domain' => $this->domain, 'record_type' => $Type, 'record_line' => $Line, 'hostname' => $KeyWord, 'pageno' => $PageNumber, 'limit' => $PageSize];
$param = ['act' => 'getdnsrecord', 'domain' => $this->domain, 'type' => $Type, 'line' => $Line, 'host' => $KeyWord, 'pageno' => $PageNumber, 'limit' => $PageSize];
if(!isNullOrEmpty(($SubDomain))){
$param['hostname'] = $SubDomain;
$param['host'] = $SubDomain;
}
$data = $this->execute2('/domain/dns/', $param);
$data = $this->execute('/domain/', $param);
if($data){
$list = [];
foreach($data['items'] as $row){
$list[] = [
'RecordId' => $row['record_id'],
'RecordId' => $row['id'],
'Domain' => $this->domain,
'Name' => $row['hostname'],
'Type' => $row['record_type'],
'Value' => $row['record_value'],
'Line' => $row['record_line'],
'TTL' => $row['record_ttl'],
'MX' => $row['record_mx'],
'Name' => $row['item'],
'Type' => $row['type'],
'Value' => $row['value'],
'Line' => $row['line'],
'TTL' => $row['ttl'],
'MX' => $row['level'],
'Status' => $row['pause'] == 1 ? '0' : '1',
'Weight' => null,
'Remark' => null,
@@ -78,11 +81,8 @@ class west implements DnsInterface {
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){
$domain_arr = explode('.', $SubDomain);
$domain = $domain_arr[count($domain_arr)-2].'.'.$domain_arr[count($domain_arr)-1];
$subdomain = rtrim(str_replace($domain,'',$SubDomain),'.');
if($subdomain == '')$subdomain='@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $subdomain, $Type, $Line);
if($SubDomain == '')$SubDomain='@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line);
}
//获取解析记录详细信息
@@ -92,15 +92,15 @@ class west implements DnsInterface {
//添加解析记录
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Remark = null){
$param = ['act' => 'dnsrec.add', 'domain' => $this->domain, 'hostname' => $Name, 'record_type' => $this->convertType($Type), 'record_value' => $Value, 'record_level' => $MX, 'record_ttl' => intval($TTL), 'record_line' => $Line];
$data = $this->execute2('/domain/dns/', $param);
return is_array($data) ? $data['record_id'] : false;
$param = ['act' => 'adddnsrecord', 'domain' => $this->domain, 'host' => $Name, 'type' => $this->convertType($Type), 'value' => $Value, 'level' => $MX, 'ttl' => intval($TTL), 'line' => $Line];
$data = $this->execute('/domain/', $param);
return is_array($data) ? $data['id'] : false;
}
//修改解析记录
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Remark = null){
$param = ['act' => 'dnsrec.modify', 'domain' => $this->domain, 'record_id' => $RecordId, 'record_type' => $this->convertType($Type), 'record_value' => $Value, 'record_level' => $MX, 'record_ttl' => intval($TTL), 'record_line' => $Line];
$data = $this->execute2('/domain/dns/', $param);
$param = ['act' => 'moddnsrecord', 'domain' => $this->domain, 'id' => $RecordId, 'type' => $this->convertType($Type), 'value' => $Value, 'level' => $MX, 'ttl' => intval($TTL), 'line' => $Line];
$data = $this->execute('/domain/', $param);
return is_array($data);
}
@@ -111,14 +111,16 @@ class west implements DnsInterface {
//删除解析记录
public function deleteDomainRecord($RecordId){
$param = ['act' => 'dnsrec.remove', 'domain' => $this->domain, 'record_id' => $RecordId];
$data = $this->execute2('/domain/dns/', $param);
$param = ['act' => 'deldnsrecord', 'domain' => $this->domain, 'id' => $RecordId];
$data = $this->execute('/domain/', $param);
return is_array($data);
}
//设置解析记录状态
public function setDomainRecordStatus($RecordId, $Status){
return false;
$param = ['act' => 'pause', 'domain' => $this->domain, 'id' => $RecordId, 'val' => $Status == '1' ? '0' : '1'];
$data = $this->execute('/domain/', $param);
return $data !== false;
}
//获取解析记录操作日志
@@ -162,26 +164,7 @@ class west implements DnsInterface {
$arr=json_decode($response,true);
if($arr){
if($arr['result'] == 200){
return $arr['data'];
}else{
$this->setError($arr['msg']);
return false;
}
}else{
$this->setError('返回数据解析失败');
return false;
}
}
private function execute2($path, $params){
$params['username'] = $this->username;
$params['apikey'] = md5($this->api_password);
$response = $this->curl($path, $params);
$response = mb_convert_encoding($response, 'UTF-8', 'GBK');
$arr=json_decode($response,true);
if($arr){
if($arr['code'] == 200){
return $arr['body'];
return isset($arr['data']) ? $arr['data'] : [];
}else{
$this->setError($arr['msg']);
return false;

72
app/lib/mail/Aliyun.php Normal file
View File

@@ -0,0 +1,72 @@
<?php
namespace app\lib\mail;
class Aliyun
{
private $AccessKeyId;
private $AccessKeySecret;
function __construct($AccessKeyId, $AccessKeySecret)
{
$this->AccessKeyId = $AccessKeyId;
$this->AccessKeySecret = $AccessKeySecret;
}
private function aliyunSignature($parameters, $accessKeySecret, $method)
{
ksort($parameters);
$canonicalizedQueryString = '';
foreach ($parameters as $key => $value) {
if($value === null) continue;
$canonicalizedQueryString .= '&' . $this->percentEncode($key) . '=' . $this->percentEncode($value);
}
$stringToSign = $method . '&%2F&' . $this->percentencode(substr($canonicalizedQueryString, 1));
$signature = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&", true));
return $signature;
}
private function percentEncode($str)
{
$search = ['+', '*', '%7E'];
$replace = ['%20', '%2A', '~'];
return str_replace($search, $replace, urlencode($str));
}
public function send($to, $sub, $msg, $from, $from_name)
{
if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) return false;
$url = 'https://dm.aliyuncs.com/';
$data = array(
'Action' => 'SingleSendMail',
'AccountName' => $from,
'ReplyToAddress' => 'false',
'AddressType' => 1,
'ToAddress' => $to,
'FromAlias' => $from_name,
'Subject' => $sub,
'HtmlBody' => $msg,
'Format' => 'JSON',
'Version' => '2015-11-23',
'AccessKeyId' => $this->AccessKeyId,
'SignatureMethod' => 'HMAC-SHA1',
'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
'SignatureVersion' => '1.0',
'SignatureNonce' => random(8)
);
$data['Signature'] = $this->aliyunSignature($data, $this->AccessKeySecret, 'POST');
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
$json = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$arr = json_decode($json, true);
if ($httpCode == 200) {
return true;
} else {
return $arr['Message'];
}
}
}

View File

@@ -0,0 +1,2 @@
<?php
namespace app\lib\mail\PHPMailer; class Exception extends \Exception { public function errorMessage() { return '<strong>' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "</strong><br />\n"; } }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,37 @@
<?php
namespace app\lib\mail;
class Sendcloud {
private $apiUser;
private $apiKey;
function __construct($apiUser, $apiKey){
$this->apiUser = $apiUser;
$this->apiKey = $apiKey;
}
public function send($to, $sub, $msg, $from, $from_name){
if(empty($this->apiUser)||empty($this->apiKey))return false;
$url='http://api.sendcloud.net/apiv2/mail/send';
$data=array(
'apiUser' => $this->apiUser,
'apiKey' => $this->apiKey,
'from' => $from,
'fromName' => $from_name,
'to' => $to,
'subject' => $sub,
'html' => $msg);
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
$json=curl_exec($ch);
curl_close($ch);
$arr=json_decode($json,true);
if($arr['statusCode']==200){
return true;
}else{
return implode("\n",$arr['message']);
}
}
}

View File

@@ -3,6 +3,7 @@ declare (strict_types = 1);
namespace app\middleware;
use Exception;
use think\facade\Db;
use think\facade\Config;
@@ -27,7 +28,15 @@ class LoadConfig
return $next($request);
}
}
Config::set([], 'sys');
try{
$res = Db::name('config')->cache('configs',0)->column('value','key');
Config::set($res, 'sys');
}catch(Exception $e){
if(!strpos($e->getMessage(), 'doesn\'t exist')){
throw $e;
}
}
$request->isApi = false;

View File

@@ -1,3 +1,16 @@
DROP TABLE IF EXISTS `dnsmgr_config`;
CREATE TABLE `dnsmgr_config` (
`key` varchar(32) NOT NULL,
`value` TEXT DEFAULT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `dnsmgr_config` VALUES ('version', '1005');
INSERT INTO `dnsmgr_config` VALUES ('notice_mail', '0');
INSERT INTO `dnsmgr_config` VALUES ('notice_wxtpl', '0');
INSERT INTO `dnsmgr_config` VALUES ('mail_smtp', 'smtp.qq.com');
INSERT INTO `dnsmgr_config` VALUES ('mail_port', '465');
DROP TABLE IF EXISTS `dnsmgr_account`;
CREATE TABLE `dnsmgr_account` (
`id` int(11) unsigned NOT NULL auto_increment,
@@ -51,7 +64,7 @@ CREATE TABLE `dnsmgr_permission` (
DROP TABLE IF EXISTS `dnsmgr_log`;
CREATE TABLE `dnsmgr_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`uid` int(11) unsigned NOT NULL,
`action` varchar(40) NOT NULL,
`domain` varchar(128) NOT NULL DEFAULT '',
@@ -61,3 +74,63 @@ CREATE TABLE `dnsmgr_log` (
KEY `uid` (`uid`),
KEY `domain` (`domain`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `dnsmgr_dmtask`;
CREATE TABLE `dnsmgr_dmtask` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`did` int(11) unsigned NOT NULL,
`rr` varchar(128) NOT NULL,
`recordid` varchar(60) NOT NULL,
`type` tinyint(1) NOT NULL DEFAULT 0,
`main_value` varchar(128) DEFAULT NULL,
`backup_value` varchar(128) DEFAULT NULL,
`checktype` tinyint(1) NOT NULL DEFAULT 0,
`checkurl` varchar(512) DEFAULT NULL,
`tcpport` int(5) DEFAULT NULL,
`frequency` tinyint(5) NOT NULL,
`cycle` tinyint(5) NOT NULL DEFAULT 3,
`timeout` tinyint(5) NOT NULL DEFAULT 2,
`remark` varchar(100) DEFAULT NULL,
`addtime` int(11) NOT NULL DEFAULT 0,
`checktime` int(11) NOT NULL DEFAULT 0,
`checknexttime` int(11) NOT NULL DEFAULT 0,
`switchtime` int(11) NOT NULL DEFAULT 0,
`errcount` tinyint(5) NOT NULL DEFAULT 0,
`status` tinyint(1) NOT NULL DEFAULT 0,
`active` tinyint(1) NOT NULL DEFAULT 0,
`recordinfo` varchar(200) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `did` (`did`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `dnsmgr_dmlog`;
CREATE TABLE `dnsmgr_dmlog` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`taskid` int(11) unsigned NOT NULL,
`action` tinyint(4) NOT NULL DEFAULT 0,
`errmsg` varchar(100) DEFAULT NULL,
`date` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `taskid` (`taskid`),
KEY `date` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `dnsmgr_optimizeip`;
CREATE TABLE `dnsmgr_optimizeip` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`did` int(11) unsigned NOT NULL,
`rr` varchar(128) NOT NULL,
`type` tinyint(1) NOT NULL DEFAULT 0,
`ip_type` varchar(10) NOT NULL,
`cdn_type` tinyint(5) NOT NULL DEFAULT 1,
`recordnum` tinyint(5) NOT NULL DEFAULT 2,
`ttl` int(5) NOT NULL DEFAULT 600,
`remark` varchar(100) DEFAULT NULL,
`addtime` datetime NOT NULL,
`updatetime` datetime DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
`active` tinyint(1) NOT NULL DEFAULT 0,
`errmsg` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `did` (`did`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

62
app/sql/update.sql Normal file
View File

@@ -0,0 +1,62 @@
CREATE TABLE IF NOT EXISTS `dnsmgr_config` (
`key` varchar(32) NOT NULL,
`value` TEXT DEFAULT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `dnsmgr_dmtask` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`did` int(11) unsigned NOT NULL,
`rr` varchar(128) NOT NULL,
`recordid` varchar(60) NOT NULL,
`type` tinyint(1) NOT NULL DEFAULT 0,
`main_value` varchar(128) DEFAULT NULL,
`backup_value` varchar(128) DEFAULT NULL,
`checktype` tinyint(1) NOT NULL DEFAULT 0,
`checkurl` varchar(512) DEFAULT NULL,
`tcpport` int(5) DEFAULT NULL,
`frequency` tinyint(5) NOT NULL,
`cycle` tinyint(5) NOT NULL DEFAULT 3,
`timeout` tinyint(5) NOT NULL DEFAULT 2,
`remark` varchar(100) DEFAULT NULL,
`addtime` int(11) NOT NULL DEFAULT 0,
`checktime` int(11) NOT NULL DEFAULT 0,
`checknexttime` int(11) NOT NULL DEFAULT 0,
`switchtime` int(11) NOT NULL DEFAULT 0,
`errcount` tinyint(5) NOT NULL DEFAULT 0,
`status` tinyint(1) NOT NULL DEFAULT 0,
`active` tinyint(1) NOT NULL DEFAULT 0,
`recordinfo` varchar(200) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `did` (`did`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `dnsmgr_dmlog` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`taskid` int(11) unsigned NOT NULL,
`action` tinyint(4) NOT NULL DEFAULT 0,
`errmsg` varchar(100) DEFAULT NULL,
`date` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `taskid` (`taskid`),
KEY `date` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `dnsmgr_optimizeip` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`did` int(11) unsigned NOT NULL,
`rr` varchar(128) NOT NULL,
`type` tinyint(1) NOT NULL DEFAULT 0,
`ip_type` varchar(10) NOT NULL,
`cdn_type` tinyint(5) NOT NULL DEFAULT 1,
`recordnum` tinyint(5) NOT NULL DEFAULT 2,
`ttl` int(5) NOT NULL DEFAULT 600,
`remark` varchar(100) DEFAULT NULL,
`addtime` datetime NOT NULL,
`updatetime` datetime DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
`active` tinyint(1) NOT NULL DEFAULT 0,
`errmsg` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `did` (`did`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -106,7 +106,35 @@
<li class="{:checkIfActive('domain,record,record_log')}">
<a href="/domain"><i class="fa fa-list-ul fa-fw"></i> <span>域名管理</span></a>
</li>
{if request()->user['level'] eq 2}<li class="{:checkIfActive('account')}">
{if request()->user['level'] eq 2}
<li class="treeview {:checkIfActive('overview,task,noticeset,taskinfo,taskform')}">
<a href="javascript:;">
<i class="fa fa-heartbeat fa-fw"></i>
<span>容灾切换</span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu">
<li><a href="/dmonitor/overview"><i class="fa fa-circle-o"></i> 运行概览</a></li>
<li><a href="/dmonitor/task"><i class="fa fa-circle-o"></i> 切换策略</a></li>
<li><a href="/dmonitor/noticeset"><i class="fa fa-circle-o"></i> 通知设置</a></li>
</ul>
</li>
<li class="treeview {:checkIfActive('opipset,opiplist,opipform')}">
<a href="javascript:;">
<i class="fa fa-globe fa-fw"></i>
<span>CF优选IP</span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu">
<li><a href="/optimizeip/opipset"><i class="fa fa-circle-o"></i> 优选设置</a></li>
<li><a href="/optimizeip/opiplist"><i class="fa fa-circle-o"></i> 任务管理</a></li>
</ul>
</li>
<li class="{:checkIfActive('account')}">
<a href="/account"><i class="fa fa-lock fa-fw"></i> <span>域名账户</span></a>
</li>
<li class="{:checkIfActive('user')}">

View File

@@ -0,0 +1,170 @@
{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">
<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="notice_mail" default="{:config_get('notice_mail')}"><option value="0">关闭</option><option value="1">开启</option></select></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">微信公众号通知</label>
<div class="col-sm-9"><select class="form-control" name="notice_wxtpl" default="{:config_get('notice_wxtpl')}"><option value="0">关闭</option><option value="1">开启</option></select></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 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"><select class="form-control" name="mail_type" default="{:config_get('mail_type')}"><option value="0">SMTP发信</option><option value="1">搜狐Sendcloud</option><option value="2">阿里云邮件推送</option></select></div>
</div>
<div id="frame_set1">
<div class="form-group">
<label class="col-sm-3 control-label">SMTP服务器</label>
<div class="col-sm-9"><input type="text" name="mail_smtp" value="{:config_get('mail_smtp')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">SMTP端口</label>
<div class="col-sm-9"><input type="text" name="mail_port" value="{:config_get('mail_port')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">邮箱账号</label>
<div class="col-sm-9"><input type="text" name="mail_name" value="{:config_get('mail_name')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">邮箱密码</label>
<div class="col-sm-9"><input type="text" name="mail_pwd" value="{:config_get('mail_pwd')}" class="form-control"/></div>
</div>
</div>
<div id="frame_set2">
<div class="form-group">
<label class="col-sm-3 control-label">API_USER</label>
<div class="col-sm-9"><input type="text" name="mail_apiuser" value="{:config_get('mail_apiuser')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">API_KEY</label>
<div class="col-sm-9"><input type="text" name="mail_apikey" value="{:config_get('mail_apikey')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">发信邮箱</label>
<div class="col-sm-9"><input type="text" name="mail_name2" value="{:config_get('mail_name')}" class="form-control"/></div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">收信邮箱</label>
<div class="col-sm-9"><input type="text" name="mail_recv" value="{:config_get('mail_recv')}" 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"/>
<a href="javascript:mailtest()" class="btn btn-default btn-block">发送测试邮件</a>
</div>
</div>
</form>
</div>
<div class="panel-footer">
<span class="glyphicon glyphicon-info-sign"></span>
使用普通模式发信时建议使用QQ邮箱SMTP服务器smtp.qq.com端口465或587密码是QQ邮箱设置界面生成的<a href="https://service.mail.qq.com/detail/0/75" target="_blank" rel="noreferrer">授权码</a><br/>阿里云邮件推送:<a href="https://www.aliyun.com/product/directmail" target="_blank" rel="noreferrer">点此进入</a><a href="https://usercenter.console.aliyun.com/#/manage/ak" target="_blank" rel="noreferrer">获取AK/SK</a>
</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">appToken</label>
<div class="col-sm-9"><input type="text" name="wechat_apptoken" value="{:config_get('wechat_apptoken')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">用户UID</label>
<div class="col-sm-9"><input type="text" name="wechat_appuid" value="{:config_get('wechat_appuid')}" class="form-control"/></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">
<b>WxPusher</b><a href="https://wxpusher.zjiecode.com/admin/" target="_blank" rel="noopener noreferrer">点此进入</a> ,注册并且创建应用 -> 将appToken填写到上方输入框 -> 扫码关注应用 -> 在用户列表查看自己的UID填写到上方输入框<br/>
</div>
</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);
}
$("select[name='mail_type']").change(function(){
if($(this).val() == 0){
$("#frame_set1").show();
$("#frame_set2").hide();
}else{
$("#frame_set1").hide();
$("#frame_set2").show();
}
});
$("select[name='mail_type']").change();
function saveSetting(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '',
data : $(obj).serialize(),
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;
}
function mailtest(){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'GET',
url : '/dmonitor/mailtest',
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg, {icon: 1});
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
}
</script>
{/block}

View File

@@ -0,0 +1,155 @@
{extend name="common/layout" /}
{block name="title"}容灾切换运行概览{/block}
{block name="main"}
<style>
.info-box-content{padding:18px 10px}
.hbox{display:table;width:100%;height:100%;border-spacing:0;table-layout:fixed;border:1px solid #edf1f2}
.hbox .col{display:table-cell;float:none;height:100%;vertical-align:top;border:1px solid #edf1f2;padding-top:18px;padding-bottom:18px;color:#98a6ad}
.hbox .col .fa{display:block;padding-bottom:3px}
.hbox .col span{font-size:14px}
.hbox .col:hover{background-color:#f9f9f9;color:#6e7173}
</style>
<div class="modal" id="modal-clean">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">数据清理</h4>
</div>
<div class="modal-body">
<form id="form-clean" onsubmit="return false;">
<div class="form-group">
<label>清理多少天前的切换记录</label>
<input type="number" class="form-control" name="days" value="30">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-info" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" onclick="submitClean()">确定</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3 col-sm-6 col-xs-12">
<div class="info-box">
<span class="info-box-icon bg-aqua"><i class="fa fa-certificate"></i></span>
<div class="info-box-content">
<span class="info-box-text">运行状态</span>
<span class="info-box-number">{$info.run_state==1?'<font color="green">正在运行</font>':'<font color="red">已停止</font>'}</span>
</div>
<!-- /.info-box-content -->
</div>
<!-- /.info-box -->
</div>
<!-- /.col -->
<div class="col-md-3 col-sm-6 col-xs-12">
<div class="info-box">
<span class="info-box-icon bg-green"><i class="fa fa-heart"></i></span>
<div class="info-box-content">
<span class="info-box-text">今日运行次数</span>
<span class="info-box-number">{$info.run_count}</span>
</div>
<!-- /.info-box-content -->
</div>
<!-- /.info-box -->
</div>
<!-- /.col -->
<!-- fix for small devices only -->
<div class="clearfix visible-sm-block"></div>
<div class="col-md-3 col-sm-6 col-xs-12">
<div class="info-box">
<span class="info-box-icon bg-red"><i class="fa fa-bandcamp"></i></span>
<div class="info-box-content">
<span class="info-box-text">24H告警次数</span>
<span class="info-box-number">{$info.fail_count}</span>
</div>
<!-- /.info-box-content -->
</div>
<!-- /.info-box -->
</div>
<!-- /.col -->
<div class="col-md-3 col-sm-6 col-xs-12">
<div class="info-box">
<span class="info-box-icon bg-yellow"><i class="fa fa-gratipay"></i></span>
<div class="info-box-content">
<span class="info-box-text">24H切换次数</span>
<span class="info-box-number">{$info.switch_count}</span>
</div>
<!-- /.info-box-content -->
</div>
<!-- /.info-box -->
</div>
<!-- /.col -->
</div>
<div class="row">
<div class="col-xs-12 col-md-6">
<div class="panel panel-success">
<div class="panel-heading"><h3 class="panel-title">运行概览</h3></div>
<div class="panel-body">
<li class="list-group-item"><span class="glyphicon glyphicon-time"></span> <b>上次运行时间:</b> {$info.run_time}</li>
<li class="list-group-item"><span class="glyphicon glyphicon-time"></span> <b>当前时间:</b> {:date('Y-m-d H:i:s')}</li>
<li class="list-group-item"><span class="fa fa-th-large"></span> <b>Swoole组件</b> {$info.swoole|raw}</li>
{if $info.run_error}<li class="list-group-item"><span class="fa fa-times-circle"></span> <b>上次运行错误信息:</b> {$info.run_error}</li>{/if}
<div class="hbox text-center text-sm">
<a href="/dmonitor/task" class="col">
<i class="fa fa-list-alt fa-2x"></i>
<span>切换策略</span>
</a>
<a href="/dmonitor/noticeset" class="col">
<i class="fa fa-bullhorn fa-2x"></i>
<span>通知设置</span>
</a>
<a href="javascript:clean()" class="col">
<i class="fa fa-trash fa-2x"></i>
<span>数据清理</span>
</a>
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">操作说明</h3></div>
<div class="panel-body">
<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>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script>
function clean(){
$('#modal-clean').modal('show');
}
function submitClean(){
var days = $('#form-clean input[name=days]').val();
if(days < 1){
layer.alert('清理天数不能小于1', {icon: 2});
return;
}
$.post('/dmonitor/clean', {days: days}, function(res){
if(res.code == 0){
layer.msg(res.msg, {icon: 1});
$('#modal-clean').modal('hide');
}else{
layer.alert(res.msg, {icon: 2});
}
});
}
</script>
{/block}

175
app/view/dmonitor/task.html Normal file
View File

@@ -0,0 +1,175 @@
{extend name="common/layout" /}
{block name="title"}容灾切换策略{/block}
{block name="main"}
<style>
tbody tr>td:nth-child(2){overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:180px;}
</style>
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default panel-intro">
<div class="panel-body">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<div class="form-group">
<label>搜索</label>
<div class="form-group">
<select name="type" class="form-control"><option value="1">域名</option><option value="3">解析记录</option><option value="4">备用解析记录</option><option value="2">解析记录ID</option><option value="5">备注</option></select>
</div>
</div>
<div class="form-group">
<input type="text" class="form-control" name="kw" 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>
<a href="/dmonitor/task/add" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
</form>
<table id="listTable">
</table>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.20.2/bootstrap-table.min.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.20.2/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 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;
$("#listTable").bootstrapTable({
url: '/dmonitor/task/data',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bordered',
columns: [
{
field: 'id',
title: 'ID'
},
{
field: 'rr',
title: '域名',
formatter: function(value, row, index) {
return '<span title="'+row.remark+'" data-toggle="tooltip" data-placement="right" title="Tooltip on right">' + value + '.' + row.domain + '</span>';
}
},
{
field: 'main_value',
title: '解析记录',
formatter: function(value, row, index) {
return value;
}
},
{
field: 'type',
title: '切换设置',
formatter: function(value, row, index) {
if(value == 1) {
return '暂停解析';
} else if(value == 2) {
return '<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="备用:'+row.backup_value+'" class="tips">切换备用</span>';
} else if(value == 3) {
return '<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="同域名正常数量<='+row.cycle+'" class="tips">条件开启</span>';
} else {
return '无操作';
}
}
},
{
field: 'frequency',
title: '检测间隔',
formatter: function(value, row, index) {
if(row.type <= 2){
var checktype = 'PING';
if(row.checktype == 2){
checktype = row.checkurl;
}else if(row.checktype == 1){
checktype = 'TCP('+row.tcpport+'端口)';
}
}else{
var checktype = '';
}
return '<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="'+checktype+'" class="tips">' + value + '秒</span>';
}
},
{
field: 'status',
title: '健康状况',
formatter: function(value, row, index) {
if(value == 0) {
return '<span class="label label-success">正常</span>';
} else {
return '<span class="label label-danger">异常</span>';
}
}
},
{
field: 'active',
title: '运行开关',
formatter: function(value, row, index) {
var html = '';
if(value == 1) {
html += '<span class="btn btn-success btn-xs" onclick="setActive('+row.id+', 0)">开启</span>';
} else {
html += '<span class="btn btn-warning btn-xs" onclick="setActive('+row.id+', 1)">暂停</span>';
}
return html;
}
},
{
field: 'checktime',
title: '上次检测时间',
formatter: function(value, row, index) {
return value > 0 ? row.checktimestr : '未运行';
}
},
{
field: '',
title: '操作',
formatter: function(value, row, index) {
var html = '<a href="/dmonitor/task/info/'+row.id+'" class="btn btn-info btn-xs">切换日志</a>&nbsp;&nbsp;';
html += '<a href="/dmonitor/task/edit?id='+row.id+'" class="btn btn-primary btn-xs">修改</a>&nbsp;&nbsp;';
html += '<a href="/record/'+row.did+'?keyword='+row.rr+'" class="btn btn-default btn-xs" target="_blank">解析</a>&nbsp;&nbsp;';
html += '<a href="javascript:delItem(\''+row.id+'\')" class="btn btn-danger btn-xs">删除</a>&nbsp;&nbsp;';
return html;
}
},
],
onLoadSuccess: function(data) {
$('[data-toggle="tooltip"]').tooltip()
}
})
})
function setActive(id, active){
$.post('/dmonitor/task/setactive', {id: id, active: active}, function(data){
if(data.code == 0) {
layer.msg('修改成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
}
function delItem(id){
layer.confirm('确定要删除此切换策略吗?', {
btn: ['确定','取消']
}, function(){
$.post('/dmonitor/task/del', {id: id}, function(data){
if(data.code == 0) {
layer.msg('删除成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
});
}
</script>
{/block}

View File

@@ -0,0 +1,254 @@
{extend name="common/layout" /}
{block name="title"}容灾切换策略{/block}
{block name="main"}
<style>
.dselect::before{
content: '.';
position: absolute;
left: 0;
}
.tips{color: #f6a838; padding-left: 5px;}
</style>
<div class="row" id="app">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title"><a href="/dmonitor/task" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>{if $action=='edit'}编辑{else}添加{/if}容灾切换策略</h3></div>
<div class="panel-body">
<form onsubmit="return false" method="post" class="form-horizontal" role="form" id="taskform">
<div class="form-group">
<label class="col-sm-3 col-xs-12 control-label no-padding-right">域名选择</label>
<div class="col-sm-3 col-xs-5"><input type="text" name="rr" v-model="set.rr" placeholder="主机记录" class="form-control" required></div>
<div class="col-sm-3 col-xs-7 dselect"><select name="did" v-model="set.did" class="form-control" required>
<option value="">--主域名--</option>
{foreach $domains as $k=>$v}
<option value="{$k}">{$v}</option>
{/foreach}
</select></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">解析记录</label>
<div class="col-sm-6"><div class="input-group">
<select name="recordid" v-model="set.recordid" id="recordid" class="form-control" required>
<option v-for="option in recordList" :value="option.RecordId">{{option.Value}} (线路:{{option.LineName}})</option>
</select>
<div class="input-group-btn">
<button type="button" @click="getRecordList" class="btn btn-info">点击获取</button>
</div>
</div></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">切换设置</label>
<div class="col-sm-6">
<label class="radio-inline" v-for="option in typeList">
<input type="radio" name="type" :value="option.value" v-model="set.type" :disabled="option.disabled"> {{option.label}}
</label>
</div>
</div>
<div class="form-group" v-show="set.type==2">
<label class="col-sm-3 control-label no-padding-right">备用解析记录</label>
<div class="col-sm-6">
<input type="text" name="backup_value" v-model="set.backup_value" placeholder="支持填写IPv4或CNAME地址" class="form-control" required>
</div>
</div>
<div class="form-group" v-show="set.type<=2">
<label class="col-sm-3 control-label no-padding-right">检测协议</label>
<div class="col-sm-6">
<label class="radio-inline" v-for="option in checktypeList">
<input type="radio" name="checktype" :value="option.value" v-model="set.checktype" :disabled="option.disabled"> {{option.label}}
</label>
</div>
</div>
<div class="form-group" v-show="set.type<=2&&set.checktype==1">
<label class="col-sm-3 control-label no-padding-right">TCP检测端口</label>
<div class="col-sm-6">
<input type="text" name="tcpport" v-model="set.tcpport" placeholder="填写TCP端口号" class="form-control" data-bv-integer="true" min="1" max="65535" required>
</div>
</div>
<div class="form-group" v-show="set.type<=2&&set.checktype==2">
<label class="col-sm-3 control-label no-padding-right">检测URL地址</label>
<div class="col-sm-6">
<input type="text" name="checkurl" v-model="set.checkurl" placeholder="填写以http(s)://开头的完整地址http状态码须为2xx/3xx" class="form-control" data-bv-uri="true" required>
</div>
</div>
<div class="form-group" v-show="set.type<=2&&set.checktype>0">
<label class="col-sm-3 control-label no-padding-right">最大超时时间</label>
<div class="col-sm-3">
<div class="input-group">
<input type="text" name="timeout" v-model="set.timeout" placeholder="填写请求最大超时时间" class="form-control" data-bv-integer="true" min="1" required>
<span class="input-group-addon"></span>
</div>
</div>
</div>
<div class="form-group" v-show="set.type==3">
<label class="col-sm-3 control-label no-padding-right">同域名正常数量<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="与暂停解析配合使用,当同域名正常记录数量&lt;=几条时开启解析"><i class="fa fa-question-circle"></i></span></label>
<div class="col-sm-3">
<input type="text" name="cycle" v-model="set.cycle" placeholder="同域名正常记录数量&lt;=几条时开启解析" class="form-control" data-bv-integer="true" min="0" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">检测间隔</label>
<div class="col-sm-3">
<div class="input-group">
<input type="text" name="frequency" v-model="set.frequency" placeholder="每次检测的间隔时间" class="form-control" data-bv-integer="true" min="1" required>
<span class="input-group-addon"></span>
</div>
</div>
</div>
<div class="form-group" v-show="set.type<=2">
<label class="col-sm-3 control-label no-padding-right">确认次数</label>
<div class="col-sm-3">
<input type="text" name="cycle" v-model="set.cycle" placeholder="连续失败几次后进行切换" class="form-control" data-bv-integer="true" min="1" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">备注</label>
<div class="col-sm-6">
<input type="text" name="remark" v-model="set.remark" placeholder="可留空" class="form-control">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-6"><button type="button" class="btn btn-primary" @click="submit">提交</button></div>
</div>
</form>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>
var action = '{$action}';
var info = {$info|json_encode|raw};
var support_ping = '{$support_ping}';
new Vue({
el: '#app',
data: {
action: '{$action}',
set: {
id: '',
remark: '',
rr: '',
did: '',
recordid: '',
recordinfo: '',
main_value: '',
type: 1,
backup_value: '',
checktype: 1,
tcpport: 80,
checkurl: '',
frequency: 5,
timeout: 2,
cycle: 3,
},
recordList: [],
typeList: [
{value:0, label:'无操作'},
{value:1, label:'暂停解析'},
{value:2, label:'切换备用解析'},
{value:3, label:'条件开启解析'},
],
checktypeList: [
{value:0, label:'PING', disabled: support_ping=='0'},
{value:1, label:'TCP'},
{value:2, label:'HTTP(S)'},
]
},
watch: {
'set.recordid': function(val){
if(val == '') return;
var record = this.recordList.find(item => item.RecordId == val);
if(record){
this.set.recordinfo = JSON.stringify({Line:record.Line, LineName:record.LineName, TTL:record.TTL});
if(typeof record.Value == 'object') this.set.main_value = record.Value[0];
else this.set.main_value = record.Value;
}
}
},
mounted() {
if(this.action == 'edit'){
Object.keys(info).forEach((key) => {
this.$set(this.set, key, info[key])
})
var recordinfo = JSON.parse(this.set.recordinfo);
this.recordList = [{RecordId:this.set.recordid, Value:this.set.main_value, Line:recordinfo.Line, LineName:recordinfo.LineName, TTL:recordinfo.TTL}];
}
$("#taskform").bootstrapValidator({
live: 'submitted',
});
$('[data-toggle="tooltip"]').tooltip();
},
methods: {
getRecordList(){
var that = this;
if(this.set.did == ''){
layer.msg('请先选择域名', {time: 800});return;
}
if(this.set.rr == ''){
layer.msg('主机记录不能为空', {time: 800});return;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/record/list',
data : {id:this.set.did, rr:this.set.rr},
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.msg('成功获取到'+data.data.length+'条解析记录', {icon:1, time:800});
that.recordList = data.data;
if(that.set.recordid){
var record = that.recordList.find(item => item.RecordId == that.set.recordid);
if(record){
that.set.recordinfo = JSON.stringify({Line:record.Line, LineName:record.LineName, TTL:record.TTL});
if(typeof record.Value == 'object') that.set.main_value = record.Value[0];
else that.set.main_value = record.Value;
}
}
}else{
layer.alert(data.msg, {icon:2});
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
},
submit(){
var that=this;
$("#taskform").data("bootstrapValidator").validate();
if(!$("#taskform").data("bootstrapValidator").isValid()){
return false;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type: "POST",
url: "",
data: this.set,
dataType: 'json',
success: function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg, {icon: 1}, function(){
if(document.referrer.indexOf('task?') > 0)
window.location.href = document.referrer;
else
window.location.href = '/dmonitor/task';
});
}else{
layer.alert(data.msg, {icon: 2});
}
},
error: function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
}
},
});
</script>
{/block}

View File

@@ -0,0 +1,73 @@
{extend name="common/layout" /}
{block name="title"}切换记录{/block}
{block name="main"}
<style>
tbody tr>td:nth-child(4){overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:180px;}
</style>
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default panel-intro">
<div class="panel-body">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<div class="form-group">
<label>搜索</label>
<div class="form-group">
<select name="action" class="form-control"><option value="0">操作类型</option><option value="1">发生异常</option><option value="2">恢复正常</option></select>
</div>
</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>
&nbsp;&nbsp;24H告警次数<strong>{$info.fail_count}</strong>&nbsp;&nbsp;切换次数:<strong>{$info.switch_count}</strong>
</form>
<table id="listTable">
</table>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.20.2/bootstrap-table.min.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.20.2/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
var action_name = {$info.action_name|json_encode|raw};
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 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;
$("#listTable").bootstrapTable({
url: '/dmonitor/task/log/data/{$info.id}',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bordered',
columns: [
{
field: 'id',
title: 'ID'
},
{
field: 'action',
title: '操作类型',
formatter: function(value, row, index) {
return action_name[value];
}
},
{
field: 'date',
title: '时间'
},
{
field: 'errmsg',
title: '异常原因'
}
],
})
})
</script>
{/block}

View File

@@ -357,5 +357,10 @@ function getDomainList(){
function loading(){
layer.load(2);
}
document.addEventListener("visibilitychange", function() {
if (document.visibilityState === "visible") {
layer.closeAll();
}
});
</script>
{/block}

View File

@@ -153,7 +153,12 @@ $(document).ready(function(){
},
{
field: 'Type',
title: '记录类型'
title: '记录类型',
formatter: function(value, row, index) {
if(value == 'REDIRECT_URL') return '显性URL';
if(value == 'FORWARD_URL') return '隐性URL';
return value;
}
},
{
field: 'LineName',

502
app/view/exception.tpl Normal file
View File

@@ -0,0 +1,502 @@
<?php
/** @var array $traces */
if (!function_exists('parse_padding')) {
function parse_padding($source)
{
$length = strlen(strval(count($source['source']) + $source['first']));
return 40 + ($length - 1) * 8;
}
}
if (!function_exists('parse_class')) {
function parse_class($name)
{
$names = explode('\\', $name);
return '<abbr title="'.$name.'">'.end($names).'</abbr>';
}
}
if (!function_exists('parse_file')) {
function parse_file($file, $line)
{
return '<a class="toggle" title="'."{$file} line {$line}".'">'.basename($file)." line {$line}".'</a>';
}
}
if (!function_exists('parse_args')) {
function parse_args($args)
{
$result = [];
foreach ($args as $key => $item) {
switch (true) {
case is_object($item):
$value = sprintf('<em>object</em>(%s)', parse_class(get_class($item)));
break;
case is_array($item):
if (count($item) > 3) {
$value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3)));
} else {
$value = sprintf('[%s]', parse_args($item));
}
break;
case is_string($item):
if (strlen($item) > 20) {
$value = sprintf(
'\'<a class="toggle" title="%s">%s...</a>\'',
htmlentities($item),
htmlentities(substr($item, 0, 20))
);
} else {
$value = sprintf("'%s'", htmlentities($item));
}
break;
case is_int($item):
case is_float($item):
$value = $item;
break;
case is_null($item):
$value = '<em>null</em>';
break;
case is_bool($item):
$value = '<em>' . ($item ? 'true' : 'false') . '</em>';
break;
case is_resource($item):
$value = '<em>resource</em>';
break;
default:
$value = htmlentities(str_replace("\n", '', var_export(strval($item), true)));
break;
}
$result[] = is_int($key) ? $value : "'{$key}' => {$value}";
}
return implode(', ', $result);
}
}
if (!function_exists('echo_value')) {
function echo_value($val)
{
if (is_array($val) || is_object($val)) {
echo htmlentities(json_encode($val, JSON_PRETTY_PRINT));
} elseif (is_bool($val)) {
echo $val ? 'true' : 'false';
} elseif (is_scalar($val)) {
echo htmlentities($val);
} else {
echo 'Resource';
}
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>系统发生错误</title>
<meta name="robots" content="noindex,nofollow" />
<style>
/* Base */
body {
color: #333;
font: 16px Verdana, "Helvetica Neue", helvetica, Arial, 'Microsoft YaHei', sans-serif;
margin: 0;
padding: 0 20px 20px;
}
h1{
margin: 10px 0 0;
font-size: 28px;
font-weight: 500;
line-height: 32px;
}
h2{
color: #4288ce;
font-weight: 400;
padding: 6px 0;
margin: 6px 0 0;
font-size: 18px;
border-bottom: 1px solid #eee;
}
h3{
margin: 12px;
font-size: 16px;
font-weight: bold;
}
abbr{
cursor: help;
text-decoration: underline;
text-decoration-style: dotted;
}
a{
color: #868686;
cursor: pointer;
}
a:hover{
text-decoration: underline;
}
.line-error{
background: #f8cbcb;
}
.echo table {
width: 100%;
}
.echo pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f7f7f7;
border: 0;
border-radius: 3px;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
.echo pre > pre {
padding: 0;
margin: 0;
}
/* Exception Info */
.exception {
margin-top: 20px;
}
.exception .message{
padding: 12px;
border: 1px solid #ddd;
border-bottom: 0 none;
line-height: 18px;
font-size:16px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
font-family: Consolas,"Liberation Mono",Courier,Verdana,"微软雅黑",serif;
}
.exception .code{
float: left;
text-align: center;
color: #fff;
margin-right: 12px;
padding: 16px;
border-radius: 4px;
background: #999;
}
.exception .source-code{
padding: 6px;
border: 1px solid #ddd;
background: #f9f9f9;
overflow-x: auto;
}
.exception .source-code pre{
margin: 0;
}
.exception .source-code pre ol{
margin: 0;
color: #4288ce;
display: inline-block;
min-width: 100%;
box-sizing: border-box;
font-size:14px;
font-family: "Century Gothic",Consolas,"Liberation Mono",Courier,Verdana,serif;
padding-left: <?php echo (isset($source) && !empty($source)) ? parse_padding($source) : 40; ?>px;
}
.exception .source-code pre li{
border-left: 1px solid #ddd;
height: 18px;
line-height: 18px;
}
.exception .source-code pre code{
color: #333;
height: 100%;
display: inline-block;
border-left: 1px solid #fff;
font-size:14px;
font-family: Consolas,"Liberation Mono",Courier,Verdana,"微软雅黑",serif;
}
.exception .trace{
padding: 6px;
border: 1px solid #ddd;
border-top: 0 none;
line-height: 16px;
font-size:14px;
font-family: Consolas,"Liberation Mono",Courier,Verdana,"微软雅黑",serif;
}
.exception .trace h2:hover {
text-decoration: underline;
cursor: pointer;
}
.exception .trace ol{
margin: 12px;
}
.exception .trace ol li{
padding: 2px 4px;
}
.exception div:last-child{
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
/* Exception Variables */
.exception-var table{
width: 100%;
margin: 12px 0;
box-sizing: border-box;
table-layout:fixed;
word-wrap:break-word;
}
.exception-var table caption{
text-align: left;
font-size: 16px;
font-weight: bold;
padding: 6px 0;
}
.exception-var table caption small{
font-weight: 300;
display: inline-block;
margin-left: 10px;
color: #ccc;
}
.exception-var table tbody{
font-size: 13px;
font-family: Consolas, "Liberation Mono", Courier, "微软雅黑",serif;
}
.exception-var table td{
padding: 0 6px;
vertical-align: top;
word-break: break-all;
}
.exception-var table td:first-child{
width: 28%;
font-weight: bold;
white-space: nowrap;
}
.exception-var table td pre{
margin: 0;
}
/* Copyright Info */
.copyright{
margin-top: 24px;
padding: 12px 0;
border-top: 1px solid #eee;
}
/* SPAN elements with the classes below are added by prettyprint. */
pre.prettyprint .pln { color: #000 } /* plain text */
pre.prettyprint .str { color: #080 } /* string content */
pre.prettyprint .kwd { color: #008 } /* a keyword */
pre.prettyprint .com { color: #800 } /* a comment */
pre.prettyprint .typ { color: #606 } /* a type name */
pre.prettyprint .lit { color: #066 } /* a literal value */
/* punctuation, lisp open bracket, lisp close bracket */
pre.prettyprint .pun, pre.prettyprint .opn, pre.prettyprint .clo { color: #660 }
pre.prettyprint .tag { color: #008 } /* a markup tag name */
pre.prettyprint .atn { color: #606 } /* a markup attribute name */
pre.prettyprint .atv { color: #080 } /* a markup attribute value */
pre.prettyprint .dec, pre.prettyprint .var { color: #606 } /* a declaration; a variable name */
pre.prettyprint .fun { color: red } /* a function name */
</style>
</head>
<body>
<?php if (\think\facade\App::isDebug()) { ?>
<?php foreach ($traces as $index => $trace) { ?>
<div class="exception">
<div class="message">
<div class="info">
<div>
<h2><?php echo "#{$index} [{$trace['code']}]" . sprintf('%s in %s', parse_class($trace['name']), parse_file($trace['file'], $trace['line'])); ?></h2>
</div>
<div><h1><?php echo nl2br(htmlentities($trace['message'])); ?></h1></div>
</div>
</div>
<?php if (!empty($trace['source'])) { ?>
<div class="source-code">
<pre class="prettyprint lang-php"><ol start="<?php echo $trace['source']['first']; ?>"><?php foreach ((array) $trace['source']['source'] as $key => $value) { ?><li class="line-<?php echo "{$index}-"; echo $key + $trace['source']['first']; echo $trace['line'] === $key + $trace['source']['first'] ? ' line-error' : ''; ?>"><code><?php echo htmlentities($value); ?></code></li><?php } ?></ol></pre>
</div>
<?php }?>
<div class="trace">
<h2 data-expand="<?php echo 0 === $index ? '1' : '0'; ?>">Call Stack</h2>
<ol>
<li><?php echo sprintf('in %s', parse_file($trace['file'], $trace['line'])); ?></li>
<?php foreach ((array) $trace['trace'] as $value) { ?>
<li>
<?php
// Show Function
if ($value['function']) {
echo sprintf(
'at %s%s%s(%s)',
isset($value['class']) ? parse_class($value['class']) : '',
isset($value['type']) ? $value['type'] : '',
$value['function'],
isset($value['args'])?parse_args($value['args']):''
);
}
// Show line
if (isset($value['file']) && isset($value['line'])) {
echo sprintf(' in %s', parse_file($value['file'], $value['line']));
}
?>
</li>
<?php } ?>
</ol>
</div>
</div>
<?php } ?>
<?php } else { ?>
<div class="exception">
<div class="info"><h1><?php echo htmlentities($message); ?></h1></div>
</div>
<?php } ?>
<?php if (!empty($datas)) { ?>
<div class="exception-var">
<h2>Exception Datas</h2>
<?php foreach ((array) $datas as $label => $value) { ?>
<table>
<?php if (empty($value)) { ?>
<caption><?php echo $label; ?><small>empty</small></caption>
<?php } else { ?>
<caption><?php echo $label; ?></caption>
<tbody>
<?php foreach ((array) $value as $key => $val) { ?>
<tr>
<td><?php echo htmlentities($key); ?></td>
<td><?php echo_value($val); ?></td>
</tr>
<?php } ?>
</tbody>
<?php } ?>
</table>
<?php } ?>
</div>
<?php } ?>
<?php if (!empty($tables)) { ?>
<div class="exception-var">
<h2>Environment Variables</h2>
<?php foreach ((array) $tables as $label => $value) { ?>
<table>
<?php if (empty($value)) { ?>
<caption><?php echo $label; ?><small>empty</small></caption>
<?php } else { ?>
<caption><?php echo $label; ?></caption>
<tbody>
<?php foreach ((array) $value as $key => $val) { ?>
<tr>
<td><?php echo htmlentities($key); ?></td>
<td><?php echo_value($val); ?></td>
</tr>
<?php } ?>
</tbody>
<?php } ?>
</table>
<?php } ?>
</div>
<?php } ?>
<?php if (\think\facade\App::isDebug()) { ?>
<div class="copyright">
<a title="官方网站" href="http://www.thinkphp.cn">ThinkPHP</a>
<span>V<?php echo \think\facade\App::version(); ?></span>
<span>{ -API开发设计的高性能框架 }</span>
<span>- <a title="官方手册" href="https://www.kancloud.cn/manual/thinkphp6_0/content"></a></span>
</div>
<script>
function $(selector, node){
var elements;
node = node || document;
if(document.querySelectorAll){
elements = node.querySelectorAll(selector);
} else {
switch(selector.substr(0, 1)){
case '#':
elements = [node.getElementById(selector.substr(1))];
break;
case '.':
if(document.getElementsByClassName){
elements = node.getElementsByClassName(selector.substr(1));
} else {
elements = get_elements_by_class(selector.substr(1), node);
}
break;
default:
elements = node.getElementsByTagName();
}
}
return elements;
function get_elements_by_class(search_class, node, tag) {
var elements = [], eles,
pattern = new RegExp('(^|\\s)' + search_class + '(\\s|$)');
node = node || document;
tag = tag || '*';
eles = node.getElementsByTagName(tag);
for(var i = 0; i < eles.length; i++) {
if(pattern.test(eles[i].className)) {
elements.push(eles[i])
}
}
return elements;
}
}
$.getScript = function(src, func){
var script = document.createElement('script');
script.async = 'async';
script.src = src;
script.onload = func || function(){};
$('head')[0].appendChild(script);
}
;(function(){
var files = $('.toggle');
var ol = $('ol', $('.prettyprint')[0]);
var li = $('li', ol[0]);
//
for(var i = 0; i < files.length; i++){
files[i].ondblclick = function(){
var title = this.title;
this.title = this.innerHTML;
this.innerHTML = title;
}
}
(function () {
var expand = function (dom, expand) {
var ol = $('ol', dom.parentNode)[0];
expand = undefined === expand ? dom.attributes['data-expand'].value === '0' : undefined;
if (expand) {
dom.attributes['data-expand'].value = '1';
ol.style.display = 'none';
dom.innerText = 'Call Stack (展开)';
} else {
dom.attributes['data-expand'].value = '0';
ol.style.display = 'block';
dom.innerText = 'Call Stack (折叠)';
}
};
var traces = $('.trace');
for (var i = 0; i < traces.length; i ++) {
var h2 = $('h2', traces[i])[0];
expand(h2);
h2.onclick = function () {
expand(this);
};
}
})();
$.getScript('//cdn.bootcdn.net/ajax/libs/prettify/r298/prettify.min.js', function(){
prettyPrint();
});
})();
</script>
<?php } ?>
</body>
</html>

View File

@@ -169,6 +169,10 @@ function speedModeNotice(){
var html = "<div class=\"panel panel-default\"><div class=\"panel-body\">当前浏览器是兼容模式,为确保后台功能正常使用,请切换到<b style='color:#51b72f'>极速模式</b><br>操作方法点击浏览器地址栏右侧的IE符号<b style='color:#51b72f;'><i class='fa fa-internet-explorer fa-fw'></i></b>→选择“<b style='color:#51b72f;'><i class='fa fa-flash fa-fw'></i></b><b style='color:#51b72f;'>极速模式</b>”</div></div>";
$("#browser-notice").html(html)
}
else if(window.location.protocol.indexOf("https")==-1){
var html = "<div class=\"panel panel-default\"><div class=\"panel-body\"><b style='color:#CC3022;'><i class='fa fa-info-circle fa-fw'></i></b>当前正在使用HTTP访问可能存在被窃取敏感信息风险请使用HTTPS访问</div></div>";
$("#browser-notice").html(html)
}
}
speedModeNotice();
</script>

View File

@@ -0,0 +1,181 @@
{extend name="common/layout" /}
{block name="title"}优选IP任务{/block}
{block name="main"}
<style>
.dselect::before{
content: '.';
position: absolute;
left: 0;
}
.tips{color: #f6a838; padding-left: 5px;}
</style>
<div class="row" id="app">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title"><a href="/optimizeip/opiplist" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>{if $action=='edit'}编辑{else}添加{/if}优选IP任务</h3></div>
<div class="panel-body">
<form onsubmit="return false" method="post" class="form-horizontal" role="form" id="taskform">
<div class="form-group">
<label class="col-sm-3 col-xs-12 control-label no-padding-right">域名选择</label>
<div class="col-sm-3 col-xs-5"><input type="text" name="rr" v-model="set.rr" placeholder="主机记录" class="form-control" required></div>
<div class="col-sm-3 col-xs-7 dselect"><select name="did" v-model="set.did" class="form-control" required>
<option value="">--主域名--</option>
{foreach $domains as $k=>$v}
<option value="{$k}">{$v}</option>
{/foreach}
</select></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">CDN服务商</label>
<div class="col-sm-6">
<label class="radio-inline" v-for="(value,key) in cdntypeList">
<input type="radio" name="cdntype" :value="key" v-model="set.cdn_type"> {{value}}
</label>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">解析线路类型</label>
<div class="col-sm-6">
<label class="radio-inline">
<input type="radio" name="type" value="0" v-model="set.type"> 电信/联通/移动线路<span class="tips" title="用于已在CF添加需优化三网访问速度的域名" data-toggle="tooltip" data-placement="bottom" data-original-title=""><i class="fa fa-question-circle"></i></span>
</label>
<label class="radio-inline">
<input type="radio" name="type" value="1" v-model="set.type"> 默认/联通/移动线路<span class="tips" title="将电信优选IP解析到默认线路用于给其他域名提供CNAME服务" data-toggle="tooltip" data-placement="bottom" data-original-title=""><i class="fa fa-question-circle"></i></span>
</label>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">解析IP类型<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="同时开启IPv6&IPv4将会请求2次接口消耗双倍积分"><i class="fa fa-question-circle"></i></span></label>
<div class="col-sm-6">
<label class="checkbox-inline" v-for="option in iptypeList">
<input type="checkbox" name="ip_type" :value="option.value" v-model="set.ip_type_select" required> {{option.label}}
</label><br/>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">每线路解析数量<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="数量不要超过当前域名套餐允许的最大数量,否则会添加解析失败"><i class="fa fa-question-circle"></i></span></label>
<div class="col-sm-6">
<input type="text" name="recordnum" v-model="set.recordnum" placeholder="填写每线路解析数量" class="form-control" data-bv-integer="true" min="1" max="5" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">TTL<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="TTL不要低于当前域名套餐允许的最小值否则会添加解析失败"><i class="fa fa-question-circle"></i></span></label>
<div class="col-sm-6">
<input type="text" name="ttl" v-model="set.ttl" placeholder="填写TTL" class="form-control" data-bv-integer="true" min="1" max="3600" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">备注</label>
<div class="col-sm-6">
<input type="text" name="remark" v-model="set.remark" placeholder="可留空" class="form-control">
</div>
</div>
<div class="form-group" v-show="set.type==0">
<div class="col-sm-offset-3 col-sm-6">
<div class="alert alert-dismissible alert-info">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<strong>提示:</strong>所选域名需保留一个默认线路的解析记录,指向{{cdntypeList[set.cdn_type]}}提供的CNAME地址其他线路的解析记录需要删除添加任务后将自动为当前域名添加电信/联通/移动线路的解析记录。
</div>
</div>
</div>
<div class="form-group" v-show="set.type==1">
<div class="col-sm-offset-3 col-sm-6">
<div class="alert alert-dismissible alert-info">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<strong>提示:</strong>所选域名需删除全部解析记录,添加任务后将自动为当前域名添加解析记录。
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-6"><button type="button" class="btn btn-primary" @click="submit">提交</button></div>
</div>
</form>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>
var action = '{$action}';
var info = {$info|json_encode|raw};
new Vue({
el: '#app',
data: {
action: '{$action}',
set: {
id: '',
remark: '',
rr: '',
did: '',
type: 0,
cdn_type: 1,
ip_type_select: ['v4'],
ip_type: 'v4',
recordnum: 2,
ttl: 600,
},
iptypeList: [
{value:'v4', label:'IPv4(A记录)'},
{value:'v6', label:'IPv6(AAAA记录)'},
],
cdntypeList: {
1:'CloudFlare',
2:"CloudFront",
3:'Gcore'
},
},
watch: {
'set.ip_type_select': function(val){
this.set.ip_type = val.join(',');
}
},
mounted() {
if(this.action == 'edit'){
Object.keys(info).forEach((key) => {
this.$set(this.set, key, info[key])
})
this.set.ip_type_select = this.set.ip_type.split(',')
}
$("#taskform").bootstrapValidator({
live: 'submitted',
});
$('[data-toggle="tooltip"]').tooltip();
},
methods: {
submit(){
var that=this;
$("#taskform").data("bootstrapValidator").validate();
if(!$("#taskform").data("bootstrapValidator").isValid()){
return false;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type: "POST",
url: "",
data: this.set,
dataType: 'json',
success: function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg, {icon: 1}, function(){
if(document.referrer.indexOf('/opiplist?') > 0)
window.location.href = document.referrer;
else
window.location.href = '/optimizeip/opiplist';
});
}else{
layer.alert(data.msg, {icon: 2});
}
},
error: function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
}
},
});
</script>
{/block}

View File

@@ -0,0 +1,184 @@
{extend name="common/layout" /}
{block name="title"}CF优选IP任务管理{/block}
{block name="main"}
<style>
tbody tr>td:nth-child(2){overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:180px;}
</style>
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default panel-intro">
<div class="panel-body">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<div class="form-group">
<label>搜索</label>
<div class="form-group">
<select name="type" class="form-control"><option value="1">域名</option><option value="2">备注</option></select>
</div>
</div>
<div class="form-group">
<input type="text" class="form-control" name="kw" 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>
<a href="/optimizeip/opipform/add" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
</form>
<table id="listTable">
</table>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.20.2/bootstrap-table.min.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.20.2/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 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;
$("#listTable").bootstrapTable({
url: '/optimizeip/opiplist/data',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bordered',
columns: [
{
field: 'id',
title: 'ID'
},
{
field: 'rr',
title: '域名',
formatter: function(value, row, index) {
return '<span title="'+row.remark+'" data-toggle="tooltip" data-placement="right" title="Tooltip on right">' + value + '.' + row.domain + '</span>';
}
},
{
field: 'cdn_type',
title: 'CDN运营商',
formatter: function(value, row, index) {
if(value == 1){
return 'CloudFlare';
}else if(value == 2){
return 'CloudFront';
}else if(value == 3){
return 'Gcore';
}else{
return '未知';
}
}
},
{
field: 'recordnum',
title: '解析数量',
formatter: function(value, row, index) {
return '<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="TTL'+row.ttl+'" class="tips">'+value+'</span>';
}
},
{
field: 'ip_type',
title: '解析IP类型',
formatter: function(value, row, index) {
var value = value.split(',')
value.forEach((element, index) => {
if(element == 'v4') value[index] = 'IPv4'
else if(element == 'v6') value[index] = 'IPv6'
});
return value.join(',');
}
},
{
field: 'active',
title: '任务开关',
formatter: function(value, row, index) {
var html = '';
if(value == 1) {
html += '<span class="btn btn-success btn-xs" onclick="setActive('+row.id+', 0)">开启</span>';
} else {
html += '<span class="btn btn-warning btn-xs" onclick="setActive('+row.id+', 1)">关闭</span>';
}
return html;
}
},
{
field: 'updatetime',
title: '上次更新时间',
formatter: function(value, row, index) {
return value ? value : '无';
}
},
{
field: 'status',
title: '上次更新结果',
formatter: function(value, row, index) {
if(value == 1) {
return '<span class="label label-success">成功</span>';
} else if(value == 2) {
return '<span class="label label-danger">失败</span> <span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="'+row.errmsg+'" class="tips"><i class="fa fa-info-circle"></i></span>';
} else {
return '<span class="label label-warning">未运行</span>';
}
}
},
{
field: '',
title: '操作',
formatter: function(value, row, index) {
var html = '<a href="javascript:runTask(\''+row.id+'\')" class="btn btn-success btn-xs">手动更新</a>&nbsp;&nbsp;';
html += '<a href="/optimizeip/opipform/edit?id='+row.id+'" class="btn btn-primary btn-xs">修改</a>&nbsp;&nbsp;';
html += '<a href="/record/'+row.did+'?keyword='+row.rr+'" class="btn btn-default btn-xs" target="_blank">解析</a>&nbsp;&nbsp;';
html += '<a href="javascript:delItem(\''+row.id+'\')" class="btn btn-danger btn-xs">删除</a>&nbsp;&nbsp;';
return html;
}
},
],
onLoadSuccess: function(data) {
$('[data-toggle="tooltip"]').tooltip()
}
})
})
function setActive(id, active){
$.post('/optimizeip/opipform/setactive', {id: id, active: active}, function(data){
if(data.code == 0) {
layer.msg('修改成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
}
function delItem(id){
layer.confirm('确定要删除此切换策略吗?', {
btn: ['确定','取消']
}, function(){
$.post('/optimizeip/opipform/del', {id: id}, function(data){
if(data.code == 0) {
layer.msg('删除成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
});
}
function runTask(id){
var ii = layer.msg('正在更新中...', {icon: 16,shade: 0.1,time: 0});
$.post('/optimizeip/opipform/run', {id: id}, function(data){
layer.close(ii);
if(data.code == 0) {
layer.alert(data.msg, {icon: 1});
$('#listTable').bootstrapTable('refresh');
} else {
layer.alert(data.msg, {icon: 2});
}
}, 'json');
}
</script>
{/block}

View File

@@ -0,0 +1,105 @@
{extend name="common/layout" /}
{block name="title"}CF优选IP设置{/block}
{block name="main"}
<div class="row">
<div class="col-xs-12 col-md-6">
<div class="panel panel-success">
<div class="panel-heading"><h3 class="panel-title">功能简介</h3></div>
<div class="panel-body">
<p>由于CloudFlare官方IP是泛播路由同一个IP在不同地区不同运营商所链接的机房是不同的速度或延迟也会有区别。目前网上也有很多CF优选CNAME服务然而公共的CNAME可能无法满足稳定性和安全性的需要。</p>
<p>本功能可以获取CloudFlare最新的优选IP地址分为电信/联通/移动线路),并自动更新到域名解析记录。</p>
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading"><h3 class="panel-title">使用说明</h3></div>
<div class="panel-body">
<p><li>数据接口:<a href="https://monitor.gacjie.cn/" target="_blank" rel="noreferrer">GacJieMonitor</a> 数据接口支持CloudFlare、CloudFront、Gcore<a href="https://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>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6">
<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"><select class="form-control" name="optimize_ip_api" default="{:config_get('optimize_ip_api')}"><option value="0">GacJieMonitor</option><option value="1">HostMonit</option></select></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">接口密钥</label>
<div class="col-sm-9"><input type="text" name="optimize_ip_key" value="{:config_get('optimize_ip_key', 'o1zrmHAF')}" class="form-control"/></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"/>
<a href="javascript:queryapi()" class="btn btn-default btn-block">查询积分</a>
</div>
</div>
</form>
</div>
</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 ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '',
data : $(obj).serialize(),
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;
}
function queryapi(){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/optimizeip/queryapi',
data : $("form").serialize(),
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg, {icon: 1});
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
}
</script>
{/block}

View File

@@ -28,7 +28,8 @@
},
"require-dev": {
"symfony/var-dumper": "^4.2",
"topthink/think-trace":"^1.0"
"topthink/think-trace":"^1.0",
"swoole/ide-helper": "^5.1"
},
"autoload": {
"psr-4": {

View File

@@ -28,7 +28,10 @@ return [
// 错误显示信息,非调试模式有效
'error_message' => '页面错误!请稍后再试~',
// 显示错误信息
'show_error_msg' => false,
'show_error_msg' => true,
'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl',
'version' => '1001',
'version' => '1005',
'dbversion' => '1005'
];

View File

@@ -5,5 +5,7 @@
return [
// 指令定义
'commands' => [
'dmtask' => 'app\command\Dmtask',
'opiptask' => 'app\command\Opiptask',
],
];

View File

@@ -51,7 +51,7 @@ return [
// 是否严格检查字段是否存在
'fields_strict' => true,
// 是否需要断线重连
'break_reconnect' => false,
'break_reconnect' => true,
// 监听SQL
'trigger_sql' => env('app_debug', true),
// 开启字段缓存

View File

@@ -12,6 +12,10 @@
// [ 应用入口文件 ]
namespace think;
if (version_compare(PHP_VERSION, '7.4.0', '<')) {
die('require PHP >= 7.4 !');
}
require __DIR__ . '/../vendor/autoload.php';
// 执行HTTP应用并响应

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -23,12 +23,15 @@ Route::any('/login', 'auth/login')->middleware(\think\middleware\SessionInit::cl
->middleware(\app\middleware\ViewOutput::class);
Route::get('/logout', 'auth/logout');
Route::any('/quicklogin', 'auth/quicklogin');
Route::any('/dmtask/status', 'dmonitor/status');
Route::any('/optimizeip/status', 'optimizeip/status');
Route::group(function () {
Route::any('/', 'index/index');
Route::post('/changeskin', 'index/changeskin');
Route::get('/cleancache', 'index/cleancache');
Route::any('/setpwd', 'index/setpwd');
Route::get('/test', 'index/test');
Route::post('/user/data', 'user/user_data');
Route::post('/user/op', 'user/user_op');
@@ -54,8 +57,25 @@ Route::group(function () {
Route::post('/record/remark/:id', 'domain/record_remark');
Route::post('/record/batch/:id', 'domain/record_batch');
Route::any('/record/log/:id', 'domain/record_log');
Route::post('/record/list', 'domain/record_list');
Route::get('/record/:id', 'domain/record');
Route::get('/dmonitor/overview', 'dmonitor/overview');
Route::post('/dmonitor/task/data', 'dmonitor/task_data');
Route::post('/dmonitor/task/log/data/:id', 'dmonitor/tasklog_data');
Route::get('/dmonitor/task/info/:id', 'dmonitor/taskinfo');
Route::any('/dmonitor/task/:action', 'dmonitor/taskform');
Route::get('/dmonitor/task', 'dmonitor/task');
Route::any('/dmonitor/noticeset', 'dmonitor/noticeset');
Route::get('/dmonitor/mailtest', 'dmonitor/mailtest');
Route::post('/dmonitor/clean', 'dmonitor/clean');
Route::any('/optimizeip/opipset', 'optimizeip/opipset');
Route::post('/optimizeip/queryapi', 'optimizeip/queryapi');
Route::post('/optimizeip/opiplist/data', 'optimizeip/opiplist_data');
Route::get('/optimizeip/opiplist', 'optimizeip/opiplist');
Route::any('/optimizeip/opipform/:action', 'optimizeip/opipform');
})->middleware(\app\middleware\CheckLogin::class)
->middleware(\app\middleware\ViewOutput::class);