简介:派小星DNS二级域名分发V2.0重构版是一套面向站长、域名服务商及中小型IDC运营者的域名分发管理系统源码,针对跨平台域名管理繁琐、会员权限与套餐策略难以灵活配置等痛点,提供一站式解决方案。压缩包共1371个文件,约16.93MB,以681个js脚本与400个css样式构成前后端交互与界面主体,辅以67个php接口文件、15个html页面、1个sql数据库脚本及gif、png、svg等图标素材,结构完整、便于二次开发。系统集成腾讯云、阿里云、华为云、Cloudflare、GoDaddy等主流DNS厂商,支持会员分级折扣、多级套餐组合、三要素实名认证与违规内容实时监测,并配备全新UI后台与模块化插件市场。目前已有191人学习下载,适合希望快速搭建域名分发平台、研究会员风控体系或进行插件扩展开发的读者参考借鉴。
1. 派小星DNS二级域名分发V2.0重构版:从手工配解析到自动化分发的分水岭
很多团队第一次接触二级域名分发,都是从一张 Excel 表开始的。运营提需求、开发手动加解析、测试环境改一条记录要等半天,域名一多,谁改过什么、哪条记录指向哪台机器,全靠记忆和聊天记录。派小星DNS二级域名分发V2.0重构版要解决的,正是这个从「人肉运维」到「系统化分发」的断层。它把域名资源、解析记录、分发规则、权限边界收进一套可编程的流程里,让二级域名像发号一样被批量、可控地分配出去。适合谁?手里管着几十上百个二级域名、需要给不同项目组或租户按需下发的运维、后端和平台工程同学。V2.0 重构版的关键词是「重构」,意味着接口、数据模型和分发逻辑都做了重新设计,老版本的脚本式思路要换一换了。
2. 派小星DNS二级域名分发V2.0重构版的核心模型:域名池、分发规则与解析记录怎么串起来
2.1 先搞清楚三个对象:域名池、分发任务、解析记录
派小星DNS二级域名分发V2.0重构版把整个系统抽象成三个核心对象,理解它们的关系,后面写代码才不会乱。
域名池(Domain Pool)是资源层,存放你拥有的主域名和可分配的二级域名前缀。比如你有一个主域名example.com,池子里可以预置api、cdn、test这些前缀,也可以只存主域名,由分发时动态生成前缀。V2.0 重构版把域名池做成了独立实体,支持按业务线、环境、区域打标签,这样分发时能按标签筛选,而不是从一大坨列表里硬找。
分发任务(Distribution Task)是动作层,一次分发就是一条任务记录。任务里包含:选中的域名池条目、目标解析值(IP 或 CNAME)、TTL、生效范围、申请人、审批状态。V2.0 重构版把任务做成了状态机,从pending到approved到active再到revoked,每一步都有审计日志。老版本经常出现「谁把这条解析改了」的扯皮,现在任务 ID 一查就清楚。
解析记录(DNS Record)是结果层,任务审批通过后,系统调用 DNS 服务商的 API 写入解析。V2.0 重构版不直接暴露解析记录的增删改,所有变更必须走分发任务,这样保证了「解析变更 = 有任务 = 有记录」的闭环。
这三者的关系用一句话概括:从域名池选资源,创建分发任务,任务驱动解析记录生成。下面这张表对比了 V1 脚本模式和 V2.0 重构版在关键维度的差异:
| 维度 | V1 脚本模式 | V2.0 重构版 |
|---|---|---|
| 域名管理 | 散落在配置文件 | 域名池 + 标签 |
| 分发方式 | 手动执行脚本 | 任务状态机 |
| 权限控制 | 靠服务器登录权限 | 任务级审批 + 角色 |
| 审计能力 | 无或仅日志 | 全链路任务日志 |
| 并发处理 | 串行,易冲突 | 队列 + 幂等写入 |
| 回滚能力 | 手动反向操作 | 任务撤销自动回滚 |
2.2 数据模型设计:四张表撑起分发链路
落地派小星DNS二级域名分发V2.0重构版,第一步是把数据模型定下来。我一般会设计四张核心表:domain_pool、distribution_task、dns_record、audit_log。下面用 SQL 给出建表语句,数据库以 MySQL 为例,其他关系型数据库改改类型即可。
-- 域名池:存放可分配的二级域名资源 CREATE TABLE domain_pool ( id BIGINT PRIMARY KEY AUTO_INCREMENT, root_domain VARCHAR(128) NOT NULL COMMENT '主域名,如 example.com', sub_prefix VARCHAR(64) NOT NULL COMMENT '二级前缀,如 api', full_domain VARCHAR(192) NOT NULL COMMENT '完整域名,如 api.example.com', biz_tag VARCHAR(64) DEFAULT '' COMMENT '业务标签,如 payment', env_tag VARCHAR(32) DEFAULT '' COMMENT '环境标签,如 prod/staging', status TINYINT DEFAULT 1 COMMENT '1可用 0停用', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_full_domain (full_domain), KEY idx_biz_env (biz_tag, env_tag) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 分发任务:一次分发动作的完整记录 CREATE TABLE distribution_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_no VARCHAR(64) NOT NULL COMMENT '任务编号,对外展示', pool_id BIGINT NOT NULL COMMENT '关联域名池ID', target_value VARCHAR(256) NOT NULL COMMENT '解析目标,IP或CNAME', record_type VARCHAR(16) DEFAULT 'A' COMMENT 'A/CNAME/TXT', ttl INT DEFAULT 600 COMMENT 'TTL秒数', applicant VARCHAR(64) NOT NULL COMMENT '申请人', approver VARCHAR(64) DEFAULT '' COMMENT '审批人', status VARCHAR(16) DEFAULT 'pending' COMMENT 'pending/approved/active/revoked/rejected', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_task_no (task_no), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 解析记录:任务生效后写入的DNS记录快照 CREATE TABLE dns_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL COMMENT '关联任务ID', full_domain VARCHAR(192) NOT NULL, record_type VARCHAR(16) NOT NULL, record_value VARCHAR(256) NOT NULL, ttl INT NOT NULL, provider_record_id VARCHAR(128) DEFAULT '' COMMENT 'DNS服务商返回的记录ID', status TINYINT DEFAULT 1 COMMENT '1生效 0已删除', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_task (task_id), KEY idx_domain (full_domain) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 审计日志:所有状态变更留痕 CREATE TABLE audit_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL, action VARCHAR(32) NOT NULL COMMENT 'create/approve/activate/revoke', operator VARCHAR(64) NOT NULL, detail JSON COMMENT '变更详情', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_task_action (task_id, action) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这四张表的逻辑说明:domain_pool是资源池,full_domain唯一索引防止重复分配;distribution_task是动作记录,task_no对外可读,status驱动整个流程;dns_record是结果快照,保留provider_record_id是为了后续调用服务商 API 删除或修改时能对上号;audit_log用 JSON 存详情,灵活且够用。
参数说明:ttl默认 600 秒,生产环境如果解析变更频繁,可以降到 60 秒加快生效,但要注意 DNS 服务商的 TTL 下限;record_type默认 A 记录,如果目标值是域名,改成 CNAME;status字段的流转必须由代码控制,不要允许直接 UPDATE,否则审计就断了。
2.3 分发流程的代码实现:从创建任务到写入解析
数据模型有了,接下来是核心分发逻辑。我用 Python 写一个简化版的服务层,展示从创建任务到调用 DNS API 的完整链路。这里假设 DNS 服务商提供了一个create_record的 HTTP 接口,实际对接时替换成你用的服务商 SDK 即可。
import uuid import requests from datetime import datetime class DistributionService: def __init__(self, db, dns_api_client): self.db = db self.dns = dns_api_client def create_task(self, pool_id, target_value, applicant, record_type='A', ttl=600): """创建分发任务,状态为 pending""" task_no = f"DT-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:6].upper()}" # 校验域名池条目是否可用 pool = self.db.query_one( "SELECT * FROM domain_pool WHERE id=%s AND status=1", (pool_id,) ) if not pool: raise ValueError("域名池条目不存在或已停用") # 检查是否已有活跃任务占用同一域名 exist = self.db.query_one( "SELECT id FROM distribution_task WHERE pool_id=%s AND status IN ('approved','active')", (pool_id,) ) if exist: raise ValueError("该域名已有生效中的分发任务") task_id = self.db.insert( "INSERT INTO distribution_task (task_no, pool_id, target_value, record_type, ttl, applicant, status) " "VALUES (%s,%s,%s,%s,%s,%s,'pending')", (task_no, pool_id, target_value, record_type, ttl, applicant) ) self._audit(task_id, 'create', applicant, {'target': target_value}) return task_no def approve_task(self, task_no, approver): """审批通过,状态变为 approved""" task = self.db.query_one( "SELECT * FROM distribution_task WHERE task_no=%s", (task_no,) ) if not task or task['status'] != 'pending': raise ValueError("任务不存在或状态不允许审批") self.db.execute( "UPDATE distribution_task SET status='approved', approver=%s WHERE id=%s", (approver, task['id']) ) self._audit(task['id'], 'approve', approver, {}) return task['id'] def activate_task(self, task_no, operator): """激活任务:调用DNS API写入解析,状态变为 active""" task = self.db.query_one( "SELECT t.*, p.full_domain FROM distribution_task t " "JOIN domain_pool p ON t.pool_id=p.id WHERE t.task_no=%s", (task_no,) ) if not task or task['status'] != 'approved': raise ValueError("任务未审批或状态错误") # 调用DNS服务商API创建解析记录 resp = self.dns.create_record( domain=task['full_domain'], record_type=task['record_type'], value=task['target_value'], ttl=task['ttl'] ) if not resp.get('success'): raise RuntimeError(f"DNS API写入失败: {resp.get('message')}") # 写入解析记录快照 self.db.insert( "INSERT INTO dns_record (task_id, full_domain, record_type, record_value, ttl, provider_record_id) " "VALUES (%s,%s,%s,%s,%s,%s)", (task['id'], task['full_domain'], task['record_type'], task['target_value'], task['ttl'], resp['record_id']) ) self.db.execute( "UPDATE distribution_task SET status='active' WHERE id=%s", (task['id'],) ) self._audit(task['id'], 'activate', operator, {'record_id': resp['record_id']}) return resp['record_id'] def revoke_task(self, task_no, operator): """撤销任务:删除DNS解析,状态变为 revoked""" task = self.db.query_one( "SELECT * FROM distribution_task WHERE task_no=%s", (task_no,) ) if not task or task['status'] != 'active': raise ValueError("只有生效中的任务才能撤销") record = self.db.query_one( "SELECT * FROM dns_record WHERE task_id=%s AND status=1", (task['id'],) ) if record: self.dns.delete_record(record['provider_record_id']) self.db.execute( "UPDATE dns_record SET status=0 WHERE id=%s", (record['id'],) ) self.db.execute( "UPDATE distribution_task SET status='revoked' WHERE id=%s", (task['id'],) ) self._audit(task['id'], 'revoke', operator, {}) def _audit(self, task_id, action, operator, detail): self.db.insert( "INSERT INTO audit_log (task_id, action, operator, detail) VALUES (%s,%s,%s,%s)", (task_id, action, operator, str(detail)) )这段代码的逻辑说明:create_task做了两层校验,一是域名池条目可用,二是同一域名没有生效中的任务,避免重复分发;approve_task只允许从pending到approved,防止跳过审批;activate_task是真正调用外部 API 的一步,先写 DNS 再写本地快照,顺序不能反,否则 API 成功了本地没记录,回滚就找不到provider_record_id;revoke_task先删 DNS 再改状态,保证外部和内部一致。
参数说明:task_no的生成规则用了日期加随机串,方便人工识别和排查;ttl在创建时传入,激活时透传给 DNS API;record_type目前支持 A 和 CNAME,如果要支持 TXT 做验证,在create_record里加分支即可。注意dns.create_record和dns.delete_record要做重试封装,网络抖动时不能直接抛异常给用户,我一般会加三次重试加指数退避。
3. 派小星DNS二级域名分发V2.0重构版的部署与对接:从本地跑通到接入真实DNS服务商
3.1 本地最小可运行环境:Docker Compose 起 MySQL 和 Redis
在对接真实 DNS 服务商之前,先在本地把服务跑起来。派小星DNS二级域名分发V2.0重构版依赖 MySQL 存数据、Redis 做任务队列和幂等锁。下面这个docker-compose.yml是我常用的最小环境,直接抄改即可。
version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: devpass MYSQL_DATABASE: dns_dist ports: - "3306:3306" volumes: - ./init.sql:/docker-entrypoint-initdb.d/init.sql command: --default-authentication-plugin=mysql_native_password redis: image: redis:7-alpine ports: - "6379:6379" command: redis-server --appendonly yes把第 2 章的四张建表语句保存为init.sql,放在同目录下,docker-compose up -d之后 MySQL 会自动执行。Redis 用来做两件事:一是任务激活时的分布式锁,防止同一任务被并发激活;二是审批通知的简易队列,V2.0 重构版如果要做异步审批提醒,可以往 Redis 里推消息。
启动后验证:docker exec -it <mysql容器名> mysql -uroot -pdevpass -e "USE dns_dist; SHOW TABLES;",能看到四张表就说明初始化成功。这一步看着简单,但经常有人卡在 MySQL 8.0 的认证插件上,--default-authentication-plugin=mysql_native_password这行别省。
3.2 对接 DNS 服务商 API:以常见记录管理接口为例
派小星DNS二级域名分发V2.0重构版本身不实现 DNS 服务器,它是个管理分发层,最终解析生效要靠 DNS 服务商。常见做法是封装一个DnsApiClient适配器,把不同服务商的接口差异屏蔽掉。下面用 Python 写一个适配器基类和两个模拟实现,展示对接思路。
from abc import ABC, abstractmethod class DnsApiClient(ABC): """DNS服务商适配器基类,所有服务商实现这个接口""" @abstractmethod def create_record(self, domain, record_type, value, ttl): """创建解析记录,返回 {'success': bool, 'record_id': str, 'message': str}""" pass @abstractmethod def delete_record(self, record_id): """删除解析记录""" pass @abstractmethod def update_record(self, record_id, value, ttl): """修改解析记录""" pass class ProviderAClient(DnsApiClient): """服务商A的实现,假设用HTTP API""" def __init__(self, api_key, api_secret, endpoint): self.api_key = api_key self.api_secret = api_secret self.endpoint = endpoint def create_record(self, domain, record_type, value, ttl): # 实际对接时替换为服务商要求的签名和请求格式 payload = { 'domain': domain, 'type': record_type, 'value': value, 'ttl': ttl, 'key': self.api_key } # 这里用伪代码表示请求,真实场景用requests.post # resp = requests.post(f"{self.endpoint}/record/create", json=payload, timeout=10) # data = resp.json() # return {'success': data['code'] == 0, 'record_id': data.get('record_id'), 'message': data.get('msg')} return {'success': True, 'record_id': 'mock-record-001', 'message': 'ok'} def delete_record(self, record_id): # resp = requests.post(f"{self.endpoint}/record/delete", json={'record_id': record_id}, timeout=10) return {'success': True} def update_record(self, record_id, value, ttl): # resp = requests.post(f"{self.endpoint}/record/update", json={...}, timeout=10) return {'success': True}逻辑说明:基类定义三个必须实现的方法,create_record返回统一结构,上层服务不用关心是哪家服务商。ProviderAClient里把签名和请求细节留空,因为不同服务商的鉴权方式差异很大,有的是 API Key 放 Header,有的是 HMAC 签名放 Query,这里不写死。参数说明:endpoint是服务商的 API 地址,api_key和api_secret从环境变量读取,不要硬编码在代码里。超时时间建议 10 秒,DNS API 一般响应很快,超过 10 秒大概率是网络问题,重试比干等划算。
3.3 任务队列与幂等:用 Redis 锁防止重复激活
分发任务激活时,如果两个人同时点「激活」,或者前端重复提交,可能对同一个域名写入两条解析。V2.0 重构版必须处理这个并发问题。我一般用 Redis 的SET NX EX做分布式锁,锁的粒度是任务 ID。
import redis import time class TaskLock: def __init__(self, redis_client, key_prefix='dns:task:lock:'): self.redis = redis_client self.prefix = key_prefix def acquire(self, task_id, timeout=30): """获取锁,成功返回True,失败返回False""" key = f"{self.prefix}{task_id}" # SET NX EX 是原子操作,value用当前时间戳便于排查 ok = self.redis.set(key, str(time.time()), nx=True, ex=timeout) return bool(ok) def release(self, task_id): """释放锁""" key = f"{self.prefix}{task_id}" self.redis.delete(key) # 在activate_task里使用 def activate_task_with_lock(self, task_no, operator): task = self.db.query_one("SELECT id FROM distribution_task WHERE task_no=%s", (task_no,)) if not task: raise ValueError("任务不存在") lock = TaskLock(self.redis) if not lock.acquire(task['id']): raise RuntimeError("任务正在处理中,请稍后重试") try: # 这里调用前面的activate_task逻辑 return self.activate_task(task_no, operator) finally: lock.release(task['id'])逻辑说明:acquire用nx=True保证只有第一个请求能设置成功,ex=timeout防止死锁,即使释放逻辑没执行,30 秒后锁自动过期。release放在finally里,保证异常时也能释放。参数说明:timeout默认 30 秒,如果 DNS API 调用较慢,可以调到 60 秒,但不要太大,否则并发冲突时用户等太久。注意锁的 value 存时间戳,排查时能看到谁先拿到的锁。
4. 派小星DNS二级域名分发V2.0重构版避坑指南:五条血泪经验
4.1 域名池唯一索引没加,重复分配后解析互相覆盖
现象:两个任务分到了同一个api.example.com,激活后一个把另一个的解析覆盖了,业务方反馈「刚配好就变了」。
原因:domain_pool表的full_domain没加唯一索引,或者加了但代码里没做「同一域名不能有生效中任务」的校验。
解决:full_domain必须加 UNIQUE KEY,同时在create_task里查distribution_task表,确保同一pool_id没有approved或active状态的任务。这两道防线缺一不可,唯一索引防数据层重复,代码校验防业务层冲突。
4.2 DNS API 调用成功但本地写库失败,回滚时找不到记录
现象:激活任务时报错,但去 DNS 服务商控制台看,解析已经写进去了,本地dns_record表却没有记录,撤销时无法删除。
原因:activate_task里先调 DNS API 再写本地库,如果写库那一步抛异常,外部已经生效,内部没留痕。
解决:调整顺序解决不了根本问题,因为写库也可能失败。正确做法是「先写本地 pending 记录,再调 API,成功后更新记录状态」。具体是:在dns_record里先插一条status=0的记录,拿到id后再调 DNS API,成功后把status改成 1 并回填provider_record_id。这样即使 API 成功写库失败,也有一条 pending 记录可以人工对账。
4.3 TTL 设得太短,DNS 服务商限流导致激活失败
现象:批量激活任务时,部分任务报「API 频率超限」,解析没生效。
原因:TTL 设了 60 秒甚至更短,DNS 服务商对短 TTL 记录的创建有更严格的频率限制,或者批量操作触发了限流。
解决:生产环境 TTL 建议 300 到 600 秒,除非业务明确要求快速切换。批量激活时在代码里加间隔,比如每激活 5 个任务 sleep 1 秒,或者用队列串行处理。别小看这个,我见过一次批量激活 50 个域名,因为没限速,后 20 个全部失败,排查了半天以为是 API 挂了。
4.4 审批状态直接 UPDATE,审计日志断链
现象:查审计日志时发现某个任务从pending直接跳到active,中间的approve记录没了。
原因:有人图省事,直接在数据库里UPDATE distribution_task SET status='active',绕过了服务层的approve_task方法。
解决:数据库账号权限收紧,应用账号只给SELECT、INSERT和特定列的UPDATE,不给全表UPDATE权限。更彻底的做法是用触发器或 ORM 的钩子,状态变更必须走audit_log写入。另外,audit_log表只允许INSERT,不允许UPDATE和DELETE,从权限上保证日志不可篡改。
4.5 撤销任务时只改状态没删解析,域名被「幽灵占用」
现象:任务撤销了,状态显示revoked,但域名池里该域名还是不能重新分配,提示「已有生效中任务」。
原因:revoke_task只把distribution_task.status改成了revoked,但dns_record的status还是 1,或者domain_pool的占用标记没清。
解决:撤销逻辑要三步一起做——删 DNS 解析、把dns_record.status置 0、确认distribution_task.status变为revoked。然后在create_task的校验里,查的是distribution_task表里status IN ('approved','active')的记录,revoked不算占用。如果用了domain_pool的占用字段,也要同步清掉。这三步放在一个事务里,要么全成功要么全回滚。
5. 派小星DNS二级域名分发V2.0重构版的进阶技巧:用批量导入和解析预检把分发效率提上去
批量分发是 V2.0 重构版最实用的进阶场景。假设某次大促要临时分配 30 个二级域名给不同业务方,一个个建任务太慢。我一般会做一个 CSV 批量导入功能,格式如下:
sub_prefix,target_value,record_type,ttl,applicant,biz_tag promo-a,10.0.1.10,A,300,ops01,marketing promo-b,10.0.1.11,A,300,ops01,marketing promo-c,cdn.example.net,CNAME,600,ops02,static导入脚本的核心逻辑是逐行调用create_task,但要做两件事:一是预检,导入前先查域名池里这些前缀是否可用、是否已被占用;二是汇总结果,把成功和失败的行分别输出,失败的带上原因。下面是一个简化的导入函数:
import csv def batch_import(csv_path, service, operator): success, failed = [], [] with open(csv_path, newline='', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: try: # 预检:查域名池 pool = service.db.query_one( "SELECT id FROM domain_pool WHERE sub_prefix=%s AND status=1", (row['sub_prefix'],) ) if not pool: failed.append({**row, 'reason': '域名池无此前缀'}) continue task_no = service.create_task( pool_id=pool['id'], target_value=row['target_value'], applicant=row['applicant'], record_type=row.get('record_type', 'A'), ttl=int(row.get('ttl', 600)) ) success.append({**row, 'task_no': task_no}) except Exception as e: failed.append({**row, 'reason': str(e)}) return success, failed逻辑说明:batch_import逐行处理,单行失败不影响其他行,最后汇总。create_task内部已经有占用校验,这里再加一层域名池预检,是为了在导入阶段就给出更明确的错误提示。参数说明:record_type和ttl用get带默认值,CSV 里不填也能跑;applicant必填,用于审计。
另一个进阶技巧是解析预检。在激活任务之前,先调用 DNS 服务商的查询接口,确认目标域名当前没有冲突解析。有些服务商允许同名多条记录,如果不预检,可能写出两条 A 记录导致轮询,业务方以为只配了一条。预检逻辑很简单:查full_domain的现有记录,如果有同类型记录且值不同,就拦截并提示「目标域名已有解析,请先确认」。这一步能省掉大量事后排查。
最后说一个我自己的习惯:每次上线新的分发规则前,先在 staging 环境用真实 DNS 服务商的测试域名跑一遍全流程,从建任务到激活到撤销,确认provider_record_id能正确回填、撤销能删干净。这个习惯帮我拦过至少三次「生产环境删不了解析」的事故。派小星DNS二级域名分发V2.0重构版的价值不在于功能多,而在于把每一步都变得可追溯、可回滚。希望帮到你。
本文还有配套的精品资源,点击获取