ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

迅风DNS Pro实战:基于泛解析与DNS API的二级域名自动分发系统

迅风DNS Pro实战:基于泛解析与DNS API的二级域名自动分发系统

简介:这是一套迅风DNS Pro二级域名分发系统V3.1.2完整源码,附搭建教程,面向需要自建域名分发管理平台的站长、服务商或技术团队。系统采用PHP开发,通过插件接入阿里、腾讯、Cloudflare等多家DNS服务商,一个后台即可完成解析管理、订单支付、工单处理与日志审计,并支持API调用、批量操作和实名认证等扩展功能。资源包共2000个文件,以PHP源码为主(1257个),辅以JS、CSS前端资源,以及JSON配置、SQL数据表、MD文档和部署所需的环境文件,压缩包大小约11.26MB,内容完整可用于生产部署。已有205人学习下载。这套源码附带完整搭建教程,从环境配置、数据库导入到站点初始化的全流程指引,适合具备一定PHP基础、希望快速搭建或二次开发域名分发系统的开发者参考。

1. 迅风 DNS Pro 二级域名分发:一个能自动开解析的面板

做短链接跳转、临时测试站、个人主页托管这类业务的朋友,最怕的就是“解析手工加到手软”。每来一个用户都得先去 DNS 后台加一条子域名记录,量大之后根本顾不过来,还容易把主域名配置改坏。迅风 DNS Pro V3.1.2 就是解决这件事的:一套自带安装教程的二级域名分发系统源码,用户在前台自助申请子域名,后台走 DNS 厂商 API 自动添加解析记录,到期自动回收,从零开始搭建大约两三个小时就能上线。适合手上有空闲主域名、想开二级域名分发服务或做站群管理的开发者和站长,也适合刚接触 PHP 项目想完整走一遍部署流程的新手。

2. 分发系统核心原理:泛解析、DNS API 与状态机这三件事

2.1 为什么这套源码选 DNS API,而不是直接改 Bind 文件

很多人拿到这类源码会先问:它为什么不直接改服务器上的 Bind 配置?确实有一批老系统是这么做的,把泛解析记录写进 zone 文件再 reload,好处是服务器自己说了算,不依赖外部厂商。但坏处也明显:第一,你的 DNS 服务和业务跑在同一台机器上,机器一挂,所有二级域名全部解析失败;第二,zone 文件格式敏感度极高,少一个括号、多一个分号,整组域名直接解析异常,这种翻车在生产环境里很难快速恢复。

V3.1.2 源码走的是另一条路:主域名托管在云厂商 DNS 上(阿里云 DNS、DNSPod 这类),系统通过厂商开放的 API 动态添加和删除解析记录。控制端做业务逻辑,DNS 厂商做解析容灾,两边互不干扰。这样做还有一个好处:API 层面的操作是可以被审计和限制的,比如只授权某个子域名前缀、限制每分钟请求次数,不会像改 Bind 那样引发全局风险。

用一个场景来对比,你就明白为什么这个选择对“分发”业务更合理。假设你的主域名下有 2000 个用户子域名,每个域名每张记录 600 秒 TTL,用户申请、续期、回收的频率一高,直接在 Bind 文件里做字符串替换几乎没法维护。API 模式则是程序拿到用户输入后,拼一个参数数组,调一次厂商接口,返回成功就落库,返回失败就记日志重试,整个流程是可编程、可追踪的。

2.2 一条二级域名从申请到可访问要经过的四步

在 V3.1.2 里,用户提交一个子域名的申请,并不是只加一条 A 记录那么简单,我拆给你看:

第一步是校验。程序会检查子域名是否在保留名单里(www、api、admin、mail 这些通常都被锁死),是否符合长度和字符规则,是否触发敏感词过滤,以及该用户剩余点数是否足够。校验不通过直接返回错误信息,不产生任何 API 调用。

第二步是调用 DNS API。程序拿到用户填写的子域名前缀和目标解析 IP,组装成请求参数。比如在 PHP 代码里,核心逻辑类似于这样:

