diff --git a/app/controller/Domain.php b/app/controller/Domain.php index d2b0a0b5..71ce60c4 100644 --- a/app/controller/Domain.php +++ b/app/controller/Domain.php @@ -569,6 +569,11 @@ public function record_add() return json(['code' => -1, 'msg' => '参数不能为空']); } + $reservedCheck = $this->checkReservedRecord($name); + if ($reservedCheck['is_reserved']) { + return json(['code' => -1, 'msg' => $reservedCheck['msg']]); + } + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); $recordid = $dns->addDomainRecord($name, $type, $value, $line, $ttl, $mx, $weight, $remark); if ($recordid) { @@ -604,6 +609,11 @@ public function record_update() return json(['code' => -1, 'msg' => '参数不能为空']); } + $reservedCheck = $this->checkReservedRecord($name); + if ($reservedCheck['is_reserved']) { + return json(['code' => -1, 'msg' => $reservedCheck['msg']]); + } + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); $recordid = $dns->updateDomainRecord($recordid, $name, $type, $value, $line, $ttl, $mx, $weight, $remark); if ($recordid) { @@ -881,10 +891,17 @@ public function record_batch_add() $success = 0; $fail = 0; + $reservedList = []; foreach ($recordlist as $record) { $record = trim($record); $arr = explode(' ', $record); if (empty($record) || empty($arr[0]) || empty($arr[1])) continue; + $reservedCheck = $this->checkReservedRecord($arr[0]); + if ($reservedCheck['is_reserved']) { + $reservedList[] = $arr[0]; + $fail++; + continue; + } $thistype = empty($type) ? getDnsType($arr[1]) : $type; $recordid = $dns->addDomainRecord($arr[0], $thistype, $arr[1], $line, $ttl, $mx, null, $remark); if ($recordid) { @@ -1469,4 +1486,141 @@ public function domain_set_category() $count = Db::name('domain')->where('id', 'in', $ids)->update(['cid' => $cid]); return json(['code' => 0, 'msg' => '成功设置' . $count . '个域名的分类!']); } + + public function record_export() + { + $id = input('param.id/d'); + $format = input('get.format', 'json', 'trim'); + $page = input('get.page/d', 0); + $pagesize = input('get.pagesize/d', 100); + + $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']); + + if ($page === 0) { + $recordLine = cache('record_line_' . $id); + $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); + return json([ + 'code' => 0, + 'data' => [ + 'domain' => $drow['name'], + 'dnstype' => $dnstype, + 'recordLine' => $recordLine, + 'pagesize' => min($pagesize, 100) + ] + ]); + } + + $domainRecords = $dns->getDomainRecords($page, $pagesize); + if (!$domainRecords) return json(['code' => -1, 'msg' => '获取解析记录失败,' . $dns->getError()]); + + $recordLine = cache('record_line_' . $id); + $records = []; + foreach ($domainRecords['list'] as $row) { + $lineName = isset($recordLine[$row['Line']]) ? $recordLine[$row['Line']]['name'] : $row['Line']; + $records[] = [ + 'Name' => $row['Name'], + 'Type' => $row['Type'], + 'Value' => is_array($row['Value']) ? implode(',', $row['Value']) : $row['Value'], + 'Line' => $row['Line'], + 'LineName' => $lineName, + 'TTL' => $row['TTL'], + 'MX' => $row['MX'] ?? '', + 'Weight' => $row['Weight'] ?? '', + 'Remark' => $row['Remark'] ?? '', + 'Status' => $row['Status'], + ]; + } + + return json([ + 'code' => 0, + 'data' => [ + 'records' => $records, + 'total' => $domainRecords['total'], + 'page' => $page, + 'hasMore' => count($records) == $pagesize + ] + ]); + } + + public function record_import() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return $this->alert('error', '域名不存在'); + } + $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + if (request()->isAjax()) { + $name = input('post.name', null, 'trim'); + $type = input('post.type', null, 'trim'); + $value = input('post.value', null, 'trim'); + $line = input('post.line', null, 'trim'); + $ttl = input('post.ttl/d', 600); + $mx = input('post.mx/d', 1); + $weight = input('post.weight/d', 0); + $remark = input('post.remark', null, 'trim'); + + if (empty($name) || empty($type) || empty($value)) { + return json(['code' => -1, 'msg' => '参数不能为空']); + } + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + $recordid = $dns->addDomainRecord($name, $type, $value, $line, $ttl, $mx, $weight, $remark); + if ($recordid) { + $this->add_log($drow['name'], '导入解析', $name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')'); + return json(['code' => 0, 'msg' => '添加解析记录成功!']); + } else { + return json(['code' => -1, 'msg' => '添加解析记录失败,' . $dns->getError()]); + } + } + + list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); + + $recordLineArr = []; + foreach ($recordLine as $key => $item) { + $recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']]; + } + + $dnsconfig = DnsHelper::$dns_config[$dnstype]; + $dnsconfig['type'] = $dnstype; + + View::assign('domainId', $id); + View::assign('domainName', $drow['name']); + View::assign('recordLine', $recordLineArr); + View::assign('minTTL', $minTTL ? $minTTL : 1); + View::assign('dnsconfig', $dnsconfig); + return view('import'); + } + + private function checkReservedRecord($recordName) + { + if (isset(request()->user['level']) && request()->user['level'] == 2) { + return ['is_reserved' => false]; + } + + $reservedConfig = config_get('reserved_records', ''); + if (empty($reservedConfig)) { + return ['is_reserved' => false]; + } + + $reservedList = array_map('trim', explode(',', $reservedConfig)); + $reservedList = array_filter($reservedList); + + if (in_array($recordName, $reservedList)) { + return [ + 'is_reserved' => true, + 'msg' => '主机记录 "' . $recordName . '" 已被系统预留,无法使用' + ]; + } + + return ['is_reserved' => false]; + } } diff --git a/app/controller/User.php b/app/controller/User.php index 91242ce8..58d30d89 100644 --- a/app/controller/User.php +++ b/app/controller/User.php @@ -185,4 +185,31 @@ public function log_data() return json(['total' => $total, 'rows' => $rows]); } + + public function api_manage() + { + if (!checkPermission(1)) return json(['code' => -1, 'msg' => '无权限']); + + $act = input('param.act'); + $userId = $this->request->user['id']; + + if ($act == 'enable_api') { + $apikey = random(16); + Db::name('user')->where('id', $userId)->update([ + 'is_api' => 1, + 'apikey' => $apikey + ]); + return json(['code' => 0, 'msg' => 'API接口已开启', 'apikey' => $apikey]); + } elseif ($act == 'regenerate_apikey') { + $user = Db::name('user')->where('id', $userId)->find(); + if ($user['is_api'] != 1) { + return json(['code' => -1, 'msg' => 'API接口未开启']); + } + $apikey = random(16); + Db::name('user')->where('id', $userId)->update(['apikey' => $apikey]); + return json(['code' => 0, 'msg' => 'API密钥已重新生成', 'apikey' => $apikey]); + } + + return json(['code' => -3, 'msg' => '未知操作']); + } } diff --git a/app/view/common/layout.html b/app/view/common/layout.html index 2804f6c3..2a7b4f31 100644 --- a/app/view/common/layout.html +++ b/app/view/common/layout.html @@ -106,6 +106,9 @@ {if request()->user['type'] eq 'user'}
  • 后台首页
  • {/if} +
  • + API对接 +
  • 域名管理
  • @@ -175,6 +178,7 @@
  • 登录设置
  • 通知设置
  • 代理设置
  • +
  • 保留设置
  • 接口文档
  • diff --git a/app/view/domain/import.html b/app/view/domain/import.html new file mode 100644 index 00000000..73bf39ac --- /dev/null +++ b/app/view/domain/import.html @@ -0,0 +1,602 @@ +{extend name="common/layout" /} +{block name="title"}导入解析记录 - {$domainName}{/block} +{block name="main"} +
    +
    +
    +
    +

    返回导入解析记录 - {$domainName}

    +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + + 选择"跳过重复记录"将自动跳过与现有记录相同的条目 +
    +
    +
    + +
    + + 自动映射会根据线路名称自动匹配当前DNS服务商的线路ID +
    +
    +
    + +
    + +
    +
    + +
    + +
    + + JSON格式示例:
    +
    {
    +  "domain": "example.com",
    +  "dnstype": "aliyun",
    +  "records": [
    +    {
    +      "Name": "www",
    +      "Type": "A",
    +      "Value": "1.2.3.4",
    +      "Line": "default",
    +      "LineName": "默认",
    +      "TTL": 600,
    +      "MX": "",
    +      "Weight": "",
    +      "Remark": ""
    +    }
    +  ]
    +}
    +
    +
    +
    +
    +
    + + 返回 +
    +
    +
    + + +
    +
    +
    +
    +{/block} +{block name="script"} + + +{/block} diff --git a/app/view/domain/record.html b/app/view/domain/record.html index ffcf44aa..bd1a491c 100644 --- a/app/view/domain/record.html +++ b/app/view/domain/record.html @@ -188,7 +188,7 @@

    {if request()->user['type'] eq 'user'}域名别名{/if}
    - +
    @@ -847,6 +847,142 @@

    {if request()->user['type'] eq 'user'} 0){ + currentPage++; + fetchPage(); + } else { + layer.close(ii); + doExportFile(domain, format, allRecords); + } + }, + error: function(){ + layer.close(ii); + layer.msg('导出失败,网络错误', {icon: 2}); + } + }); + } + fetchPage(); + }, + error: function(){ + layer.close(ii); + layer.msg('导出失败,网络错误', {icon: 2}); + } + }); +} + +function doExportFile(domain, format, records) { + if(format === 'json'){ + var exportData = { + domain: domain, + dnstype: dnsconfig.type, + export_time: new Date().toLocaleString(), + records: records + }; + var content = JSON.stringify(exportData, null, 2); + var blob = new Blob([content], {type: 'application/json;charset=utf-8'}); + var filename = domain + '_dns_records_' + formatDate(new Date()) + '.json'; + downloadBlob(blob, filename); + } else { + var csvContent = '\uFEFF'; + csvContent += '主机记录,记录类型,记录值,线路类型,TTL,MX优先级,权重,备注,状态\n'; + for(var i = 0; i < records.length; i++){ + var r = records[i]; + csvContent += escapeCsv(r.Name) + ',' + + r.Type + ',' + + escapeCsv(r.Value) + ',' + + escapeCsv(r.LineName) + ',' + + r.TTL + ',' + + (r.MX || '') + ',' + + (r.Weight || '') + ',' + + escapeCsv(r.Remark || '') + ',' + + (r.Status == '1' ? '启用' : '暂停') + '\n'; + } + var blob = new Blob([csvContent], {type: 'text/csv;charset=utf-8'}); + var filename = domain + '_dns_records_' + formatDate(new Date()) + '.csv'; + downloadBlob(blob, filename); + } + layer.msg('导出成功,共 ' + records.length + ' 条记录', {icon: 1}); +} + +function escapeCsv(value) { + if(value == null) return ''; + value = String(value); + if(value.indexOf(',') !== -1 || value.indexOf('"') !== -1 || value.indexOf('\n') !== -1){ + return '"' + value.replace(/"/g, '""') + '"'; + } + return value; +} + +function formatDate(date) { + var y = date.getFullYear(); + var m = String(date.getMonth()+1).padStart(2, '0'); + var d = String(date.getDate()).padStart(2, '0'); + var h = String(date.getHours()).padStart(2, '0'); + var min = String(date.getMinutes()).padStart(2, '0'); + var s = String(date.getSeconds()).padStart(2, '0'); + return y + m + d + h + min + s; +} + +function downloadBlob(blob, filename) { + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + function copyToClipboard(text, selector) { if (!text && selector) { var el = document.querySelector(selector); diff --git a/app/view/index/setapi.html b/app/view/index/setapi.html new file mode 100644 index 00000000..afaace73 --- /dev/null +++ b/app/view/index/setapi.html @@ -0,0 +1,141 @@ +{extend name="common/layout" /} +{block name="title"}API对接信息{/block} +{block name="main"} + +{/block} +{block name="script"} + +{/block} diff --git a/app/view/system/reservedset.html b/app/view/system/reservedset.html new file mode 100644 index 00000000..517b08b4 --- /dev/null +++ b/app/view/system/reservedset.html @@ -0,0 +1,52 @@ +{extend name="common/layout" /} +{block name="title"}预留主机记录设置{/block} +{block name="main"} +
    +
    +
    +

    预留主机记录设置

    +
    +
    + + 设置预留的主机记录后,普通用户将无法添加这些主机记录的解析。多个主机记录请用逗号分隔,例如:www,@,mail,ftp +
    +
    + + +
    +
    + +
    +
    +
    + +
    +

    说明

    +
    +
      +
    • 预留主机记录是系统级别的设置,对所有域名生效
    • +
    • 普通用户(非管理员)无法添加预留的主机记录
    • +
    • 管理员不受限制,可以添加任何主机记录
    • +
    • 常见需要预留的主机记录:www, @, mail, ftp, admin, api 等
    • +
    +
    +
    +
    +
    +{/block} +{block name="script"} + +{/block} diff --git a/route/app.php b/route/app.php index 7e9bcbcd..801f9d2b 100644 --- a/route/app.php +++ b/route/app.php @@ -36,10 +36,12 @@ Route::post('/changeskin', 'index/changeskin'); Route::get('/cleancache', 'index/cleancache'); Route::any('/setpwd', 'index/setpwd'); + Route::any('/setapi', 'index/setapi'); Route::any('/totp/:action', 'index/totp'); Route::get('/test', 'index/test'); Route::post('/user/data', 'user/user_data'); + Route::post('/user/api_manage/:act', 'user/api_manage'); Route::post('/user/op', 'user/user_op'); Route::get('/user', 'user/user'); @@ -107,6 +109,8 @@ Route::any('/record/batchadd/:id', 'domain/record_batch_add'); Route::get('/record/batchadd', 'domain/record_batch_add2'); Route::any('/record/batchedit', 'domain/record_batch_edit2'); + Route::any('/record/export/:id', 'domain/record_export'); + Route::any('/record/import/:id', 'domain/record_import'); Route::any('/record/log/:id', 'domain/record_log'); Route::post('/record/groups/:id', 'domain/record_groups'); Route::post('/record/list', 'domain/record_list'); @@ -171,6 +175,7 @@ Route::get('/system/customwebhooktest', 'system/customwebhooktest'); Route::post('/system/proxytest', 'system/proxytest'); Route::get('/system/cronset', 'system/cronset'); + Route::get('/system/reservedset', 'system/reservedset'); })->middleware(CheckLogin::class) ->middleware(ViewOutput::class);