From 33fcf658d77ec75c6501f068f3fbfdcf49e99dac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=8E=96?= <232709122+xiaojiu-code@users.noreply.github.com> Date: Sun, 17 May 2026 20:50:19 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=9F=9F=E5=90=8D?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=AF=BC=E5=85=A5=E5=AF=BC=E5=87=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持将DNS记录导出为JSON/CSV格式,并支持从JSON/CSV文件导入记录 --- app/controller/Domain.php | 271 ++++++++++++++++++++++++++++++++++++ app/view/domain/import.html | 214 ++++++++++++++++++++++++++++ app/view/domain/record.html | 2 +- route/app.php | 2 + 4 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 app/view/domain/import.html diff --git a/app/controller/Domain.php b/app/controller/Domain.php index d2b0a0b..e6f3d77 100644 --- a/app/controller/Domain.php +++ b/app/controller/Domain.php @@ -1469,4 +1469,275 @@ 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'); + + $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->getDomainRecords(1, 1000); + 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'], + ]; + } + + $exportData = [ + 'domain' => $drow['name'], + 'export_time' => date('Y-m-d H:i:s'), + 'records' => $records + ]; + + if ($format === 'csv') { + return $this->exportCsv($exportData); + } else { + return $this->exportJson($exportData); + } + } + + private function exportJson($data) + { + $filename = $data['domain'] . '_dns_records_' . date('YmdHis') . '.json'; + $content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + + return response($content, 200, [ + 'Content-Type' => 'application/json; charset=utf-8', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + 'Content-Length' => strlen($content) + ]); + } + + private function exportCsv($data) + { + $filename = $data['domain'] . '_dns_records_' . date('YmdHis') . '.csv'; + + $output = fopen('php://temp', 'r+'); + fwrite($output, "\xEF\xBB\xBF"); + fputcsv($output, ['主机记录', '记录类型', '记录值', '线路类型', 'TTL', 'MX优先级', '权重', '备注', '状态']); + foreach ($data['records'] as $record) { + fputcsv($output, [ + $record['Name'], + $record['Type'], + $record['Value'], + $record['LineName'], + $record['TTL'], + $record['MX'], + $record['Weight'], + $record['Remark'], + $record['Status'] == '1' ? '启用' : '暂停' + ]); + } + + rewind($output); + $content = stream_get_contents($output); + fclose($output); + + return response($content, 200, [ + 'Content-Type' => 'text/csv; charset=utf-8', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + 'Content-Length' => strlen($content) + ]); + } + 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()) { + $format = input('post.format', 'json', 'trim'); + $override = input('post.override/d', 0); + + $records = []; + if ($format === 'json') { + $data = input('post.data', null, 'trim'); + if (empty($data)) { + return json(['code' => -1, 'msg' => '导入数据不能为空']); + } + $records = $this->parseJsonImport($data); + } else { + $file = request()->file('csvfile'); + if (!$file) { + return json(['code' => -1, 'msg' => '请上传CSV文件']); + } + $fileExt = strtolower($file->getOriginalExtension()); + if (!in_array($fileExt, ['csv', 'txt'])) { + return json(['code' => -1, 'msg' => '仅支持 .csv 或 .txt 格式的文件']); + } + + $filePath = $file->getPathname(); + $data = file_get_contents($filePath); + if (empty($data)) { + return json(['code' => -1, 'msg' => '文件内容为空']); + } + + $records = $this->parseCsvImport($data); + } + + if (empty($records)) { + return json(['code' => -1, 'msg' => '解析导入数据失败,请检查数据格式']); + } + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + + $existingRecords = []; + if (!$override) { + $domainRecords = $dns->getDomainRecords(1, 1000); + if ($domainRecords) { + foreach ($domainRecords['list'] as $row) { + $key = $row['Name'] . '|' . $row['Type'] . '|' . (is_array($row['Value']) ? implode(',', $row['Value']) : $row['Value']); + $existingRecords[$key] = true; + } + } + } + + $success = 0; + $fail = 0; + $skip = 0; + $errors = []; + + foreach ($records as $record) { + if (empty($record['Name']) || empty($record['Type']) || empty($record['Value'])) { + $fail++; + $errors[] = '记录缺少必填字段: ' . json_encode($record); + continue; + } + + if (!$override) { + $key = $record['Name'] . '|' . $record['Type'] . '|' . $record['Value']; + if (isset($existingRecords[$key])) { + $skip++; + continue; + } + } + + $name = $record['Name']; + $type = $record['Type']; + $value = $record['Value']; + $line = $record['Line'] ?? DnsHelper::$line_name[$dnstype]['DEF'] ?? 'default'; + $ttl = $record['TTL'] ?? 600; + $mx = $record['MX'] ?? 1; + $weight = $record['Weight'] ?? null; + $remark = $record['Remark'] ?? null; + + $recordid = $dns->addDomainRecord($name, $type, $value, $line, $ttl, $mx, $weight, $remark); + if ($recordid) { + $this->add_log($drow['name'], '导入解析', $name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')'); + $success++; + } else { + $fail++; + $errors[] = $name . ' [' . $type . '] 添加失败: ' . $dns->getError(); + } + } + + $msg = '导入完成:成功 ' . $success . ' 条'; + if ($skip > 0) $msg .= ',跳过重复 ' . $skip . ' 条'; + if ($fail > 0) $msg .= ',失败 ' . $fail . ' 条'; + + return json([ + 'code' => $fail > 0 && $success == 0 ? -1 : 0, + 'msg' => $msg, + 'data' => [ + 'success' => $success, + 'skip' => $skip, + 'fail' => $fail, + 'errors' => array_slice($errors, 0, 10) + ] + ]); + } + + 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 parseJsonImport($data) + { + $json = json_decode($data, true); + if (!$json || !isset($json['records'])) { + return []; + } + return $json['records']; + } + + private function parseCsvImport($data) + { + $records = []; + $lines = explode("\n", $data); + + if (isset($lines[0]) && substr($lines[0], 0, 3) === "\xEF\xBB\xBF") { + $lines[0] = substr($lines[0], 3); + } + + $startIndex = 0; + foreach ($lines as $i => $line) { + $line = trim($line); + if (empty($line)) continue; + $firstCol = strtolower(str_replace(['"', "'"], '', explode(',', $line)[0] ?? '')); + if (in_array($firstCol, ['主机记录', 'name', '主机名', 'record'])) { + $startIndex = $i + 1; + break; + } + } + + for ($i = $startIndex; $i < count($lines); $i++) { + $line = trim($lines[$i]); + if (empty($line)) continue; + + $cols = str_getcsv($line); + if (count($cols) < 3) continue; + + $records[] = [ + 'Name' => trim($cols[0]), + 'Type' => strtoupper(trim($cols[1])), + 'Value' => trim($cols[2]), + 'LineName' => trim($cols[3] ?? ''), + 'TTL' => is_numeric($cols[4] ?? '') ? intval($cols[4]) : 600, + 'MX' => is_numeric($cols[5] ?? '') ? intval($cols[5]) : 1, + 'Weight' => is_numeric($cols[6] ?? '') ? intval($cols[6]) : null, + 'Remark' => trim($cols[7] ?? ''), + 'Status' => trim($cols[8] ?? '1'), + ]; + } + + return $records; + } } diff --git a/app/view/domain/import.html b/app/view/domain/import.html new file mode 100644 index 0000000..b0bd996 --- /dev/null +++ b/app/view/domain/import.html @@ -0,0 +1,214 @@ +{extend name="common/layout" /} +{block name="title"}导入解析记录 - {$domainName}{/block} +{block name="main"} +
+
+
+
+

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