$params = [ 'domain' => 'example.com', // 主域名 'sub_domain'=> $subdomain, // 用户申请的二级域名前缀 'record_type' => 'A', // 记录类型固定为 A 'record_line' => '默认', // 解析线路,国内厂商一般是“默认” 'value' => $serverIp, // 指向你的服务器 IP 'ttl' => $config['default_ttl'] // 默认 600 秒 ]; $result = $dnsApi->addRecord($params);

这段代码的关键点有两个:第一是$subdomain必须做白名单校验,不能允许用户提交@或*这种危险前缀,否则等于把整个域名控制权交出去了;第二是record_line字段,有些厂商支持默认、电信、联通、移动分线路解析,分发场景一般只填“默认”即可,填了多个线路反而会让 TTL 和缓存策略变复杂。调用成功后,程序把记录 ID 存回数据库,状态标记为“已解析”。

第三步是生成 Nginx 站点配置。程序检测到 DNS 记录添加成功后,会自动在当前服务器的 Nginx 配置目录里生成一个以该子域名为文件名的 vhost 配置,指向站点根目录,然后执行nginx -t && nginx -s reload,让新域名立即可以被 Web 服务响应。

第四步是回写状态。如果前面三步都成功,用户前台会看到“使用中”的状态;如果 DNS 记录添加成功但 Nginx 配置失败,程序会进入“异常”状态,等待定时任务补偿,不会直接把失败结果抛给用户。

2.3 数据结构与状态机:几个关键字段撑起整个自动回收

这套系统的数据库设计其实不复杂,核心就三张表:主域名表、子域名表、订单表。主域名表记录你接入的每个域名及对应的 API 密钥标识;子域名表记录每条分发记录的归属用户、解析目标、到期时间、当前状态;订单表负责记录用户申请、续费、回收的完整流水。

子域名表里最能说明问题的是状态字段,我一般叫它status,取值有四个:0 表示已申请但未完成解析,1 表示正常使用中,2 表示已过期进入宽限期,3 表示已回收。这个状态机决定了定时任务怎么工作:每分钟跑一次,把所有expire_at小于当前时间且status=1的记录改成 2,同时发送提醒邮件;再往后推三天,如果用户还没续费,就把status=2的记录改成 3,并触发 DNS 记录删除和 Nginx 配置删除。

有朋友拿到这套源码后,第一件事是去改界面、改颜色,我建议先别急。先把expire_at和status这两个字段的逻辑吃透,因为整个自动回收都依赖它们。你会发现,很多后续的“域名删不掉”“过期还在访问”之类的怪问题,根源都出在状态没有按这个顺序流转上。

3. 源码部署实操:PHP+Nginx+MySQL 环境下两小时跑起来

3.1 部署前环境检查:PHP 版本和扩展缺一不可

这份 V3.1.2 源码默认按 PHP 7.2 以上版本写的,低于 7.0 会直接语法报错。MySQL 建议 5.7 以上,Nginx 1.16 以上就够用。部署之前先在自己的服务器上跑一遍检查命令:

php -v php -m | grep -E 'curl|fileinfo|openssl|pdo_mysql' nginx -v mysql --version

如果你的 PHP 版本大于等于 7.2,且php -m能同时看到curl、fileinfo、openssl、pdo_mysql这四项,环境基本就达标了。少了curl,DNS API 请求发不出去;少了fileinfo,上传 SSL 证书或批量导入文件时会有兼容问题;少了openssl,程序无法正确生成签名。很多安装教程只写了“环境要求”,没告诉你每个扩展是干什么用的,等你装到一半发现 API 调不通,回来查才发现是扩展缺失,那才浪费时间。

需要注意一个常见坑:有些服务器装的是 PHP 7.4 但用的是旧版 HTTP 服务,Nginx 和 PHP 之间没有正确配置fastcgi_pass,导致 PHP 文件直接被下载而不是被解析。部署前先用一个探针文件测试phpinfo,确认能看到页面再开始装系统。

3.2 上传源码与安装向导:从压缩包到能登录后台

源码包拿到手是一个 zip 压缩包,里面除了程序文件,还有一份install目录和一份wg.sql数据库初始化脚本。把压缩包上传到服务器站点根目录后,按这个顺序操作:

cd /www/wwwroot/ unzip xunfeng_dns_pro_v3.1.2.zip cd xunfeng_dns_pro_v3.1.2 chown -R www:www . chmod -R 755 .

注意chown -R www:www这一步别省。源码在安装向导里要写配置文件、要建缓存目录,如果目录属主还是 root,PHP 进程没有写权限,安装过程就会卡在“配置文件写入失败”这一环。很多新手在这一步反复重装,其实只是权限没给到位。

权限设置好之后,浏览器访问http://你的域名/install/,进入安装向导。向导页面会让你填数据库地址、数据库名、用户名、密码,以及管理员账号。填完后程序自动导入wg.sql,初始化表结构,然后提示你删除install目录。

这里我的习惯是安装完成后立即执行rm -rf install,不是偷懒,而是防止别人通过重装向导覆盖你的配置。源码包里也写了这条建议,但很多人会忘,等被扫描工具扫到才想起来后悔。

3.3 Nginx 伪静态与站点配置:不配好这一行,二级域名全 404

这套系统的前端页面和后端接口都走统一的入口文件,Nginx 下必须配置伪静态规则。以下是一份可以直接用的 server 配置,注意root要指向源码所在的目录:

server { listen 80; server_name example.com *.example.com; root /www/wwwroot/xunfeng_dns_pro_v3.1.2; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } } location ~ \.php$ { fastcgi_pass unix:/tmp/php-cgi-74.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }

