Browse Source

Massive update of the DDNS module

Improve DDNS related code quality;
Update DNSPod API to version 3.0 (entire rework);
Add GoDaddy | Namecheap | DigitalOcean | Baidu  to DDNS options;
BrettonYe 1 year ago
parent
commit
c83f263cb8

+ 10 - 0
app/Utils/DDNS.php

@@ -3,10 +3,15 @@
 namespace App\Utils;
 
 use App\Utils\DDNS\AliYun;
+use App\Utils\DDNS\Baidu;
 use App\Utils\DDNS\CloudFlare;
+use App\Utils\DDNS\DigitalOcean;
 use App\Utils\DDNS\DNSPod;
+use App\Utils\DDNS\GoDaddy;
+use App\Utils\DDNS\Namecheap;
 use App\Utils\DDNS\Namesilo;
 use App\Utils\Library\Templates\DNS;
+use InvalidArgumentException;
 use Log;
 
 /**
@@ -23,6 +28,11 @@ class DDNS
             'namesilo' => new Namesilo($domain),
             'dnspod' => new DNSPod($domain),
             'cloudflare' => new CloudFlare($domain),
+            'godaddy' => new GoDaddy($domain),
+            'namecheap' => new Namecheap($domain),
+            'digitalocean' => new DigitalOcean($domain),
+            'baidu' => new Baidu($domain),
+            default => throw new InvalidArgumentException('Invalid DDNS mode configuration'),
         };
     }
 

+ 58 - 51
app/Utils/DDNS/AliYun.php

@@ -4,19 +4,21 @@ namespace App\Utils\DDNS;
 
 use App\Utils\Library\Templates\DNS;
 use Arr;
+use Cache;
 use Http;
 use Log;
+use RuntimeException;
 
 class AliYun implements DNS
 {
     //  开发依据: https://api.aliyun.com/document/Alidns/2015-01-09/overview
-    private const API_HOST = 'https://alidns.aliyuncs.com/';
+    private const API_ENDPOINT = 'https://alidns.aliyuncs.com/';
 
     private string $accessKeyID;
 
     private string $accessKeySecret;
 
-    public function __construct(private readonly string $subDomain)
+    public function __construct(private readonly string $subdomain)
     {
         $this->accessKeyID = sysConfig('ddns_key');
         $this->accessKeySecret = sysConfig('ddns_secret');
@@ -24,33 +26,44 @@ class AliYun implements DNS
 
     public function store(string $ip, string $type): bool
     {
-        $domainInfo = $this->analysisDomain();
-        if ($domainInfo) {
-            $ret = $this->send('AddDomainRecord', ['DomainName' => $domainInfo['host'], 'RR' => $domainInfo['rr'], 'Type' => $type, 'Value' => $ip]);
+        $domainInfo = $this->parseDomainInfo();
+
+        if (! $domainInfo) {
+            return false;
         }
 
-        return (bool) ($ret ?? false);
+        return (bool) $this->sendRequest('AddDomainRecord', [
+            'DomainName' => $domainInfo['domain'],
+            'RR' => $domainInfo['sub'],
+            'Type' => $type,
+            'Value' => $ip,
+        ]);
     }
 
-    private function analysisDomain(): array|false
+    private function parseDomainInfo(): array
     {
-        $domains = data_get($this->send('DescribeDomains'), 'Domains.Domain.*.DomainName');
+        $domains = Cache::remember('ddns_get_domains', now()->addHour(), function () {
+            return array_column($this->sendRequest('DescribeDomains')['Domains']['Domain'] ?? [], 'DomainName');
+        });
 
         if ($domains) {
-            foreach ($domains as $domain) {
-                if (str_contains($this->subDomain, $domain)) {
-                    return ['rr' => rtrim(substr($this->subDomain, 0, -strlen($domain)), '.'), 'host' => $domain];
-                }
-            }
-            Log::error('[AliYun - DescribeDomains] 错误域名 '.$this->subDomain.' 不在账号拥有域名里');
+            $matched = Arr::first($domains, fn ($domain) => str_contains($this->subdomain, $domain));
+        }
+
+        if (empty($matched)) {
+            throw new RuntimeException("[AliYun – DescribeDomains] The subdomain {$this->subdomain} does not match any domain in your account.");
         }
 
-        return false;
+        return [
+            'sub' => rtrim(substr($this->subdomain, 0, -strlen($matched)), '.'),
+            'domain' => $matched,
+        ];
     }
 
-    private function send(string $action, array $info = []): array
+    private function sendRequest(string $action, array $parameters = []): array
     {
-        $public = [
+        $parameters = array_merge([
+            'Action' => $action,
             'Format' => 'JSON',
             'Version' => '2015-01-09',
             'AccessKeyId' => $this->accessKeyID,
@@ -58,19 +71,18 @@ class AliYun implements DNS
             'Timestamp' => gmdate("Y-m-d\TH:i:s\Z"), //公共参数Timestamp GMT时间
             'SignatureVersion' => '1.0',
             'SignatureNonce' => str_replace('.', '', microtime(true)), //唯一数,用于防止网络重放攻击
-        ];
-        $parameters = array_merge(['Action' => $action], $public, $info);
-        $parameters['Signature'] = $this->computeSignature($parameters);
+        ], $parameters);
+        $parameters['Signature'] = $this->generateSignature($parameters);
 
-        $response = Http::asForm()->timeout(15)->post(self::API_HOST, $parameters);
+        $response = Http::asForm()->timeout(15)->post(self::API_ENDPOINT, $parameters);
         $data = $response->json();
 
         if ($data) {
-            if ($response->ok()) {
-                return Arr::except($data, ['TotalCount', 'PageSize', 'RequestId', 'PageNumber']);
+            if ($response->successful()) {
+                return $data;
             }
 
-            Log::error('[AliYun - '.$action.'] 返回错误信息:'.$data['Message']);
+            Log::error('[AliYun - '.$action.'] 返回错误信息:'.$data['Message'] ?? 'Unknown error');
         } else {
             Log::error('[AliYun - '.$action.'] 请求失败');
         }
@@ -78,7 +90,7 @@ class AliYun implements DNS
         exit(400);
     }
 
-    private function computeSignature(array $parameters): string
+    private function generateSignature(array $parameters): string
     { // 签名
         ksort($parameters, SORT_STRING);
 
@@ -89,49 +101,44 @@ class AliYun implements DNS
 
     public function update(string $latest_ip, string $original_ip, string $type): bool
     {
-        $records = $this->getRecordId($type, $original_ip);
-        $domainInfo = $this->analysisDomain();
-        if ($records && $domainInfo) {
-            $ret = $this->send('UpdateDomainRecord', ['RR' => $domainInfo['rr'], 'RecordId' => Arr::first($records), 'Type' => $type, 'Value' => $latest_ip]);
+        $recordIds = $this->getRecordIds($type, $original_ip);
+        $domainInfo = $this->parseDomainInfo();
+        if ($recordIds && $domainInfo) {
+            $ret = $this->sendRequest('UpdateDomainRecord', ['RR' => $domainInfo['sub'], 'RecordId' => Arr::first($recordIds), 'Type' => $type, 'Value' => $latest_ip]);
         }
 
         return (bool) ($ret ?? false);
     }
 
-    private function getRecordId(string $type, string $ip): array|false
+    private function getRecordIds(string $type, string $ip): array
     { // 域名信息
-        $parameters = ['SubDomain' => $this->subDomain];
+        $parameters = ['SubDomain' => $this->subdomain];
         if ($type) {
             $parameters['Type'] = $type;
         }
-        $records = $this->send('DescribeSubDomainRecords', $parameters);
-
-        if ($records) {
-            $filtered = data_get($records, 'DomainRecords.Record');
-            if ($ip) {
-                $filtered = Arr::where($filtered, static function (array $value) use ($ip) {
-                    return $value['Value'] === $ip;
-                });
-            }
 
-            return data_get($filtered, '*.RecordId');
+        $records = $this->sendRequest('DescribeSubDomainRecords', $parameters)['DomainRecords']['Record'] ?? [];
+
+        if ($ip) {
+            $records = array_filter($records, static function ($record) use ($ip) {
+                return $record['Value'] === $ip;
+            });
         }
 
-        return false;
+        return array_column($records, 'RecordId');
     }
 
-    public function destroy(string $type = '', string $ip = ''): int
+    public function destroy(string $type, string $ip): int
     {
-        $records = $this->getRecordId($type, $ip);
-        $count = 0;
-        if ($records) {
-            foreach ($records as $record) {
-                if ($this->send('DeleteDomainRecord', ['RecordId' => $record])) {
-                    $count++;
-                }
+        $recordIds = $this->getRecordIds($type, $ip);
+        $deletedCount = 0;
+
+        foreach ($recordIds as $recordId) {
+            if ($this->sendRequest('DeleteDomainRecord', ['RecordId' => $recordId])) {
+                $deletedCount++;
             }
         }
 
-        return $count;
+        return $deletedCount;
     }
 }

+ 161 - 0
app/Utils/DDNS/Baidu.php

@@ -0,0 +1,161 @@
+<?php
+
+namespace App\Utils\DDNS;
+
+use App\Utils\Library\Templates\DNS;
+use Arr;
+use Cache;
+use Http;
+use Log;
+use RuntimeException;
+
+class Baidu implements DNS
+{
+    // 开发依据: https://cloud.baidu.com/doc/DNS/index.html
+    private const API_ENDPOINT = 'https://dns.baidubce.com';
+
+    private string $secretId;
+
+    private string $secretKey;
+
+    private array $domainInfo;
+
+    public function __construct(private readonly string $subdomain)
+    {
+        $this->secretId = sysConfig('ddns_key');
+        $this->secretKey = sysConfig('ddns_secret');
+        $this->domainInfo = $this->parseDomainInfo();
+    }
+
+    private function parseDomainInfo(): array
+    {
+        $domains = Cache::remember('ddns_get_domains', now()->addHour(), function () {
+            return array_column($this->sendRequest('DescribeDomains')['zones'] ?? [], 'name');
+        });
+
+        if ($domains) {
+            $matched = Arr::first($domains, fn ($domain) => str_contains($this->subdomain, $domain));
+        }
+
+        if (empty($matched)) {
+            throw new RuntimeException("[Baidu – DescribeDomains] The subdomain {$this->subdomain} does not match any domain in your account.");
+        }
+
+        return [
+            'sub' => rtrim(substr($this->subdomain, 0, -strlen($matched)), '.'),
+            'domain' => $matched,
+        ];
+    }
+
+    private function sendRequest(string $action, array $parameters = [], ?string $recordId = null): array
+    {
+        $date = gmdate("Y-m-d\TH:i:s\Z");
+        $client = Http::timeout(15)->withHeaders(['Host' => 'dns.baidubce.com', 'x-bce-date' => $date, 'Content-Type' => 'application/json; charset=utf-8'])->baseUrl(self::API_ENDPOINT);
+
+        $path = match ($action) {
+            'DescribeDomains' => '/v1/dns/zone',
+            'DescribeSubDomainRecords', 'AddDomainRecord' => "/v1/dns/zone/{$this->domainInfo['domain']}/record",
+            'UpdateDomainRecord', 'DeleteDomainRecord' => "/v1/dns/zone/{$this->domainInfo['domain']}/record/$recordId",
+        };
+
+        $method = match ($action) {
+            'DescribeDomains', 'DescribeSubDomainRecords' => 'GET',
+            'AddDomainRecord' => 'POST',
+            'UpdateDomainRecord' => 'PUT',
+            'DeleteDomainRecord' => 'DELETE',
+        };
+
+        $client->withHeader('Authorization', $this->generateSignature($parameters, $method, $path, $date));
+
+        $response = match ($method) {
+            'GET' => $client->get($path, $parameters),
+            'POST' => $client->post($path, $parameters),
+            'PUT' => $client->put($path, $parameters),
+            'DELETE' => $client->delete($path, $parameters),
+        };
+
+        $data = $response->json();
+        if ($response->successful()) {
+            return $data ?? [];
+        }
+
+        if ($data) {
+            Log::error('[Baidu – '.$action.'] 返回错误信息:'.$data['message'] ?? 'Unknown error');
+        } else {
+            Log::error('[Baidu – '.$action.'] 请求失败');
+        }
+
+        exit(400);
+    }
+
+    private function generateSignature(array $parameters, string $httpMethod, string $path, string $date): string
+    { // 签名
+        $authStringPrefix = "bce-auth-v1/$this->secretId/$date/1800";
+        $signingKey = hash_hmac('sha256', $authStringPrefix, $this->secretKey);
+        $canonicalRequest = "$httpMethod\n$path\n".http_build_query($parameters)."\nhost:dns.baidubce.com\nx-bce-date:".rawurlencode($date);
+        $signature = hash_hmac('sha256', $canonicalRequest, $signingKey);
+
+        return "$authStringPrefix/host;x-bce-date/$signature";
+    }
+
+    public function store(string $ip, string $type): bool
+    {
+        return $this->sendRequest('AddDomainRecord', [
+            'rr' => $this->domainInfo['sub'],
+            'type' => $type,
+            'value' => $ip,
+        ]) === [];
+    }
+
+    public function update(string $latest_ip, string $original_ip, string $type): bool
+    {
+        $recordIds = $this->getRecordIds($type, $original_ip);
+        if ($recordIds) {
+            return $this->sendRequest('UpdateDomainRecord', [
+                'rr' => $this->domainInfo['sub'],
+                'type' => $type,
+                'value' => $latest_ip,
+            ], $recordIds[0]) === [];
+        }
+
+        return false;
+    }
+
+    private function getRecordIds(string $type, string $ip): array
+    {
+        $parameters = ['rr' => $this->domainInfo['sub']];
+        $response = $this->sendRequest('DescribeSubDomainRecords', $parameters);
+
+        if (isset($response['records'])) {
+            $records = $response['records'];
+
+            if ($ip) {
+                $records = array_filter($records, static function ($record) use ($ip) {
+                    return $record['value'] === $ip;
+                });
+            } elseif ($type) {
+                $records = array_filter($records, static function ($record) use ($type) {
+                    return $record['type'] === $type;
+                });
+            }
+
+            return array_column($records, 'id');
+        }
+
+        return [];
+    }
+
+    public function destroy(string $type, string $ip): int
+    {
+        $recordIds = $this->getRecordIds($type, $ip);
+        $deletedCount = 0;
+
+        foreach ($recordIds as $recordId) {
+            if ($this->sendRequest('DeleteDomainRecord', [], $recordId) === []) {
+                $deletedCount++;
+            }
+        }
+
+        return $deletedCount;
+    }
+}

+ 38 - 35
app/Utils/DDNS/CloudFlare.php

@@ -4,43 +4,48 @@ namespace App\Utils\DDNS;
 
 use App\Utils\Library\Templates\DNS;
 use Arr;
+use Cache;
 use Http;
 use Log;
+use RuntimeException;
 
 class CloudFlare implements DNS
 {
     // 开发依据: https://developers.cloudflare.com/api/
-    private string $apiHost;
+    private string $apiEndpoint;
 
     private array $auth;
 
-    public function __construct(private readonly string $subDomain)
+    public function __construct(private readonly string $subdomain)
     {
-        $this->apiHost = 'https://api.cloudflare.com/client/v4/zones/';
+        $this->apiEndpoint = 'https://api.cloudflare.com/client/v4/zones/';
         $this->auth = ['X-Auth-Key' => sysConfig('ddns_secret'), 'X-Auth-Email' => sysConfig('ddns_key')];
-        $zoneIdentifier = $this->getZone();
+        $zoneIdentifier = $this->getZoneIdentifier();
         if ($zoneIdentifier) {
-            $this->apiHost .= $zoneIdentifier.'/dns_records/';
+            $this->apiEndpoint .= $zoneIdentifier.'/dns_records/';
         }
     }
 
-    private function getZone(): string
+    private function getZoneIdentifier(): string
     {
-        $zones = $this->send('list');
+        $zones = Cache::remember('ddns_get_domains', now()->addHour(), function () {
+            return $this->sendRequest('list');
+        });
+
         if ($zones) {
-            foreach ($zones as $zone) {
-                if (str_contains($this->subDomain, Arr::get($zone, 'name'))) {
-                    return $zone['id'];
-                }
-            }
+            $matched = Arr::first($zones, fn ($zone) => str_contains($this->subdomain, $zone['name']));
         }
 
-        exit(400);
+        if (empty($matched)) {
+            throw new RuntimeException("[CloudFlare – list] The subdomain {$this->subdomain} does not match any domain in your account.");
+        }
+
+        return $matched['id'];
     }
 
-    private function send(string $action, array $parameters = [], ?string $identifier = null): array
+    private function sendRequest(string $action, array $parameters = [], ?string $identifier = null): array
     {
-        $client = Http::timeout(10)->retry(3, 1000)->withHeaders($this->auth)->baseUrl($this->apiHost)->asJson();
+        $client = Http::timeout(10)->retry(3, 1000)->withHeaders($this->auth)->baseUrl($this->apiEndpoint)->asJson();
 
         $response = match ($action) {
             'list' => $client->get(''),
@@ -52,10 +57,10 @@ class CloudFlare implements DNS
 
         $data = $response->json();
         if ($data) {
-            if ($response->ok() && Arr::get($data, 'success')) {
-                return Arr::get($data, 'result');
+            if ($data['success'] && $response->ok()) {
+                return $data['result'] ?? [];
             }
-            Log::error('[CloudFlare - '.$action.'] 返回错误信息:'.Arr::get($data, 'errors.error_chain.message', Arr::get($data, 'errors.0.message')));
+            Log::error('[CloudFlare - '.$action.'] 返回错误信息:'.$data['errors'][0]['message'] ?? 'Unknown error');
         } else {
             Log::error('[CloudFlare - '.$action.'] 请求失败');
         }
@@ -65,28 +70,28 @@ class CloudFlare implements DNS
 
     public function store(string $ip, string $type): bool
     {
-        $ret = $this->send('create', ['content' => $ip, 'name' => $this->subDomain, 'type' => $type]);
+        $result = $this->sendRequest('create', ['content' => $ip, 'name' => $this->subdomain, 'type' => $type]);
 
-        return (bool) $ret;
+        return ! empty($result);
     }
 
     public function update(string $latest_ip, string $original_ip, string $type): bool
     {
-        $recordId = Arr::first($this->getRecordId($type, $original_ip));
+        $recordIds = $this->getRecordIds($type, $original_ip);
 
-        if ($recordId) {
-            $ret = $this->send('update', ['content' => $latest_ip, 'name' => $this->subDomain, 'type' => $type], $recordId);
+        if ($recordIds) {
+            $ret = $this->sendRequest('update', ['content' => $latest_ip, 'name' => $this->subdomain, 'type' => $type], $recordIds[0]);
         }
 
         return (bool) ($ret ?? false);
     }
 
-    private function getRecordId(string $type, string $ip): array|false
+    private function getRecordIds(string $type, string $ip): array|false
     {
-        $records = $this->send('get', ['content' => $ip, 'name' => $this->subDomain, 'type' => $type]);
+        $records = $this->sendRequest('get', ['content' => $ip, 'name' => $this->subdomain, 'type' => $type]);
 
         if ($records) {
-            return data_get($records, '*.id');
+            return array_column($records, 'id');
         }
 
         return false;
@@ -94,17 +99,15 @@ class CloudFlare implements DNS
 
     public function destroy(string $type, string $ip): int
     {
-        $records = $this->getRecordId($type, $ip);
-        $count = 0;
-        if ($records) {
-            foreach ($records as $record) {
-                $ret = $this->send('delete', [], $record);
-                if ($ret) {
-                    $count++;
-                }
+        $recordIds = $this->getRecordIds($type, $ip);
+        $deletedCount = 0;
+
+        foreach ($recordIds as $recordId) {
+            if ($this->sendRequest('delete', [], $recordId)) {
+                $deletedCount++;
             }
         }
 
-        return $count;
+        return $deletedCount;
     }
 }

+ 92 - 67
app/Utils/DDNS/DNSPod.php

@@ -4,128 +4,153 @@ namespace App\Utils\DDNS;
 
 use App\Utils\Library\Templates\DNS;
 use Arr;
+use Cache;
 use Http;
 use Log;
+use RuntimeException;
 
 class DNSPod implements DNS
 {
     // 开发依据: https://docs.dnspod.cn/api/
-    private const API_HOST = 'https://dnsapi.cn/';
+    private const API_ENDPOINT = 'https://dnspod.tencentcloudapi.com';
 
-    private string $loginToken;
+    private string $secretId;
 
-    private array $domainData;
+    private string $secretKey;
 
-    public function __construct(private readonly string $subDomain)
-    {
-        $this->loginToken = sysConfig('ddns_key').','.sysConfig('ddns_secret');
+    private array $domainInfo;
 
-        $data = $this->analysisDomain();
-        if ($data) {
-            $this->domainData = $data;
-        } else {
-            abort(400, '域名存在异常');
-        }
+    public function __construct(private readonly string $subdomain)
+    {
+        $this->secretId = sysConfig('ddns_key');
+        $this->secretKey = sysConfig('ddns_secret');
+        $this->domainInfo = $this->parseDomainInfo();
     }
 
-    private function analysisDomain(): array
+    private function parseDomainInfo(): array
     {
-        $domains = data_get($this->send('Domain.List', ['type' => 'mine']), 'domains.*.name');
+        $domains = Cache::remember('ddns_get_domains', now()->addHour(), function () {
+            return array_column($this->sendRequest('DescribeDomainList', ['Type' => 'mine'])['DomainList'], 'Name');
+        });
+
         if ($domains) {
-            foreach ($domains as $domain) {
-                if (str_contains($this->subDomain, $domain)) {
-                    return ['rr' => rtrim(substr($this->subDomain, 0, -strlen($domain)), '.'), 'host' => $domain];
-                }
-            }
-            Log::error('[DNS] DNSPod - 错误域名 '.$this->subDomain.' 不在账号拥有域名里');
+            $matched = Arr::first($domains, fn ($domain) => str_contains($this->subdomain, $domain));
         }
 
-        exit(400);
+        if (empty($matched)) {
+            throw new RuntimeException("[DNSPod – DescribeDomainList] The subdomain {$this->subdomain} does not match any domain in your account.");
+        }
+
+        return [
+            'sub' => rtrim(substr($this->subdomain, 0, -strlen($matched)), '.'),
+            'domain' => $matched,
+        ];
     }
 
-    private function send(string $action, array $parameters = []): array
+    private function sendRequest(string $action, array $parameters = []): array
     {
-        $response = Http::timeout(15)->asForm()->post(self::API_HOST.$action, array_merge(['login_token' => $this->loginToken, 'format' => 'json'], $parameters));
-
-        if ($response->ok()) {
-            $data = $response->json();
-            if (Arr::get($data, 'status.code') === 1) {
-                return Arr::except($data, ['status']);
+        $timestamp = time();
+        $response = Http::timeout(15)->withHeaders([
+            'X-TC-Action' => $action,
+            'X-TC-Timestamp' => $timestamp,
+            'X-TC-Version' => '2021-03-23',
+            'Authorization' => $this->generateSignature($parameters, $timestamp),
+            'Host' => 'dnspod.tencentcloudapi.com',
+        ])->withBody(json_encode($parameters, JSON_FORCE_OBJECT))->post(self::API_ENDPOINT);
+
+        $data = $response->json();
+        if ($data) {
+            $data = $data['Response'];
+            if ($response->ok()) {
+                return $data;
             }
 
-            Log::error('[DNSPod - '.$action.'] 返回错误信息:'.Arr::get($data, 'status.message'));
+            Log::error('[DNSPod – '.$action.'] 返回错误信息:'.$data['Error']['Message'] ?? 'Unknown error');
         } else {
-            Log::error('[DNSPod - '.$action.'] 请求失败');
+            Log::error('[DNSPod  '.$action.'] 请求失败');
         }
 
         exit(400);
     }
 
+    private function generateSignature(array $parameters, int $timestamp): string
+    { // 签名
+        $date = gmdate('Y-m-d', $timestamp);
+        $canonicalRequest = "POST\n/\n\ncontent-type:application/json\nhost:dnspod.tencentcloudapi.com\n\ncontent-type;host\n".hash('sha256', json_encode($parameters, JSON_FORCE_OBJECT));
+
+        $credentialScope = "$date/dnspod/tc3_request";
+        $stringToSign = "TC3-HMAC-SHA256\n$timestamp\n$credentialScope\n".hash('sha256', $canonicalRequest);
+
+        $secretDate = hash_hmac('SHA256', $date, 'TC3'.$this->secretKey, true);
+        $secretService = hash_hmac('SHA256', 'dnspod', $secretDate, true);
+        $secretSigning = hash_hmac('SHA256', 'tc3_request', $secretService, true);
+        $signature = hash_hmac('SHA256', $stringToSign, $secretSigning);
+
+        return 'TC3-HMAC-SHA256 Credential='.$this->secretId.'/'.$credentialScope.', SignedHeaders=content-type;host, Signature='.$signature;
+    }
+
     public function store(string $ip, string $type): bool
     {
-        $ret = $this->send('Record.Create', [
-            'domain' => $this->domainData['host'],
-            'sub_domain' => $this->domainData['rr'],
-            'record_type' => $type,
-            'record_line_id' => 0,
-            'value' => $ip,
+        return (bool) $this->sendRequest('CreateRecord', [
+            'Domain' => $this->domainInfo['domain'],
+            'SubDomain' => $this->domainInfo['sub'],
+            'RecordType' => $type,
+            'RecordLine' => '默认',
+            'Value' => $ip,
         ]);
-
-        return (bool) $ret;
     }
 
     public function update(string $latest_ip, string $original_ip, string $type): bool
     {
-        $record = Arr::first($this->getRecordId($type, $original_ip));
-        if ($record) {
-            $ret = $this->send('Record.Modify', [
-                'domain' => $this->domainData['host'],
-                'record_id' => $record,
-                'sub_domain' => $this->domainData['rr'],
-                'record_type' => $type,
-                'record_line_id' => 0,
-                'value' => $latest_ip,
+        $recordIds = $this->getRecordIds($type, $original_ip);
+        if ($recordIds) {
+            $result = $this->sendRequest('ModifyRecord', [
+                'Domain' => $this->domainInfo['domain'],
+                'RecordType' => $type,
+                'RecordLine' => '默认',
+                'Value' => $latest_ip,
+                'RecordId' => $recordIds[0],
+                'SubDomain' => $this->domainInfo['sub'],
             ]);
         }
 
-        return (bool) ($ret ?? false);
+        return (bool) ($result ?? false);
     }
 
-    private function getRecordId(string $type, string $ip): array|false
+    private function getRecordIds(string $type, string $ip): array
     {
-        $parameters = ['domain' => $this->domainData['host'], 'sub_domain' => $this->domainData['rr']];
+        $parameters = ['Domain' => $this->domainInfo['domain'], 'Subdomain' => $this->domainInfo['sub']];
         if ($type) {
-            $parameters['record_type'] = $type;
+            $parameters['RecordType'] = $type;
         }
-        $records = $this->send('Record.List', $parameters);
+        $response = $this->sendRequest('DescribeRecordList', $parameters);
+
+        if (isset($response['RecordList'])) {
+            $records = $response['RecordList'];
 
-        if ($records) {
-            $filtered = Arr::get($records, 'records');
             if ($ip) {
-                $filtered = Arr::where($filtered, static function (array $value) use ($ip) {
-                    return $value['value'] === $ip;
+                $records = array_filter($records, static function ($record) use ($ip) {
+                    return $record['Value'] === $ip;
                 });
             }
 
-            return data_get($filtered, '*.id');
+            return array_column($records, 'RecordId');
         }
 
-        return false;
+        return [];
     }
 
     public function destroy(string $type, string $ip): int
     {
-        $records = $this->getRecordId($type, $ip);
-        $count = 0;
-        if ($records) {
-            foreach ($records as $record) {
-                $result = $this->send('Record.Remove', ['domain' => $this->domainData['host'], 'record_id' => $record]);
-                if ($result === []) {
-                    $count++;
-                }
+        $recordIds = $this->getRecordIds($type, $ip);
+        $deletedCount = 0;
+
+        foreach ($recordIds as $recordId) {
+            if ($this->sendRequest('DeleteRecord', ['Domain' => $this->domainInfo['domain'], 'RecordId' => $recordId])) {
+                $deletedCount++;
             }
         }
 
-        return $count;
+        return $deletedCount;
     }
 }

+ 132 - 0
app/Utils/DDNS/DigitalOcean.php

@@ -0,0 +1,132 @@
+<?php
+
+namespace App\Utils\DDNS;
+
+use App\Utils\Library\Templates\DNS;
+use Arr;
+use Cache;
+use Http;
+use Log;
+use RuntimeException;
+
+class DigitalOcean implements DNS
+{
+    //  开发依据:https://docs.digitalocean.com/products/networking/dns/how-to/manage-records/
+    private const API_ENDPOINT = 'https://api.digitalocean.com/v2/domains';
+
+    private string $accessKeySecret;
+
+    private array $domainInfo;
+
+    public function __construct(private readonly string $subdomain)
+    {
+        $this->accessKeySecret = sysConfig('ddns_secret');
+        $this->domainInfo = $this->parseDomainInfo();
+    }
+
+    private function parseDomainInfo(): array
+    {
+        $domains = Cache::remember('ddns_get_domains', now()->addHour(), function () {
+            return array_column($this->sendRequest('DescribeDomains')['domains'] ?? [], 'name');
+        });
+
+        if ($domains) {
+            $matched = Arr::first($domains, fn ($domain) => str_contains($this->subdomain, $domain));
+        }
+
+        if (empty($matched)) {
+            throw new RuntimeException("[DigitalOcean – DescribeDomains] The subdomain {$this->subdomain} does not match any domain in your account.");
+        }
+
+        return [
+            'sub' => rtrim(substr($this->subdomain, 0, -strlen($matched)), '.'),
+            'domain' => $matched,
+        ];
+    }
+
+    private function sendRequest(string $action, array $parameters = []): array|bool
+    {
+        $client = Http::timeout(15)->retry(3, 1000)->withHeader('Authorization', "Bearer $this->accessKeySecret")->baseUrl(self::API_ENDPOINT)->asJson();
+
+        $response = match ($action) {
+            'DescribeDomains' => $client->get(''),
+            'DescribeSubDomainRecords' => $client->get("/{$this->domainInfo['domain']}/records"),
+            'AddDomainRecord' => $client->post("/{$this->domainInfo['domain']}/records", $parameters),
+            'UpdateDomainRecord' => $client->patch("/{$this->domainInfo['domain']}/records/{$parameters['domainRecordId']}", $parameters['data']),
+            'DeleteDomainRecord' => $client->delete("/{$this->domainInfo['domain']}/records/{$parameters['domainRecordId']}"),
+        };
+
+        $data = $response->json();
+        if ($response->successful()) {
+            return $data ?: true;
+        }
+
+        if ($data) {
+            Log::error('[DigitalOcean - '.$action.'] 返回错误信息:'.$data['message'] ?? 'Unknown error');
+        } else {
+            Log::error('[DigitalOcean - '.$action.'] 请求失败');
+        }
+
+        exit(400);
+    }
+
+    public function store(string $ip, string $type): bool
+    {
+        return (bool) $this->sendRequest('AddDomainRecord', ['name' => $this->domainInfo['sub'], 'type' => $type, 'data' => $ip]);
+    }
+
+    public function update(string $latest_ip, string $original_ip, string $type): bool
+    {
+        $recordIds = $this->getRecordIds($type, $original_ip);
+        if ($recordIds) {
+            foreach ($recordIds as $recordId) {
+                $this->sendRequest('UpdateDomainRecord', ['domainRecordId' => $recordId, 'data' => ['type' => $type, 'data' => $latest_ip]]);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    private function getRecordIds(string $type, string $ip): array
+    {
+        $response = $this->sendRequest('DescribeSubDomainRecords');
+
+        if (isset($response['domain_records'])) {
+            $records = $response['domain_records'];
+
+            if ($ip) {
+                $records = array_filter($records, function ($record) use ($ip) {
+                    return $record['data'] === $ip && $record['name'] === $this->domainInfo['sub'];
+                });
+            } elseif ($type) {
+                $records = array_filter($records, function ($record) use ($type) {
+                    return $record['type'] === $type && $record['name'] === $this->domainInfo['sub'];
+                });
+            } else {
+                $records = array_filter($records, function ($record) {
+                    return $record['name'] === $this->domainInfo['sub'];
+                });
+            }
+
+            return array_column($records, 'id');
+        }
+
+        return [];
+    }
+
+    public function destroy(string $type, string $ip): int
+    {
+        $recordIds = $this->getRecordIds($type, $ip);
+        $deletedCount = 0;
+
+        foreach ($recordIds as $recordId) {
+            if ($this->sendRequest('DeleteDomainRecord', ['domainRecordId' => $recordId])) {
+                $deletedCount++;
+            }
+        }
+
+        return $deletedCount;
+    }
+}

+ 123 - 0
app/Utils/DDNS/GoDaddy.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace App\Utils\DDNS;
+
+use App\Utils\Library\Templates\DNS;
+use Arr;
+use Cache;
+use Http;
+use Log;
+use RuntimeException;
+
+class GoDaddy implements DNS
+{
+    //  开发依据: https://developer.godaddy.com/doc/endpoint/domains
+    private const API_ENDPOINT = 'https://api.godaddy.com/v1/domains/';
+
+    private string $accessKeyID;
+
+    private string $accessKeySecret;
+
+    private array $domainInfo;
+
+    public function __construct(private readonly string $subdomain)
+    {
+        $this->accessKeyID = sysConfig('ddns_key');
+        $this->accessKeySecret = sysConfig('ddns_secret');
+        $this->domainInfo = $this->parseDomainInfo();
+    }
+
+    private function parseDomainInfo(): array
+    {
+        $domains = Cache::remember('ddns_get_domains', now()->addHour(), function () {
+            return array_column($this->sendRequest('DescribeDomains') ?? [], 'domain');
+        });
+
+        if ($domains) {
+            $matched = Arr::first($domains, fn ($domain) => str_contains($this->subdomain, $domain));
+        }
+
+        if (empty($matched)) {
+            throw new RuntimeException("[GoDaddy – DescribeDomains] The subdomain {$this->subdomain} does not match any domain in your account.");
+        }
+
+        return [
+            'sub' => rtrim(substr($this->subdomain, 0, -strlen($matched)), '.'),
+            'domain' => $matched,
+        ];
+    }
+
+    private function sendRequest(string $action, array $parameters = []): array|bool
+    {
+        $client = Http::timeout(15)->retry(3, 1000)->withHeader('Authorization', "sso-key $this->accessKeyID:$this->accessKeySecret")->baseUrl(self::API_ENDPOINT)->asJson();
+
+        $response = match ($action) {
+            'DescribeDomains' => $client->get('', ['statuses' => 'ACTIVE']),
+            'DescribeSubDomainRecords' => $client->get("{$parameters['Domain']}/records/{$parameters['Type']}/{$parameters['Sub']}"),
+            'AddDomainRecord' => $client->patch("{$parameters['Domain']}/records", $parameters['Data']),
+            'UpdateDomainRecord' => $client->put("{$parameters['Domain']}/records/{$parameters['Type']}/{$parameters['Sub']}", $parameters['Data']),
+            'DeleteDomainRecord' => $client->delete("{$parameters['Domain']}/records/{$parameters['Type']}/{$parameters['Sub']}"),
+        };
+
+        $data = $response->json();
+        if ($response->successful()) {
+            return $data ?: true;
+        }
+
+        if ($data) {
+            Log::error('[GoDaddy - '.$action.'] 返回错误信息:'.$data['message'] ?? 'Unknown error');
+        } else {
+            Log::error('[GoDaddy - '.$action.'] 请求失败');
+        }
+
+        return false;
+    }
+
+    public function store(string $ip, string $type): bool
+    {
+        $ret = $this->sendRequest('AddDomainRecord', ['Domain' => $this->domainInfo['domain'], 'Data' => [['name' => $this->domainInfo['sub'], 'type' => $type, 'data' => $ip]]]);
+
+        return (bool) ($ret ?: false);
+    }
+
+    public function destroy(string $type, string $ip): bool
+    {
+        if ($ip) {
+            $ret = $this->update('', $ip, $type);
+        } else {
+            $ret = $this->sendRequest('DeleteDomainRecord', ['Sub' => $this->domainInfo['sub'], 'Domain' => $this->domainInfo['domain'], 'Type' => $type]);
+        }
+
+        return (bool) ($ret ?: false);
+    }
+
+    public function update(string $latest_ip, string $original_ip, string $type): bool
+    {
+        $recordIPs = $this->getRecordIPs($type);
+        if ($recordIPs) {
+            $recordIPs = array_values(array_filter($recordIPs, static fn ($ip) => $ip !== $original_ip));
+
+            if ($latest_ip) {
+                $recordIPs[] = $latest_ip;
+            }
+
+            $ret = $this->sendRequest('UpdateDomainRecord', [
+                'Sub' => $this->domainInfo['sub'],
+                'Domain' => $this->domainInfo['domain'],
+                'Type' => $type,
+                'Data' => array_map(static function ($ip) {
+                    return ['data' => $ip];
+                }, $recordIPs),
+            ]);
+        }
+
+        return (bool) ($ret ?? false);
+    }
+
+    private function getRecordIPs(string $type): array
+    {
+        $records = $this->sendRequest('DescribeSubDomainRecords', ['Sub' => $this->domainInfo['sub'], 'Domain' => $this->domainInfo['domain'], 'Type' => $type]);
+
+        return array_column($records, 'data');
+    }
+}

+ 169 - 0
app/Utils/DDNS/Namecheap.php

@@ -0,0 +1,169 @@
+<?php
+
+namespace App\Utils\DDNS;
+
+use App\Utils\IP;
+use App\Utils\Library\Templates\DNS;
+use Arr;
+use Cache;
+use Http;
+use Log;
+use RuntimeException;
+
+class Namecheap implements DNS
+{
+    //  开发依据: https://www.namecheap.com/support/api/methods/
+    private const API_ENDPOINT = 'https://api.namecheap.com/xml.response';
+
+    private string $accessKeyID;
+
+    private string $accessKeySecret;
+
+    private array $domainInfo;
+
+    private array $domainRecords;
+
+    public function __construct(private readonly string $subdomain)
+    {
+        $this->accessKeyID = sysConfig('ddns_key');
+        $this->accessKeySecret = sysConfig('ddns_secret');
+        $this->domainInfo = $this->parseDomainInfo();
+        $this->domainRecords = $this->fetchDomainRecords();
+    }
+
+    private function parseDomainInfo(): array
+    {
+        $domains = Cache::remember('ddns_get_domains', now()->addHour(), function () {
+            return array_map(static function ($domain) {
+                return $domain['@attributes']['Name'];
+            }, $this->sendRequest('namecheap.domains.getList')['DomainGetListResult']['Domain']);
+        });
+
+        if ($domains) {
+            $matched = Arr::first($domains, fn ($domain) => str_contains($this->subdomain, $domain));
+        }
+
+        if (empty($matched)) {
+            throw new RuntimeException("[Namecheap – domains.getList] The subdomain {$this->subdomain} does not match any domain in your account.");
+        }
+
+        $domainParts = explode('.', $matched);
+
+        return [
+            'sub' => rtrim(substr($this->subdomain, 0, -strlen($matched)), '.'),
+            'domain' => $domainParts[0],
+            'tld' => end($domainParts),
+        ];
+    }
+
+    private function sendRequest(string $command, array $parameters = []): array
+    {
+        $parameters = array_merge([
+            'ApiUser' => $this->accessKeyID,
+            'ApiKey' => $this->accessKeySecret,
+            'UserName' => $this->accessKeyID,
+            'ClientIp' => IP::getClientIP(),
+            'Command' => $command,
+        ], $parameters);
+
+        $response = Http::timeout(15)->retry(3, 1000)->get(self::API_ENDPOINT, $parameters);
+        $data = $response->body();
+        if ($data) {
+            $data = json_decode(json_encode(simplexml_load_string($data)), true);
+            if ($response->successful() && $data['@attributes']['Status'] === 'OK') {
+                return $data['CommandResponse'];
+            }
+            Log::error('[Namecheap - '.$command.'] 返回错误信息:'.$data['Errors']['Error'] ?? 'Unknown error');
+        } else {
+            Log::error('[Namecheap - '.$command.'] 请求失败');
+        }
+
+        exit(400);
+    }
+
+    private function fetchDomainRecords(): array
+    {
+        $records = $this->sendRequest('namecheap.domains.dns.getHosts', ['SLD' => $this->domainInfo['domain'], 'TLD' => $this->domainInfo['tld']]);
+
+        if (isset($records['DomainDNSGetHostsResult']['host'])) {
+            $hosts = $records['DomainDNSGetHostsResult']['host'];
+
+            if (isset($hosts[0])) {
+                foreach ($hosts as $record) {
+                    $ret[] = $this->parseRecordData($record);
+                }
+            } else {
+                $ret[] = $this->parseRecordData($hosts);
+            }
+        }
+
+        return $ret ?? [];
+    }
+
+    private function parseRecordData(array $record): array
+    {
+        return [
+            'name' => $record['@attributes']['Name'],
+            'type' => $record['@attributes']['Type'],
+            'address' => $record['@attributes']['Address'],
+            'ttl' => $record['@attributes']['TTL'],
+        ];
+    }
+
+    public function store(string $ip, string $type): bool
+    {
+        $this->domainRecords[] = [
+            'name' => $this->domainInfo['sub'],
+            'type' => $type,
+            'address' => $ip,
+            'ttl' => '60',
+        ];
+
+        return $this->updateDomainRecords();
+    }
+
+    private function updateDomainRecords(): bool
+    {
+        $para = ['SLD' => $this->domainInfo['domain'], 'TLD' => $this->domainInfo['tld']];
+        foreach ($this->domainRecords as $index => $record) {
+            $para['HostName'.($index + 1)] = $record['name'];
+            $para['RecordType'.($index + 1)] = $record['type'];
+            $para['Address'.($index + 1)] = $record['address'];
+            $para['TTL'.($index + 1)] = $record['ttl'];
+        }
+
+        return $this->sendRequest('namecheap.domains.dns.setHosts', $para)['DomainDNSSetHostsResult']['@attributes']['IsSuccess'] === 'true';
+    }
+
+    public function update(string $latest_ip, string $original_ip, string $type): bool
+    {
+        foreach ($this->domainRecords as &$record) {
+            if ($record['address'] === $original_ip && $record['name'] === $this->domainInfo['sub'] && $record['type'] === $type) {
+                $record['address'] = $latest_ip;
+
+                return $this->updateDomainRecords();
+            }
+        }
+
+        return false;
+    }
+
+    public function destroy(string $type, string $ip): int|bool
+    {
+        if ($ip) {
+            $this->domainRecords = array_filter($this->domainRecords, function ($record) use ($ip) {
+                return $record['address'] !== $ip || $record['name'] !== $this->domainInfo['sub'];
+            });
+        } elseif ($type) {
+            $this->domainRecords = array_filter($this->domainRecords, function ($record) use ($type) {
+                return $record['type'] !== $type || $record['name'] !== $this->domainInfo['sub'];
+            });
+        } else {
+            $this->domainRecords = array_filter($this->domainRecords, function ($record) {
+                return $record['name'] !== $this->domainInfo['sub'];
+            });
+        }
+
+        return $this->updateDomainRecords();
+    }
+}

+ 54 - 59
app/Utils/DDNS/Namesilo.php

@@ -4,56 +4,58 @@ namespace App\Utils\DDNS;
 
 use App\Utils\Library\Templates\DNS;
 use Arr;
+use Cache;
 use Log;
+use RuntimeException;
 
 class Namesilo implements DNS
 {
     // 开发依据: https://www.namesilo.com/api-reference
-    private const API_HOST = 'https://www.namesilo.com/api/';
+    private const API_ENDPOINT = 'https://www.namesilo.com/api/';
 
     private string $apiKey;
 
-    private array $domainData;
+    private array $domainInfo;
 
-    public function __construct(private readonly string $subDomain)
+    public function __construct(private readonly string $subdomain)
     {
         $this->apiKey = sysConfig('ddns_key');
-        $data = $this->analysisDomain();
-        if ($data) {
-            $this->domainData = $data;
-        } else {
-            abort(400, '域名存在异常');
-        }
+        $this->domainInfo = $this->parseDomainInfo();
     }
 
-    private function analysisDomain(): array
+    private function parseDomainInfo(): array
     {
-        $domains = Arr::get($this->send('listDomains'), 'domains.domain');
+        $domains = Cache::remember('ddns_get_domains', now()->addHour(), function () {
+            return $this->sendRequest('listDomains')['domains']['domain'];
+        });
+
         if ($domains) {
-            foreach ($domains as $domain) {
-                if (str_contains($this->subDomain, $domain)) {
-                    return ['rr' => rtrim(substr($this->subDomain, 0, -strlen($domain)), '.'), 'host' => $domain];
-                }
-            }
-            Log::error('[DNS] Namesilo - 错误域名 '.$this->subDomain.' 不在账号拥有域名里');
+            $matched = Arr::first($domains, fn ($domain) => str_contains($this->subdomain, $domain));
         }
 
-        exit(400);
+        if (empty($matched)) {
+            throw new RuntimeException("[Namesilo – listDomains] The subdomain {$this->subdomain} does not match any domain in your account.");
+        }
+
+        return [
+            'sub' => rtrim(substr($this->subdomain, 0, -strlen($matched)), '.'),
+            'domain' => $matched,
+        ];
     }
 
-    private function send(string $operation, array $parameters = []): array
+    private function sendRequest(string $operation, array $parameters = []): array
     {
-        $request = simplexml_load_string(file_get_contents(self::API_HOST.$operation.'?'.Arr::query(array_merge(['version' => 1, 'type' => 'xml', 'key' => $this->apiKey], $parameters))));
+        $request = simplexml_load_string(file_get_contents(self::API_ENDPOINT.$operation.'?'.Arr::query(array_merge(['version' => 1, 'type' => 'xml', 'key' => $this->apiKey], $parameters))));
 
         if ($request) {
-            $result = json_decode(json_encode($request), true);
-            if ($result && $result['reply']['code'] === '300') {
-                return Arr::except($result['reply'], ['code', 'detail']);
+            $data = json_decode(json_encode($request), true);
+            if ($data && $data['reply']['code'] === '300') {
+                return $data['reply'];
             }
 
-            Log::error('[Namesilo - '.$operation.'] 返回错误信息:'.$result['reply']['detail']);
+            Log::error('[Namesilo – '.$operation.'] 返回错误信息:'.$data['reply']['detail'] ?? 'Unknown error');
         } else {
-            Log::error('[Namesilo - '.$operation.'] 请求失败');
+            Log::error('[Namesilo  '.$operation.'] 请求失败');
         }
 
         exit(400);
@@ -61,25 +63,23 @@ class Namesilo implements DNS
 
     public function store(string $ip, string $type): bool
     {
-        $ret = $this->send('dnsAddRecord', [
-            'domain' => $this->domainData['host'],
+        return (bool) $this->sendRequest('dnsAddRecord', [
+            'domain' => $this->domainInfo['domain'],
             'rrtype' => $type,
-            'rrhost' => $this->domainData['rr'],
+            'rrhost' => $this->domainInfo['sub'],
             'rrvalue' => $ip,
             'rrttl' => 3600,
         ]);
-
-        return (bool) $ret;
     }
 
     public function update(string $latest_ip, string $original_ip, string $type): bool
     {
-        $record = Arr::first($this->getRecordId($type, $original_ip));
+        $record = Arr::first($this->getRecordIds($type, $original_ip));
         if ($record) {
-            $ret = $this->send('dnsUpdateRecord', [
-                'domain' => $this->domainData['host'],
+            $ret = $this->sendRequest('dnsUpdateRecord', [
+                'domain' => $this->domainInfo['domain'],
                 'rrid' => $record,
-                'rrhost' => $this->domainData['rr'],
+                'rrhost' => $this->domainInfo['sub'],
                 'rrvalue' => $latest_ip,
                 'rrttl' => 3600,
             ]);
@@ -88,49 +88,44 @@ class Namesilo implements DNS
         return (bool) ($ret ?? false);
     }
 
-    private function getRecordId(string $type, string $ip): array|false
+    private function getRecordIds(string $type, string $ip): array|false
     {
-        $records = $this->send('dnsListRecords', ['domain' => $this->domainData['host']]);
+        $response = $this->sendRequest('dnsListRecords', ['domain' => $this->domainInfo['domain']]);
 
-        if (Arr::has($records, 'resource_record')) {
-            $records = $records['resource_record'];
+        if (isset($response['resource_record'])) {
+            $records = $response['resource_record'];
 
             if ($ip) {
-                $filtered = Arr::where($records, function (array $value) use ($ip) {
-                    return $value['host'] === $this->subDomain && $value['value'] === $ip;
+                $records = array_filter($records, function ($record) use ($ip) {
+                    return $record['host'] === $this->subdomain && $record['value'] === $ip;
                 });
             } elseif ($type) {
-                $filtered = Arr::where($records, function (array $value) use ($type) {
-                    return $value['host'] === $this->subDomain && $value['type'] === $type;
+                $records = array_filter($records, function ($record) use ($type) {
+                    return $record['host'] === $this->subdomain && $record['type'] === $type;
                 });
             } else {
-                $filtered = Arr::where($records, function (array $value) {
-                    return $value['host'] === $this->subDomain;
+                $records = array_filter($records, function ($record) {
+                    return $record['host'] === $this->subdomain;
                 });
             }
 
-            if ($filtered) {
-                return data_get($filtered, '*.record_id');
-            }
+            return array_column($records, 'record_id');
         }
 
-        return false;
+        return [];
     }
 
-    public function destroy(string $type = '', string $ip = ''): int
+    public function destroy(string $type, string $ip): int
     {
-        $records = $this->getRecordId($type, $ip);
-        $count = 0;
-        if ($records) {
-            foreach ($records as $record) {
-                $result = $this->send('dnsDeleteRecord', ['domain' => $this->domainData['host'], 'rrid' => $record]);
-
-                if ($result === []) {
-                    $count++;
-                }
+        $recordIds = $this->getRecordIds($type, $ip);
+        $deletedCount = 0;
+
+        foreach ($recordIds as $recordId) {
+            if ($this->sendRequest('dnsDeleteRecord', ['domain' => $this->domainInfo['domain'], 'rrid' => $recordId])) {
+                $deletedCount++;
             }
         }
 
-        return $count;
+        return $deletedCount;
     }
 }

+ 1 - 1
app/Utils/Library/Templates/DNS.php

@@ -8,5 +8,5 @@ interface DNS
 
     public function update(string $latest_ip, string $original_ip, string $type): bool;
 
-    public function destroy(string $type, string $ip): int;
+    public function destroy(string $type, string $ip): int|bool;
 }

+ 4 - 0
resources/lang/zh_CN/admin.php

@@ -781,6 +781,10 @@ return [
             'aliyun' => '阿里云(国际&国内)',
             'dnspod' => 'DNSPod',
             'cloudflare' => 'CloudFlare',
+            'godaddy' => 'GoDaddy',
+            'namecheap' => 'Namecheap',
+            'digitalocean' => 'DigitalOcean',
+            'baidu' => '百度智能云',
         ],
         'captcha' => [
             'standard' => '普通验证码',

+ 1 - 1
resources/views/admin/config/system.blade.php

@@ -130,7 +130,7 @@
                             <x-system.input code="v2ray_tls_provider" :value="$v2ray_tls_provider"/>
                         </x-system.tab-pane>
                         <x-system.tab-pane id="extend">
-                            <x-system.select code="ddns_mode" :list="[trans('common.status.closed') => '', trans('admin.system.ddns.namesilo') => 'namesilo', trans('admin.system.ddns.aliyun') => 'aliyun', trans('admin.system.ddns.dnspod') =>  'dnspod', trans('admin.system.ddns.cloudflare') => 'cloudflare']"/>
+                            <x-system.select code="ddns_mode" :list="[trans('common.status.closed') => '', trans('admin.system.ddns.namesilo') => 'namesilo', trans('admin.system.ddns.aliyun') => 'aliyun', trans('admin.system.ddns.dnspod') =>  'dnspod', trans('admin.system.ddns.cloudflare') => 'cloudflare', trans('admin.system.ddns.godaddy') => 'godaddy', trans('admin.system.ddns.namecheap') => 'namecheap', trans('admin.system.ddns.digitalocean') => 'digitalocean', trans('admin.system.ddns.baidu') => 'baidu']"/>
                             <x-system.input code="ddns_key" :value="$ddns_key"/>
                             <x-system.input code="ddns_secret" :value="$ddns_secret"/>
                             <hr class="col-lg-12">