+
+
+
+
+ +
+ +
+
+
+ +
+ + 选择"跳过重复记录"将自动跳过与现有记录相同的条目 +
+
+
+ +
+ +
+
+ +
+ +
+ + JSON格式示例:
+
{
+  "domain": "example.com",
+  "records": [
+    {
+      "Name": "www",
+      "Type": "A",
+      "Value": "1.2.3.4",
+      "Line": "default",
+      "TTL": 600,
+      "MX": "",
+      "Weight": "",
+      "Remark": ""
+    }
+  ]
+}
+
+
+
+
+
+ + 返回 +
+
+
+
+
+
+
+{/block} +{block name="script"} + + +{/block} \ No newline at end of file diff --git a/app/view/domain/record.html b/app/view/domain/record.html index ffcf44a..5fb2505 100644 --- a/app/view/domain/record.html +++ b/app/view/domain/record.html @@ -188,7 +188,7 @@

{if request()->user['type'] eq 'user'}域名别名{/if}
- +
diff --git a/route/app.php b/route/app.php index 7e9bcbc..908d938 100644 --- a/route/app.php +++ b/route/app.php @@ -107,6 +107,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'); From 6a8ab59d0da3b0c3ff717688f00e4b07ffa77d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=8E=96?= <232709122+xiaojiu-code@users.noreply.github.com> Date: Sun, 17 May 2026 21:05:21 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0API=E5=AF=B9=E6=8E=A5?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加API对接页面,用户可自由开关API的启用和关闭重新生成密钥 --- app/controller/User.php | 27 +++++++ app/view/common/layout.html | 3 + app/view/index/setapi.html | 141 ++++++++++++++++++++++++++++++++++++ route/app.php | 2 + 4 files changed, 173 insertions(+) create mode 100644 app/view/index/setapi.html diff --git a/app/controller/User.php b/app/controller/User.php index 91242ce..58d30d8 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 2804f6c..7e3b46a 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对接 +
  • 域名管理
  • diff --git a/app/view/index/setapi.html b/app/view/index/setapi.html new file mode 100644 index 0000000..afaace7 --- /dev/null +++ b/app/view/index/setapi.html @@ -0,0 +1,141 @@ +{extend name="common/layout" /} +{block name="title"}API对接信息{/block} +{block name="main"} +
    +
    +
    +

    API对接信息

    +
    +
    + 以下信息用于API接口对接,请妥善保管,不要泄露给他人,如怀疑密钥泄露,请及时重新生成。 +
    +
    + + + + + + + + + + + + + + + + + + + +
    接口地址 + {$siteurl}/ + +
    账户UID + {$user.id} + +
    API密钥 + {if $user.is_api == 1} + {$user.apikey} + + + {else} + API接口未开启 + 开启API + {/if} +
    API接口状态 + {if $user.is_api == 1} + 已开启 + {else} + 未开启 + {/if} +
    +
    +
    +
    + +
    +

    API对接教程

    +
    +
    同系统对接
    +

    您可以通过API将本账户可管理的域名对接到您自己的聚合DNS管理系统中,实现统一管理。 +

    +

    系统下载:

    +

    + 下载聚合DNS管理系统 +

    +

    使用教程:

    +
      +
    1. 在后台域名账户配置中选择同系统对接插件,在配置中填写相关信息
    2. +
    3. 保存配置后即可实现系统对接
    4. +
    +
    +
    +
    +
    +{/block} +{block name="script"} + +{/block} diff --git a/route/app.php b/route/app.php index 908d938..002e35f 100644 --- a/route/app.php +++ b/route/app.php @@ -38,8 +38,10 @@ Route::any('/setpwd', 'index/setpwd'); Route::any('/totp/:action', 'index/totp'); Route::get('/test', 'index/test'); + Route::get('/setapi', 'index/setapi'); 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'); From 8207e10e6e7f33840861cce127ff9aa50bf0e397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=8E=96?= <232709122+xiaojiu-code@users.noreply.github.com> Date: Sun, 17 May 2026 21:20:53 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A2=84=E7=95=99?= =?UTF-8?q?=E4=B8=BB=E6=9C=BA=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在域名记录添加和修改前检查是否为预留记录,管理员级别用户不受预留记录限制 --- app/controller/Domain.php | 41 +++++++++++++++++++++++++ app/view/common/layout.html | 1 + app/view/system/reservedset.html | 52 ++++++++++++++++++++++++++++++++ route/app.php | 1 + 4 files changed, 95 insertions(+) create mode 100644 app/view/system/reservedset.html diff --git a/app/controller/Domain.php b/app/controller/Domain.php index e6f3d77..5124639 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) { @@ -1740,4 +1757,28 @@ private function parseCsvImport($data) return $records; } + + 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/view/common/layout.html b/app/view/common/layout.html index 7e3b46a..2a7b4f3 100644 --- a/app/view/common/layout.html +++ b/app/view/common/layout.html @@ -178,6 +178,7 @@
  • 登录设置
  • 通知设置
  • 代理设置
  • +
  • 保留设置
  • 接口文档
  • diff --git a/app/view/system/reservedset.html b/app/view/system/reservedset.html new file mode 100644 index 0000000..517b08b --- /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 002e35f..50ac80b 100644 --- a/route/app.php +++ b/route/app.php @@ -175,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); From c7a8dc767b1da23b922b3fcfd39d23f74ae602cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=8E=96?= <232709122+xiaojiu-code@users.noreply.github.com> Date: Sun, 17 May 2026 21:23:22 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- route/app.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/route/app.php b/route/app.php index 50ac80b..801f9d2 100644 --- a/route/app.php +++ b/route/app.php @@ -36,9 +36,9 @@ 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::get('/setapi', 'index/setapi'); Route::post('/user/data', 'user/user_data'); Route::post('/user/api_manage/:act', 'user/api_manage'); From 17e059867acaa7ae36dc7294e0ace1cc712701ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=8E=96?= <2829981162@qq.com> Date: Mon, 1 Jun 2026 16:30:29 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持分页导出和可视化导入流程 --- app/controller/Domain.php | 242 +++-------------- app/view/domain/import.html | 502 ++++++++++++++++++++++++++++++++---- app/view/domain/record.html | 138 +++++++++- 3 files changed, 624 insertions(+), 258 deletions(-) diff --git a/app/controller/Domain.php b/app/controller/Domain.php index 5124639..71ce60c 100644 --- a/app/controller/Domain.php +++ b/app/controller/Domain.php @@ -1491,6 +1491,8 @@ 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) { @@ -1499,7 +1501,22 @@ public function record_export() if (!checkPermission(0, $drow['name'])) return json(['code' => -1, 'msg' => '无权限']); $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - $domainRecords = $dns->getDomainRecords(1, 1000); + + 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); @@ -1520,62 +1537,17 @@ public function record_export() ]; } - $exportData = [ - 'domain' => $drow['name'], - 'export_time' => date('Y-m-d H:i:s'), - 'records' => $records - ]; - - if ($format === 'csv') { - return $this->exportCsv($exportData); - } else { - return $this->exportJson($exportData); - } - } - - private function exportJson($data) - { - $filename = $data['domain'] . '_dns_records_' . date('YmdHis') . '.json'; - $content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); - - return response($content, 200, [ - 'Content-Type' => 'application/json; charset=utf-8', - 'Content-Disposition' => 'attachment; filename="' . $filename . '"', - 'Content-Length' => strlen($content) + return json([ + 'code' => 0, + 'data' => [ + 'records' => $records, + 'total' => $domainRecords['total'], + 'page' => $page, + 'hasMore' => count($records) == $pagesize + ] ]); } - private function exportCsv($data) - { - $filename = $data['domain'] . '_dns_records_' . date('YmdHis') . '.csv'; - - $output = fopen('php://temp', 'r+'); - fwrite($output, "\xEF\xBB\xBF"); - fputcsv($output, ['主机记录', '记录类型', '记录值', '线路类型', 'TTL', 'MX优先级', '权重', '备注', '状态']); - foreach ($data['records'] as $record) { - fputcsv($output, [ - $record['Name'], - $record['Type'], - $record['Value'], - $record['LineName'], - $record['TTL'], - $record['MX'], - $record['Weight'], - $record['Remark'], - $record['Status'] == '1' ? '启用' : '暂停' - ]); - } - - rewind($output); - $content = stream_get_contents($output); - fclose($output); - - return response($content, 200, [ - 'Content-Type' => 'text/csv; charset=utf-8', - 'Content-Disposition' => 'attachment; filename="' . $filename . '"', - 'Content-Length' => strlen($content) - ]); - } public function record_import() { $id = input('param.id/d'); @@ -1587,105 +1559,27 @@ public function record_import() if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); if (request()->isAjax()) { - $format = input('post.format', 'json', 'trim'); - $override = input('post.override/d', 0); - - $records = []; - if ($format === 'json') { - $data = input('post.data', null, 'trim'); - if (empty($data)) { - return json(['code' => -1, 'msg' => '导入数据不能为空']); - } - $records = $this->parseJsonImport($data); - } else { - $file = request()->file('csvfile'); - if (!$file) { - return json(['code' => -1, 'msg' => '请上传CSV文件']); - } - $fileExt = strtolower($file->getOriginalExtension()); - if (!in_array($fileExt, ['csv', 'txt'])) { - return json(['code' => -1, 'msg' => '仅支持 .csv 或 .txt 格式的文件']); - } - - $filePath = $file->getPathname(); - $data = file_get_contents($filePath); - if (empty($data)) { - return json(['code' => -1, 'msg' => '文件内容为空']); - } - - $records = $this->parseCsvImport($data); - } + $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($records)) { - return json(['code' => -1, 'msg' => '解析导入数据失败,请检查数据格式']); + if (empty($name) || empty($type) || empty($value)) { + return json(['code' => -1, 'msg' => '参数不能为空']); } $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - - $existingRecords = []; - if (!$override) { - $domainRecords = $dns->getDomainRecords(1, 1000); - if ($domainRecords) { - foreach ($domainRecords['list'] as $row) { - $key = $row['Name'] . '|' . $row['Type'] . '|' . (is_array($row['Value']) ? implode(',', $row['Value']) : $row['Value']); - $existingRecords[$key] = true; - } - } - } - - $success = 0; - $fail = 0; - $skip = 0; - $errors = []; - - foreach ($records as $record) { - if (empty($record['Name']) || empty($record['Type']) || empty($record['Value'])) { - $fail++; - $errors[] = '记录缺少必填字段: ' . json_encode($record); - continue; - } - - if (!$override) { - $key = $record['Name'] . '|' . $record['Type'] . '|' . $record['Value']; - if (isset($existingRecords[$key])) { - $skip++; - continue; - } - } - - $name = $record['Name']; - $type = $record['Type']; - $value = $record['Value']; - $line = $record['Line'] ?? DnsHelper::$line_name[$dnstype]['DEF'] ?? 'default'; - $ttl = $record['TTL'] ?? 600; - $mx = $record['MX'] ?? 1; - $weight = $record['Weight'] ?? null; - $remark = $record['Remark'] ?? null; - - $recordid = $dns->addDomainRecord($name, $type, $value, $line, $ttl, $mx, $weight, $remark); - if ($recordid) { - $this->add_log($drow['name'], '导入解析', $name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')'); - $success++; - } else { - $fail++; - $errors[] = $name . ' [' . $type . '] 添加失败: ' . $dns->getError(); - } + $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()]); } - - $msg = '导入完成:成功 ' . $success . ' 条'; - if ($skip > 0) $msg .= ',跳过重复 ' . $skip . ' 条'; - if ($fail > 0) $msg .= ',失败 ' . $fail . ' 条'; - - return json([ - 'code' => $fail > 0 && $success == 0 ? -1 : 0, - 'msg' => $msg, - 'data' => [ - 'success' => $success, - 'skip' => $skip, - 'fail' => $fail, - 'errors' => array_slice($errors, 0, 10) - ] - ]); } list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); @@ -1706,58 +1600,6 @@ public function record_import() return view('import'); } - private function parseJsonImport($data) - { - $json = json_decode($data, true); - if (!$json || !isset($json['records'])) { - return []; - } - return $json['records']; - } - - private function parseCsvImport($data) - { - $records = []; - $lines = explode("\n", $data); - - if (isset($lines[0]) && substr($lines[0], 0, 3) === "\xEF\xBB\xBF") { - $lines[0] = substr($lines[0], 3); - } - - $startIndex = 0; - foreach ($lines as $i => $line) { - $line = trim($line); - if (empty($line)) continue; - $firstCol = strtolower(str_replace(['"', "'"], '', explode(',', $line)[0] ?? '')); - if (in_array($firstCol, ['主机记录', 'name', '主机名', 'record'])) { - $startIndex = $i + 1; - break; - } - } - - for ($i = $startIndex; $i < count($lines); $i++) { - $line = trim($lines[$i]); - if (empty($line)) continue; - - $cols = str_getcsv($line); - if (count($cols) < 3) continue; - - $records[] = [ - 'Name' => trim($cols[0]), - 'Type' => strtoupper(trim($cols[1])), - 'Value' => trim($cols[2]), - 'LineName' => trim($cols[3] ?? ''), - 'TTL' => is_numeric($cols[4] ?? '') ? intval($cols[4]) : 600, - 'MX' => is_numeric($cols[5] ?? '') ? intval($cols[5]) : 1, - 'Weight' => is_numeric($cols[6] ?? '') ? intval($cols[6]) : null, - 'Remark' => trim($cols[7] ?? ''), - 'Status' => trim($cols[8] ?? '1'), - ]; - } - - return $records; - } - private function checkReservedRecord($recordName) { if (isset(request()->user['level']) && request()->user['level'] == 2) { diff --git a/app/view/domain/import.html b/app/view/domain/import.html index b0bd996..73bf39a 100644 --- a/app/view/domain/import.html +++ b/app/view/domain/import.html @@ -21,17 +21,27 @@

    - 选择"跳过重复记录"将自动跳过与现有记录相同的条目

    +
    + +
    + + 自动映射会根据线路名称自动匹配当前DNS服务商的线路ID +
    +
    - +
    + + @@ -79,6 +134,9 @@

    -{/block} \ No newline at end of file +{/block} diff --git a/app/view/domain/record.html b/app/view/domain/record.html index 5fb2505..bd1a491 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);