这个配置里最关键的是server_name example.com *.example.com,如果没有*.example.com这一段,用户访问zhangsan.example.com时 Nginx 找不到对应 server,会直接落到默认站点上,表现就是 404 或者打开了一个完全不相关的页面。rewrite ^(.*)$ /index.php?s=$1 last;的作用是前端路由重写,把/index.php?s=user/apply这种路径变成友好的 URL。

另一个容易忽略的点是fastcgi_pass的路径必须和你服务器上实际 PHP-FPM 的 socket 路径一致。宝塔面板默认是/tmp/php-cgi-74.sock,但你如果用的 PHP 版本不同,这个路径会跟着变。配错的话,PHP 请求会直接报 502,而这些报错在源码里是看不到明确提示的,只能翻 Nginx 错误日志。

3.4 config.php 参数速查表:改错一个值就少一个功能

源码根目录下有个config.php,安装完成后会自动生成,里面是所有全局参数。我给一张速查表,照着改就行:

参数名示例值含义改错的后果
DB_HOST127.0.0.1数据库地址连不上库,后台白屏
DB_NAMEwg_dns数据库名同上
DB_USERwg_dns数据库用户同上
API_TYPEaliyun / dnspodDNS 厂商类型类型不匹配,接口请求格式错误
API_KEY你的 AccessKey厂商 API 密钥鉴权失败,记录添加不了
API_SECRET你的 SecretKey签名密钥签名错误,同上
DEFAULT_TTL600默认解析生效时间TTL 设 60 会频繁请求厂商接口,容易被限流
SUB_LIMIT30单个用户最多持有子域名数设太大会被刷解析,设太小影响正常业务
RECYCLE_DAYS3过期后宽限天数设 0 等于到期秒删,用户投诉率高

这里单独说下API_SECRET。这个值只存在服务端配置文件里,绝对不要出现在日志中,也不要在前端页面上有任何展示。V3.1.2 的日志模块默认会过滤掉包含secret的请求参数,但你自己二次开发时要注意,不要把整个请求数组直接写进日志。

4. V3.1.2 面板与 API 的落地参数:让用户实现自助申请

4.1 后台添加主域名:先过泛解析校验这一关

登录后台后,第一件事就是在“域名管理”里添加你要用来分发的主域名。填写表单时有几项必须注意:域名本身不带www、不带协议;DNS 厂商类型要和你实际的托管商一致;API 密钥要选对,如果你在 DNSPod 申请的密钥却选了阿里云,后面每次调用都会签名报错。

填写完并保存后,系统会进入“校验中”状态。它会通过 API 查询当前域名下是否已存在一条泛解析记录,也就是*.example.com指向你的服务器 IP。如果没有,页面会给出明确提示,要求你先去 DNS 厂商后台添加这条 A 记录。这一步是硬校验,泛解析不存在时,后续所有子域名的添加请求都会被拒绝。

很多用户不理解为什么必须有这条泛解析记录。道理很简单:V3.1.2 的商业模式是“用户随时可能申请任意前缀”,如果每个子域名都手动创建精确解析,那就又回到人工操作了。泛解析保证任何未单独创建记录的子域名请求都能被 DNS 服务器解析到你的服务器 IP,程序再在 Nginx 层区分哪些域名是已开通的、哪些是走默认页面的。

4.2 套餐点数与续费规则:额度怎么分配才不亏

系统里没有传统的“包月付费”概念,而是用“点数”来计量。每个用户在后台可以通过卡密兑换点数,每开通一个二级域名消耗一定点数,到期后需要扣点数续期。这里有组参数可以自己控制:

参数默认值说明
开通消耗1 点申请新子域名时扣除
续期消耗0.5 点 / 月到期前续费一个月
续期限制最多提前 30 天防止用户一次续很多年
宽限期3 天到期后 3 天内未续费则回收
单人持有上限30 个防止单个用户刷大量子域名

