mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-05-02 11:56:27 +02:00
```
feat(cloudflare): 添加TXT记录目标域名选择功能 - 新增hostnames_txt_targets接口用于查找TXT记录的目标域名候选列表 - 实现findTxtRecordTargetDomains方法用于匹配最合适的解析域名 - 添加matchHostnameToDomainRecordName方法用于主机名与域名匹配逻辑 - 在前端页面中集成TXT记录快速添加的域名选择弹窗功能 - 更新域名快速切换功能,增加onclick事件处理 - 升级静态资源版本号以确保缓存更新 ```
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace app\controller;
|
||||
|
||||
use app\BaseController;
|
||||
use app\lib\DnsHelper;
|
||||
use app\service\CloudflareEnhanceService;
|
||||
use Exception;
|
||||
use think\facade\Db;
|
||||
@@ -143,6 +144,27 @@ class Cloudflare extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
public function hostnames_txt_targets()
|
||||
{
|
||||
try {
|
||||
$context = $this->getCloudflareDomainContext(input('param.id/d'));
|
||||
$hostname = trim(input('post.hostname', '', 'trim'));
|
||||
if ($hostname === '') {
|
||||
throw new Exception('缺少 TXT 主机名');
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 0,
|
||||
'data' => [
|
||||
'hostname' => $hostname,
|
||||
'candidates' => $this->findTxtRecordTargetDomains($context['domain'], $hostname),
|
||||
],
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage(), 'data' => ['candidates' => []]]);
|
||||
}
|
||||
}
|
||||
|
||||
public function fallback_get()
|
||||
{
|
||||
try {
|
||||
@@ -802,6 +824,85 @@ class Cloudflare extends BaseController
|
||||
];
|
||||
}
|
||||
|
||||
private function findTxtRecordTargetDomains(array $currentDomain, string $hostname): array
|
||||
{
|
||||
$rows = Db::name('domain')->alias('D')
|
||||
->join('account A', 'D.aid = A.id')
|
||||
->field('D.id,D.aid,D.name,A.type account_type,A.name account_name,A.remark account_remark')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$candidates = [];
|
||||
$bestLength = -1;
|
||||
foreach ($rows as $row) {
|
||||
$recordName = $this->matchHostnameToDomainRecordName($hostname, $row['name'] ?? '');
|
||||
if ($recordName === null) {
|
||||
continue;
|
||||
}
|
||||
$domainName = $this->normalizeHostname($row['name'] ?? '');
|
||||
$matchedLength = strlen($domainName);
|
||||
if ($matchedLength > $bestLength) {
|
||||
$bestLength = $matchedLength;
|
||||
$candidates = [];
|
||||
}
|
||||
if ($matchedLength === $bestLength) {
|
||||
$candidates[] = $this->formatTxtTargetCandidate($row, $recordName, intval($currentDomain['id'] ?? 0));
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($candidates)) {
|
||||
$fallbackRecordName = $this->matchHostnameToDomainRecordName($hostname, $currentDomain['name'] ?? '', true);
|
||||
if ($fallbackRecordName !== null) {
|
||||
$candidates[] = $this->formatTxtTargetCandidate([
|
||||
'id' => $currentDomain['id'] ?? 0,
|
||||
'aid' => $currentDomain['aid'] ?? 0,
|
||||
'name' => $currentDomain['name'] ?? '',
|
||||
'account_type' => $currentDomain['type'] ?? '',
|
||||
'account_name' => $currentDomain['account_name'] ?? '',
|
||||
'account_remark' => $currentDomain['account_remark'] ?? '',
|
||||
], $fallbackRecordName, intval($currentDomain['id'] ?? 0));
|
||||
}
|
||||
}
|
||||
|
||||
usort($candidates, function ($a, $b) {
|
||||
if ($a['is_current_domain'] !== $b['is_current_domain']) {
|
||||
return $a['is_current_domain'] ? -1 : 1;
|
||||
}
|
||||
$providerCompare = strcmp($a['account_type_name'], $b['account_type_name']);
|
||||
if ($providerCompare !== 0) {
|
||||
return $providerCompare;
|
||||
}
|
||||
$accountCompare = strcmp($a['account_display_name'], $b['account_display_name']);
|
||||
if ($accountCompare !== 0) {
|
||||
return $accountCompare;
|
||||
}
|
||||
return strcmp($a['domain_name'], $b['domain_name']);
|
||||
});
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
private function formatTxtTargetCandidate(array $row, string $recordName, int $currentDomainId): array
|
||||
{
|
||||
$account = [
|
||||
'id' => intval($row['aid'] ?? 0),
|
||||
'name' => trim((string)($row['account_name'] ?? '')),
|
||||
'remark' => trim((string)($row['account_remark'] ?? '')),
|
||||
];
|
||||
$accountType = trim((string)($row['account_type'] ?? ''));
|
||||
|
||||
return [
|
||||
'domain_id' => intval($row['id'] ?? 0),
|
||||
'domain_name' => trim((string)($row['name'] ?? '')),
|
||||
'record_name' => $recordName,
|
||||
'account_id' => $account['id'],
|
||||
'account_type' => $accountType,
|
||||
'account_type_name' => $this->formatDnsTypeName($accountType),
|
||||
'account_display_name' => $this->formatAccountDisplayName($account),
|
||||
'is_current_domain' => intval($row['id'] ?? 0) === $currentDomainId,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatAccountDisplayName(array $account): string
|
||||
{
|
||||
$name = trim((string)($account['name'] ?? ''));
|
||||
@@ -896,16 +997,44 @@ class Cloudflare extends BaseController
|
||||
if ($domainName === '') {
|
||||
continue;
|
||||
}
|
||||
if ($hostname === $domainName || str_ends_with($hostname, '.' . $domainName)) {
|
||||
if (strlen($domainName) > $bestLength) {
|
||||
$best = $domain;
|
||||
$bestLength = strlen($domainName);
|
||||
}
|
||||
if ($this->matchHostnameToDomainRecordName($hostname, $domainName) !== null && strlen($domainName) > $bestLength) {
|
||||
$best = $domain;
|
||||
$bestLength = strlen($domainName);
|
||||
}
|
||||
}
|
||||
return $best;
|
||||
}
|
||||
|
||||
private function matchHostnameToDomainRecordName(string $hostname, string $domainName, bool $allowRelative = false): ?string
|
||||
{
|
||||
$hostname = preg_replace('/^\*\./', '', $this->normalizeHostname($hostname));
|
||||
$domainName = $this->normalizeHostname($domainName);
|
||||
if ($hostname === '' || $domainName === '') {
|
||||
return null;
|
||||
}
|
||||
if ($hostname === $domainName) {
|
||||
return '@';
|
||||
}
|
||||
if (str_ends_with($hostname, '.' . $domainName)) {
|
||||
return substr($hostname, 0, -strlen($domainName) - 1);
|
||||
}
|
||||
if ($allowRelative) {
|
||||
if ($hostname === '@') {
|
||||
return '@';
|
||||
}
|
||||
if (!str_contains($hostname, '.')) {
|
||||
return $hostname;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function formatDnsTypeName(string $type): string
|
||||
{
|
||||
$dnsList = DnsHelper::getList();
|
||||
return $dnsList[$type]['name'] ?? ($type !== '' ? $type : '-');
|
||||
}
|
||||
|
||||
private function normalizeHostname($hostname): string
|
||||
{
|
||||
$hostname = trim((string)$hostname);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{/volist}
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="quickDomainSwitchBtn" style="vertical-align:middle;margin-right:6px;"><i class="fa fa-random fa-fw"></i> 切换域名</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="quickDomainSwitchBtn" style="vertical-align:middle;margin-right:6px;" onclick="return quickSwitchDomain('/cloudflare/hostnames/')"><i class="fa fa-random fa-fw"></i> 切换域名</button>
|
||||
<a href="/record/{$domainId}" class="btn btn-sm btn-default" style="vertical-align:middle;"><i class="fa fa-reply fa-fw"></i> 返回解析</a>
|
||||
</div>
|
||||
<h3 class="panel-title" style="padding-top:4px;">Cloudflare增强 - {$domainName}</h3>
|
||||
@@ -109,16 +109,16 @@
|
||||
<script src="/static/js/bootstrapValidator.min.js"></script>
|
||||
<script src="/static/js/select2-4.0.13.min.js"></script>
|
||||
<script src="/static/js/select2-i18n-zh-CN-4.0.13.min.js"></script>
|
||||
<script src="/static/js/custom.js"></script>
|
||||
<script src="/static/js/custom.js?v=1005"></script>
|
||||
<script>
|
||||
var currentVerificationHostnameId = '';
|
||||
var currentDomainName = '{$domainName}';
|
||||
|
||||
$(document).ready(function(){
|
||||
initDomainQuickSwitch({
|
||||
currentId: '{$domainId}',
|
||||
currentText: {$domainName|json_encode|raw},
|
||||
type: 'cloudflare',
|
||||
initDomainQuickSwitch({
|
||||
buttonSelector: '',
|
||||
currentId: '{$domainId}',
|
||||
currentText: {$domainName|json_encode|raw},
|
||||
type: 'cloudflare',
|
||||
buildUrl: function(id){
|
||||
return '/cloudflare/hostnames/' + id;
|
||||
}
|
||||
@@ -452,65 +452,145 @@ function fallbackCopyText(text){
|
||||
function quickAddTxtRecord(btn){
|
||||
var fullName = decodeURIComponent($(btn).attr('data-name') || '');
|
||||
var value = decodeURIComponent($(btn).attr('data-value') || '');
|
||||
var rr = convertFullHostnameToRecordName(fullName);
|
||||
if(rr === null){
|
||||
layer.alert('TXT 记录名称与当前域名不匹配,无法自动添加,请手动到解析页添加', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
layer.confirm('确定要快速添加 TXT 记录吗?<br><code>' + htmlEscape(fullName) + '</code>', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/record/add/{$domainId}',
|
||||
data: {
|
||||
name: rr,
|
||||
type: 'TXT',
|
||||
value: value,
|
||||
line: '0',
|
||||
ttl: 600,
|
||||
mx: 1,
|
||||
weight: 0,
|
||||
remark: 'Cloudflare证书校验'
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#modal-verification").modal('show');
|
||||
layer.msg('TXT 记录添加成功', {icon: 1, time: 1200});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
resolveTxtRecordTargets(fullName, function(targets){
|
||||
if(!targets.length){
|
||||
layer.alert('系统中未找到与该 TXT 主机名对应的托管域名,请手动到解析页添加', {icon: 2});
|
||||
return;
|
||||
}
|
||||
if(targets.length === 1){
|
||||
confirmQuickAddTxtRecord(fullName, value, targets[0]);
|
||||
return;
|
||||
}
|
||||
openTxtTargetPicker(fullName, value, targets);
|
||||
});
|
||||
}
|
||||
|
||||
function convertFullHostnameToRecordName(fullName){
|
||||
var name = String(fullName || '').trim().replace(/\.$/, '');
|
||||
var domain = String(currentDomainName || '').trim().replace(/\.$/, '');
|
||||
if(!name || !domain){
|
||||
return null;
|
||||
function resolveTxtRecordTargets(fullName, callback){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/hostnames/txttargets/{$domainId}',
|
||||
data: {hostname: fullName},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
var targets = res.data && $.isArray(res.data.candidates) ? res.data.candidates : [];
|
||||
callback(targets);
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openTxtTargetPicker(fullName, value, targets){
|
||||
var html = '<div style="padding:16px 18px 6px;">';
|
||||
html += '<div class="alert alert-warning" style="margin-bottom:12px;">检测到多个可用解析域名,请确认要写入哪个服务商。</div>';
|
||||
html += '<div class="form-group"><label>TXT 主机名</label><div><code>' + htmlEscape(fullName) + '</code></div></div>';
|
||||
html += '<div class="form-group"><label>TXT 值</label><textarea class="form-control" rows="3" readonly>' + htmlEscape(value) + '</textarea></div>';
|
||||
html += '<form id="txtTargetPickerForm">';
|
||||
for(var i = 0; i < targets.length; i++){
|
||||
var target = targets[i] || {};
|
||||
var providerName = target.account_type_name || target.account_type || '-';
|
||||
var accountName = target.account_display_name || ('账户#' + (target.account_id || ''));
|
||||
html += '<div class="radio" style="margin:0 0 12px;border:1px solid #e5e5e5;border-radius:4px;padding:10px 12px;">';
|
||||
html += '<label style="display:block;padding-left:22px;">';
|
||||
html += '<input type="radio" name="txtTarget" value="' + htmlEscape(String(target.domain_id || '')) + '"' + (i === 0 ? ' checked' : '') + '>';
|
||||
html += '<strong>' + htmlEscape(target.domain_name || '-') + '</strong>';
|
||||
if(target.is_current_domain){
|
||||
html += ' <span class="label label-primary">当前页</span>';
|
||||
}
|
||||
html += '<div class="help-block" style="margin:8px 0 0;">';
|
||||
html += '主机记录:<code>' + htmlEscape(target.record_name || '@') + '</code><br>';
|
||||
html += '服务商:' + htmlEscape(providerName) + '<br>';
|
||||
html += '账户:' + htmlEscape(accountName);
|
||||
html += '</div>';
|
||||
html += '</label></div>';
|
||||
}
|
||||
var lowerName = name.toLowerCase();
|
||||
var lowerDomain = domain.toLowerCase();
|
||||
if(lowerName === lowerDomain){
|
||||
return '@';
|
||||
}
|
||||
if(lowerName.endsWith('.' + lowerDomain)){
|
||||
return name.slice(0, name.length - domain.length - 1);
|
||||
}
|
||||
if(name === '@'){
|
||||
return '@';
|
||||
}
|
||||
if(name.indexOf('.') === -1){
|
||||
return name;
|
||||
html += '</form></div>';
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '选择解析服务商',
|
||||
area: ['640px', 'auto'],
|
||||
shadeClose: false,
|
||||
content: html,
|
||||
btn: ['添加 TXT', '取消'],
|
||||
yes: function(index){
|
||||
var selectedId = $('#txtTargetPickerForm input[name=txtTarget]:checked').val();
|
||||
var target = findTxtTargetByDomainId(targets, selectedId);
|
||||
if(!target){
|
||||
layer.msg('请选择要写入的解析域名', {icon: 2});
|
||||
return;
|
||||
}
|
||||
layer.close(index);
|
||||
submitQuickAddTxtRecord(value, target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirmQuickAddTxtRecord(fullName, value, target){
|
||||
layer.confirm(buildQuickAddConfirmHtml(fullName, target), {title: '提示', icon: 0}, function(index){
|
||||
layer.close(index);
|
||||
submitQuickAddTxtRecord(value, target);
|
||||
});
|
||||
}
|
||||
|
||||
function buildQuickAddConfirmHtml(fullName, target){
|
||||
var providerName = target.account_type_name || target.account_type || '-';
|
||||
var accountName = target.account_display_name || ('账户#' + (target.account_id || ''));
|
||||
return '确定要快速添加 TXT 记录吗?<br><br>'
|
||||
+ 'TXT 主机名:<code>' + htmlEscape(fullName) + '</code><br>'
|
||||
+ '解析域名:<code>' + htmlEscape(target.domain_name || '-') + '</code><br>'
|
||||
+ '主机记录:<code>' + htmlEscape(target.record_name || '@') + '</code><br>'
|
||||
+ '服务商:' + htmlEscape(providerName) + '<br>'
|
||||
+ '账户:' + htmlEscape(accountName);
|
||||
}
|
||||
|
||||
function submitQuickAddTxtRecord(value, target){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/record/add/' + target.domain_id,
|
||||
data: {
|
||||
name: target.record_name,
|
||||
type: 'TXT',
|
||||
value: value,
|
||||
line: '0',
|
||||
ttl: 600,
|
||||
mx: 1,
|
||||
weight: 0,
|
||||
remark: 'Cloudflare证书校验'
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#modal-verification").modal('show');
|
||||
layer.msg('TXT 记录已添加到 ' + (target.domain_name || '-'), {icon: 1, time: 1400});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function findTxtTargetByDomainId(targets, domainId){
|
||||
var selected = String(domainId || '');
|
||||
for(var i = 0; i < targets.length; i++){
|
||||
var item = targets[i] || {};
|
||||
if(String(item.domain_id || '') === selected){
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
|
||||
{/volist}
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="quickDomainSwitchBtn" style="vertical-align:middle;margin-right:6px;"><i class="fa fa-random fa-fw"></i> 切换域名</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="quickDomainSwitchBtn" style="vertical-align:middle;margin-right:6px;" onclick="return quickSwitchDomain('/record/')"><i class="fa fa-random fa-fw"></i> 切换域名</button>
|
||||
{if $user['type'] eq 'user'}<a href="/domain" class="btn btn-sm btn-default" style="vertical-align:middle;"><i class="fa fa-reply fa-fw"></i> 返回</a>{/if}
|
||||
</div>
|
||||
<h3 class="panel-title" style="padding-top:4px;">{$domainName}</h3>
|
||||
@@ -256,7 +256,7 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
|
||||
<script src="/static/js/bootstrapValidator.min.js"></script>
|
||||
<script src="/static/js/select2-4.0.13.min.js"></script>
|
||||
<script src="/static/js/select2-i18n-zh-CN-4.0.13.min.js"></script>
|
||||
<script src="/static/js/custom.js?v=1003"></script>
|
||||
<script src="/static/js/custom.js?v=1005"></script>
|
||||
<script>
|
||||
var recordLine = {$recordLine|json_encode|raw};
|
||||
var dnsconfig = {$dnsconfig|json_encode|raw};
|
||||
@@ -264,13 +264,16 @@ var defaultLine = recordLine[0].id;
|
||||
var sidePagination = dnsconfig.page ? 'client' : 'server';
|
||||
var showWeight = dnsconfig.weight;
|
||||
$(document).ready(function(){
|
||||
initDomainQuickSwitch({
|
||||
currentId: '{$domainId}',
|
||||
currentText: {$domainName|json_encode|raw},
|
||||
buildUrl: function(id){
|
||||
return '/record/' + id;
|
||||
}
|
||||
});
|
||||
if(typeof initDomainQuickSwitch === 'function'){
|
||||
initDomainQuickSwitch({
|
||||
buttonSelector: '',
|
||||
currentId: '{$domainId}',
|
||||
currentText: {$domainName|json_encode|raw},
|
||||
buildUrl: function(id){
|
||||
return '/record/' + id;
|
||||
}
|
||||
});
|
||||
}
|
||||
updateToolbar();
|
||||
let defaultPageSize = getCookie('record_pagesize') ? getCookie('record_pagesize') : 15;
|
||||
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
|
||||
|
||||
@@ -143,6 +143,23 @@ function initDomainQuickSwitch(options){
|
||||
};
|
||||
}
|
||||
|
||||
function quickSwitchDomain(basePath){
|
||||
if(typeof $ === 'undefined'){
|
||||
return false;
|
||||
}
|
||||
var targetId = $.trim($('#quickDomainSwitch').val());
|
||||
if(targetId === ''){
|
||||
if(typeof layer !== 'undefined'){
|
||||
layer.msg('请先选择域名');
|
||||
}else{
|
||||
alert('请先选择域名');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
window.location.href = String(basePath || '') + targetId;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof $.fn.bootstrapTable !== "undefined") {
|
||||
$.fn.bootstrapTable.custom = {
|
||||
method: 'post',
|
||||
|
||||
@@ -57,6 +57,7 @@ Route::group(function () {
|
||||
Route::post('/cloudflare/hostnames/update/:id', 'cloudflare/hostnames_update');
|
||||
Route::post('/cloudflare/hostnames/refresh/:id', 'cloudflare/hostnames_refresh');
|
||||
Route::post('/cloudflare/hostnames/delete/:id', 'cloudflare/hostnames_delete');
|
||||
Route::post('/cloudflare/hostnames/txttargets/:id', 'cloudflare/hostnames_txt_targets');
|
||||
Route::post('/cloudflare/fallback/get/:id', 'cloudflare/fallback_get');
|
||||
Route::post('/cloudflare/fallback/set/:id', 'cloudflare/fallback_set');
|
||||
Route::post('/cloudflare/fallback/delete/:id', 'cloudflare/fallback_delete');
|
||||
|
||||
Reference in New Issue
Block a user