这种“点数制”比按时间付费更适合分发业务,因为用户的使用行为差异很大:有人开了子域名只用来做短链接测试,两天就扔了;有人是长期用的,到期稳定续费。点数制可以做到用多少扣多少,回收之后域名释放,点数不会退还,这个政策逻辑要在前台帮助中心写清楚,能减少大量售后纠纷。

4.3 开放 API 对接:申请、查询、回收三个接口怎么调

V3.1.2 源码自带一套面向第三方平台的开放 API,认证方式是appid + token + sign签名。签名算法是常见做法:把所有请求参数按参数名 ASCII 升序排列,拼接成字符串后加上密钥做 MD5。下面是一段用 PHP 实现的申请接口调用示例:

$appid = '10001'; $key = '你的开放平台密钥'; $params = [ 'appid' => $appid, 'subdomain' => 'myblog', 'domain' => 'example.com', 'val' => '1.2.3.4', 'expire' => '2025-12-31', 'timestamp' => time() ]; ksort($params); $signStr = ''; foreach ($params as $k => $v) { $signStr .= $k . '=' . $v . '&'; } $signStr .= 'key=' . $key; $params['sign'] = md5($signStr); $ch = curl_init('https://你的域名/api/apply'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $resp = curl_exec($ch); curl_close($ch);

这段代码里最容易错的是ksort($params),签名要求必须排序后再拼接,否则服务端验签永远失败。另外,expire参数我建议传日期格式而不是时间戳,因为源码里对过期时间的解析用的是strtotime(),两种格式都能识别,但日期格式在数据库里更直观,方便排查问题。

开放 API 的回收接口调用方式和申请类似,只是地址换成/api/recycle,参数里多一个subdomain,不需要传val。注意回收接口的操作不可逆,第三方在调用前必须二次确认,源码里做了一个简单的 confirm 字段校验,传confirm=1才会执行,防止误删。

4.4 到期自动回收与邮件提醒:crontab 里的活

自动回收不是靠用户访问页面触发的,而是靠系统定时任务。源码包里有一个cron/目录,里面是三个 PHP 脚本:expire.php(检查过期)、remind.php(发送提醒)、recycle.php(执行回收)。你需要把它们加到服务器的 crontab 里:

* * * * * php /www/wwwroot/xunfeng_dns_pro_v3.1.2/cron/expire.php > /dev/null 2>&1 0 9 * * * php /www/wwwroot/xunfeng_dns_pro_v3.1.2/cron/remind.php > /dev/null 2>&1 */5 * * * * php /www/wwwroot/xunfeng_dns_pro_v3.1.2/cron/recycle.php > /dev/null 2>&1

expire.php每分钟跑一次,负责把过期记录的状态从“使用中”改成“宽限期”;remind.php每天上午 9 点发送提醒邮件,提醒那些即将到期的用户;recycle.php每 5 分钟跑一次,把超过宽限期的记录彻底回收。三个脚本职责分开,而不是合成一个,是为了避免单个脚本执行时间过长导致任务堆积。

5. 二级域名分发的避坑指南:五个现场与对应处理办法

5.1 泛解析加了也半天不生效

现象:后台校验域名时始终提示“未检测到泛解析记录”,或者用户申请成功但本机 ping 不通。

原因:两种情况最常见。第一种是你在 DNS 厂商后台刚添加泛解析记录,TTL 设置成了默认的 86400 秒,24 小时内生效周期太长;第二种是你本地电脑或路由器缓存了旧的 DNS 结果。

解决:泛解析记录的 TTL 建议直接设成 600 秒,等五分钟再让后台重新校验。本机排查时用dig +trace或nslookup指定公共 DNS 服务器查询,不要用浏览器判断,因为浏览器和系统 DNS 缓存会干扰判断。我在部署时一定会先提前一天把泛解析加好,等 TTL 完全扩散再导入系统,避免上线第一天全部域名解析失败。

5.2 前台下单成功但浏览器打不开

现象:用户前台显示“已开通”,但访问zhangsan.example.com提示连接被拒绝或直接超时。

原因:DNS 记录添加成功并不代表 Nginx 站点已生效。常见原因是程序在生成 vhost 时没有写权限,或者nginx -s reload执行失败。这类系统的源码里,不活跃的站点配置经常会因为路径写错而没有正常载入。

解决:先到服务器上执行nginx -t,看配置语法是否通过;然后查看 Nginx 的conf.d目录下有没有对应的子域名配置文件;最后看vhost目录的属主是不是www。如果确认配置文件存在但 reload 失败,最常见原因是配置里的root路径不存在,程序生成的站点根目录如果你没有手动创建,Nginx 会报open() failed。源码默认的站点根目录是/data/www/域名/,我一般会通过后台参数把站点根目录统一改成/www/wwwroot/domains/,并且预先建好这个目录。

5.3 回收之后域名居然还能访问

现象:用户到期被回收,前台状态已经变成“已回收”,但浏览器里仍然能打开这个二级域名,页面内容还在。

原因:回收流程不完整。V3.1.2 的回收动作在源码里是先调用 DNS API 删除解析记录,再删除 Nginx 配置文件,两步都成功才算回收完成。如果你的回收脚本只执行了第一步,或者第二步因为权限问题失败,流量仍然会通过泛解析到达服务器,然后命中残留的 Nginx 配置。

解决:手动检查时不要只看 DNS 解析结果,要看 Nginx 配置目录下是否还残留这个域名的配置文件。我遇到这种情况的处置是:先删除 vhost 文件,再执行nginx -s reload,最后再调 API 删除解析记录。顺序上先删 Nginx 后删 DNS,而不是反过来,因为在 DNS 缓存还没失效的这段时间里,Nginx 层面已经先拒绝了请求,用户感知就是“打不开”,而不是“解析失败”,从体验上看前者更明确。

5.4 API 密钥带 & 号导致签名一直失败

现象:后台填写密钥后,添加解析记录一直报“签名错误”,但密钥确实是从 DNS 厂商后台复制的,没有问题。

原因:厂商生成的 Secret 里可能包含&、+等 URL 特殊字符,源码在保存时直接写库,在取出拼接签名字符串时又没有做反转义,导致签名内容和厂商端不一致。

解决:在保存密钥时强制做 URL 编码,取出时做urldecode。如果你已经装好了系统,可以直接改config.php,把密钥用单引号包起来,然后写一个临时 PHP 文件输出urldecode('你的密钥')的结果,再把它填回配置里。从那以后我再也没有把密钥明文写在笔记里,统一存在服务器环境变量中,避免特殊字符问题反复出现。

5.5 cron 回收脚本到点不执行

现象:crontab 配置好了,但过了 24 小时,过期域名一个都没被回收。

原因:要么 crontab 服务没启动,要么脚本里的 PHP 路径不对。很多服务器安装的是多版本 PHP,php命令默认指向的是 7.4,但源码运行在另一个版本上,脚本执行时直接报错被> /dev/null吞掉了。

解决:先去掉> /dev/null,把输出重定向到日志文件,然后手动执行一遍脚本,看是不是有 PHP 报错。再执行which php确认路径,建议 crontab 里直接写绝对路径,例如/www/server/php/74/bin/php /www/wwwroot/xunfeng_dns_pro_v3.1.2/cron/expire.php。另外,脚本执行报错信息要落盘,这是排查这类问题最直接的手段,不要在 crontab 里静默丢弃输出,你会失去唯一的排错线索。

6. 进阶:把分发系统的健康检查做成自动化脚本

当你的分发系统上线跑了一段时间,子域名数量到几百条之后,你会发现人工后台检查根本看不过来。我的做法是再加一套独立于源码的健康检查脚本,独立部署在另一台内网机器上,每隔 5 分钟检查一次所有“使用中”的二级域名是否正常响应。核心逻辑很简单,用 curl 请求每个域名,检查 HTTP 返回码和响应时间:

#!/usr/bin/env python3 # coding: utf-8 import requests, sqlite3 domains = ['zhangsan.example.com', 'lisi.example.com'] for d in domains: try: r = requests.get(f'https://{d}/', timeout=10, verify=False) status = r.status_code except Exception as exc: print(f'{d} 请求异常: {exc}') continue if status not in (200, 301, 302): print(f'{d} 返回异常状态码 {status}')

这个脚本的verify=False是刻意关掉 SSL 证书校验的,因为分发场景下大量子域名不可能每个都配独立证书,泛解析证书本来就会报不匹配;脚本的目的只是探活,不是做证书校验。检查结果通过钉钉机器人 webhook 推到工作群,省去人工盯后台的功夫。

落地后别忘了把脚本本身加到 crontab,让它在系统重启后也能自动运行。我就有过一次教训:脚本写好后忘了设置开机自启,某天凌晨服务器更新重启,健康检查停跑了大半天,还是用户群里有人反馈才发现。从那以后,我每次上线任何定时类脚本,都会先强制走一遍“重启后恢复”的验证流程,确认 crontab 和脚本依赖的服务都能自启动,才敢说交付完成。这套健康检查脚本和迅风 DNS Pro 搭配使用,基本能覆盖日常运维里九成的问题,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进