Files
getDomain/domainCheck/app/utils/database.py
Your Name 7cbde2aa78 d
2026-04-22 14:13:21 +08:00

2743 lines
102 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :database.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 0:07
@explain : 数据库操作类
'''
import json
import os
import socket
import threading
import time
import psycopg2
import redis
from loguru import logger
from psycopg2 import extensions
from psycopg2.extras import Json
from app.config import config
from app.utils.status_codes import (
DETECT_STATUS_BLACKLISTED,
DETECT_STATUS_COMPLETED,
DETECT_STATUS_FAILED,
DETECT_STATUS_PENDING,
DETECT_STATUS_RUNNING,
REGISTER_STATUS_AVAILABLE,
REGISTER_STATUS_REGISTERED,
REVIEW_STATUS_PENDING,
THIRD_PARTY_STATUS_DONE,
)
_POOL_VERBOSE_LOGS = bool(int(os.getenv("DOMAINCHECK_DB_POOL_VERBOSE_LOGS", "0") or 0))
_STEP_CLAIM_PRIORITY = {
# Prefer deeper pipeline steps first so domains that already passed an
# earlier gate can keep advancing instead of being starved behind the
# oldest first-step backlog. This keeps later steps like 360/chinaz/aizhan
# from sitting pending forever while baidu/register continue to dominate
# the queue.
"detect_juziseo": 10,
"detect_jucha": 20,
"detect_wayback": 30,
"detect_aizhan": 40,
"detect_chinaz": 50,
"detect_360_site": 60,
"detect_baidu_site": 70,
"detect_register": 100,
}
def _detect_job_item_step_priority(step_code: str) -> int:
normalized = str(step_code or "").strip()
if not normalized:
return 999
return int(_STEP_CLAIM_PRIORITY.get(normalized, 10))
def _ordered_step_claim_codes() -> list[str]:
return [
step_code
for step_code, _ in sorted(
_STEP_CLAIM_PRIORITY.items(),
key=lambda item: (int(item[1] or 0), str(item[0] or "")),
)
]
def _resolve_step_claim_quota(limit: int) -> int:
configured_quota = int(os.getenv("DOMAINCHECK_STEP_CLAIM_QUOTA", "0") or 0)
if configured_quota > 0:
return max(1, configured_quota)
normalized_limit = max(1, int(limit or 1))
return max(64, min(normalized_limit, max(1, normalized_limit // 4)))
class Database:
"""
数据库操作类
"""
def __init__(self, host=None, port=None, database=None, user=None, password=None):
"""
初始化数据库连接
:param host: 数据库主机
:param port: 数据库端口
:param database: 数据库名称
:param user: 用户名
:param password: 密码
"""
self.host = host or config.DB_HOST
self.port = port or config.DB_PORT
self.database = database or config.DB_DATABASE
self.user = user or config.DB_USER
self.password = password or config.DB_PASSWORD
# 数据库连接池
self.connection_pool = []
self.pool_size = max(1, int(config.DB_POOL_SIZE)) # 连接池大小
self.pool_warm_size = max(1, min(self.pool_size, int(getattr(config, 'DB_POOL_WARM_SIZE', 16) or 16)))
self.pool_idle_keep_max = max(
self.pool_warm_size,
min(self.pool_size, int(getattr(config, 'DB_POOL_IDLE_KEEP_MAX', self.pool_size) or self.pool_size)),
)
self.pool_healthcheck_interval = max(
0.0,
float(getattr(config, 'DB_POOL_HEALTHCHECK_INTERVAL', 30) or 30),
)
self.pool_acquire_timeout = max(1.0, float(getattr(config, 'DB_POOL_ACQUIRE_TIMEOUT', 20)))
self.pool_lock = threading.Lock()
self.pool_condition = threading.Condition(self.pool_lock)
self._pool_initialized = False
self.total_connections = 0
self._connection_last_healthcheck = {}
# 初始化连接池
self._init_connection_pool()
# 初始化 Redis 客户端
try:
self.redis_client = redis.Redis(
host=config.REDIS_HOST,
port=config.REDIS_PORT,
password=config.REDIS_PASSWORD,
db=config.REDIS_DB,
decode_responses=True
)
# 测试连接
self.redis_client.ping()
logger.info(f"Redis 连接成功: {config.REDIS_HOST}:{config.REDIS_PORT}")
self.use_redis = True
# 初始化布隆过滤器
self._init_bloom_filter()
except Exception as e:
logger.warning(f"Redis 连接失败: {e},将使用数据库查询")
self.redis_client = None
self.use_redis = False
self.use_bloom_filter = False
def _init_connection_pool(self):
"""
初始化数据库连接池
"""
try:
with self.pool_condition:
missing = self.pool_warm_size - self.total_connections
if missing <= 0:
return
created = []
for i in range(missing):
created.append(self._create_connection())
with self.pool_condition:
self.connection_pool.extend(created)
self.total_connections += len(created)
self._pool_initialized = True
now = time.monotonic()
for conn in created:
self._connection_last_healthcheck[id(conn)] = now
self.pool_condition.notify_all()
logger.info(
f"数据库连接池初始化成功,预热: {len(created)},池中空闲: {len(self.connection_pool)},总连接: {self.total_connections}/{self.pool_size}"
)
except Exception as e:
logger.error(f"初始化数据库连接池失败: {e}")
def _create_connection(self):
return psycopg2.connect(
host=self.host,
port=self.port,
database=self.database,
user=self.user,
password=self.password,
connect_timeout=5,
application_name="domaincheck-worker",
)
def _init_bloom_filter(self):
"""
初始化布隆过滤器
"""
try:
# 检查 Redis 是否支持布隆过滤器
# 如果不支持,将使用普通缓存
try:
# 尝试创建布隆过滤器
self.redis_client.execute_command('BF.RESERVE', 'domain_bloom', 0.001, 1073741824)
logger.info("布隆过滤器初始化成功")
self.use_bloom_filter = True
except Exception as e:
# 检查是否是因为布隆过滤器已存在
if "item exists" in str(e):
logger.info("布隆过滤器已存在,直接使用")
self.use_bloom_filter = True
else:
# 如果命令不存在,说明 Redis 没有加载布隆过滤器模块
logger.info(f"Redis 布隆过滤器不可用: {e},将使用普通缓存")
self.use_bloom_filter = False
except Exception as e:
logger.warning(f"初始化布隆过滤器失败: {e}")
self.use_bloom_filter = False
def connect(self, thread_id=None):
"""
从连接池获取数据库连接
:param thread_id: 线程ID为None时使用当前线程ID
:return: tuple - (连接对象, 游标对象)
"""
import threading
thread_id = thread_id or threading.current_thread().ident
try:
if not self._pool_initialized:
self._init_connection_pool()
deadline = time.monotonic() + self.pool_acquire_timeout
warned_pool_empty = False
while True:
create_new = False
with self.pool_condition:
while self.connection_pool:
conn = self.connection_pool.pop()
if conn and not conn.closed:
try:
if conn.get_transaction_status() != extensions.TRANSACTION_STATUS_IDLE:
conn.rollback()
last_healthcheck = float(self._connection_last_healthcheck.get(id(conn), 0.0) or 0.0)
now = time.monotonic()
if self.pool_healthcheck_interval > 0 and now - last_healthcheck >= self.pool_healthcheck_interval:
cur = conn.cursor()
cur.execute("SELECT 1")
cur.fetchone()
cur.close()
self._connection_last_healthcheck[id(conn)] = now
if _POOL_VERBOSE_LOGS:
logger.debug(f"线程 {thread_id} 从连接池获取连接成功")
return conn, conn.cursor()
except Exception:
self._connection_last_healthcheck.pop(id(conn), None)
try:
conn.close()
except Exception:
pass
self.total_connections = max(0, self.total_connections - 1)
continue
if self.total_connections < self.pool_size:
self.total_connections += 1
create_new = True
else:
remaining = deadline - time.monotonic()
if remaining <= 0:
logger.warning(
f"连接池耗尽,线程 {thread_id} 等待超时 {self.pool_acquire_timeout}s返回空连接"
)
return None, None
if not warned_pool_empty:
warned_pool_empty = True
logger.warning(
f"连接池耗尽,线程 {thread_id} 等待可复用连接,池大小 {self.pool_size}"
)
self.pool_condition.wait(timeout=min(0.5, remaining))
if create_new:
try:
conn = self._create_connection()
self._connection_last_healthcheck[id(conn)] = time.monotonic()
if _POOL_VERBOSE_LOGS:
logger.debug(f"线程 {thread_id} 新建数据库连接成功,总连接 {self.total_connections}/{self.pool_size}")
return conn, conn.cursor()
except Exception as e:
with self.pool_condition:
self.total_connections = max(0, self.total_connections - 1)
self.pool_condition.notify()
logger.error(f"线程 {thread_id} 新建数据库连接失败: {e}")
return None, None
except Exception as e:
logger.error(f"线程 {thread_id} 获取数据库连接失败: {e}")
return None, None
def get_connection(self):
"""
获取数据库连接(兼容方法)
:return: 连接对象
"""
conn, _ = self.connect()
return conn
def close(self, conn=None, cur=None):
"""
将数据库连接放回连接池
:param conn: 连接对象
:param cur: 游标对象
"""
try:
if cur:
try:
cur.close()
except:
pass
if conn and not conn.closed:
with self.pool_condition:
try:
if conn.get_transaction_status() != extensions.TRANSACTION_STATUS_IDLE:
conn.rollback()
except Exception:
try:
self._connection_last_healthcheck.pop(id(conn), None)
conn.close()
finally:
self.total_connections = max(0, self.total_connections - 1)
self.pool_condition.notify()
logger.warning("连接归还前回滚失败,已关闭连接")
return
if len(self.connection_pool) < self.pool_idle_keep_max:
self.connection_pool.append(conn)
self.pool_condition.notify()
if _POOL_VERBOSE_LOGS:
logger.debug("连接已放回连接池")
else:
# 启动阶段只预热少量连接,高并发跑起来后允许保留更多空闲连接,
# 避免本地数据库在“建连/关连”之间来回抖动。
self._connection_last_healthcheck.pop(id(conn), None)
conn.close()
self.total_connections = max(0, self.total_connections - 1)
self.pool_condition.notify()
if _POOL_VERBOSE_LOGS:
logger.debug(f"空闲连接超过保留阈值({self.pool_idle_keep_max}),已关闭多余连接")
except Exception as e:
logger.error(f"关闭数据库连接失败: {e}")
try:
if conn and not conn.closed:
self._connection_last_healthcheck.pop(id(conn), None)
conn.close()
except:
pass
def get_sensitive_words(self):
"""
获取所有敏感词
:return: list - 敏感词列表
"""
try:
sql = "SELECT word, category, priority FROM sensitive_words ORDER BY priority DESC, word ASC"
result = self.fetch_all(sql)
return result
except Exception as e:
logger.error(f"获取敏感词失败: {e}")
return []
def add_sensitive_word(self, word, category='default', priority=1):
"""
添加敏感词
:param word: 敏感词
:param category: 分类
:param priority: 优先级
:return: bool - 是否成功
"""
try:
sql = "INSERT INTO sensitive_words (word, category, priority) VALUES (%s, %s, %s) ON CONFLICT (word) DO NOTHING"
return self.execute(sql, (word, category, priority))
except Exception as e:
logger.error(f"添加敏感词失败: {word}, 错误: {e}")
return False
def delete_sensitive_word(self, word):
"""
删除敏感词
:param word: 敏感词
:return: bool - 是否成功
"""
try:
sql = "DELETE FROM sensitive_words WHERE word = %s"
return self.execute(sql, (word,))
except Exception as e:
logger.error(f"删除敏感词失败: {word}, 错误: {e}")
return False
def update_sensitive_word(self, old_word, new_word, category=None, priority=None):
"""
更新敏感词
:param old_word: 旧敏感词
:param new_word: 新敏感词
:param category: 分类
:param priority: 优先级
:return: bool - 是否成功
"""
try:
if category is not None and priority is not None:
sql = "UPDATE sensitive_words SET word = %s, category = %s, priority = %s WHERE word = %s"
return self.execute(sql, (new_word, category, priority, old_word))
elif category is not None:
sql = "UPDATE sensitive_words SET word = %s, category = %s WHERE word = %s"
return self.execute(sql, (new_word, category, old_word))
elif priority is not None:
sql = "UPDATE sensitive_words SET word = %s, priority = %s WHERE word = %s"
return self.execute(sql, (new_word, priority, old_word))
else:
sql = "UPDATE sensitive_words SET word = %s WHERE word = %s"
return self.execute(sql, (new_word, old_word))
except Exception as e:
logger.error(f"更新敏感词失败: {old_word} -> {new_word}, 错误: {e}")
return False
def batch_add_sensitive_words(self, words):
"""
批量添加敏感词
:param words: 敏感词列表,每个元素是 (word, category, priority) 元组
:return: bool - 是否成功
"""
try:
if not words:
return True
sql = "INSERT INTO sensitive_words (word, category, priority) VALUES (%s, %s, %s) ON CONFLICT (word) DO NOTHING"
return self.execute_many(sql, words)
except Exception as e:
logger.error(f"批量添加敏感词失败: {e}")
return False
def execute(self, sql, params=None):
"""
执行SQL语句
:param sql: SQL语句
:param params: 参数
:return: 执行结果
"""
import threading
thread_id = threading.current_thread().ident
conn = None
cur = None
try:
conn, cur = self.connect(thread_id)
if not conn or not cur:
logger.error(f"线程 {thread_id} 数据库连接失败无法执行SQL")
return False
cur.execute(sql, params)
conn.commit()
return True
except Exception as e:
logger.error(f"执行SQL失败: {sql}, 错误: {e}")
try:
if conn:
conn.rollback()
except:
pass
return False
finally:
# 将连接放回连接池
self.close(conn, cur)
def execute_many(self, sql, params_list):
"""
批量执行SQL语句
:param sql: SQL语句
:param params_list: 参数列表
:return: 执行结果
"""
import threading
thread_id = threading.current_thread().ident
conn = None
cur = None
try:
conn, cur = self.connect(thread_id)
if not conn or not cur:
logger.error(f"线程 {thread_id} 数据库连接失败无法执行批量SQL")
return False
cur.executemany(sql, params_list)
conn.commit()
return True
except Exception as e:
logger.error(f"执行批量SQL失败: {sql}, 错误: {e}")
try:
if conn:
conn.rollback()
except:
pass
return False
finally:
self.close(conn, cur)
def ensure_cluster_runtime_tables(self):
sql = """
CREATE TABLE IF NOT EXISTS detect_worker_nodes (
node_code VARCHAR(64) PRIMARY KEY,
region VARCHAR(32) NOT NULL DEFAULT 'unknown',
role VARCHAR(32) NOT NULL DEFAULT 'worker',
hostname VARCHAR(255) NOT NULL DEFAULT '',
ip VARCHAR(64) NOT NULL DEFAULT '',
status VARCHAR(32) NOT NULL DEFAULT 'unknown',
worker_version VARCHAR(32) NOT NULL DEFAULT '',
current_load INTEGER NOT NULL DEFAULT 0,
metadata_json JSONB,
last_heartbeat_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS detect_jobs (
id BIGSERIAL PRIMARY KEY,
job_code VARCHAR(64) NOT NULL UNIQUE,
source VARCHAR(64) NOT NULL DEFAULT 'manual',
plan_hash VARCHAR(128) NOT NULL DEFAULT '',
task_mode VARCHAR(32) NOT NULL DEFAULT 'domain_pipeline',
step_code VARCHAR(64) NOT NULL DEFAULT '',
status VARCHAR(32) NOT NULL DEFAULT 'pending',
remark TEXT NOT NULL DEFAULT '',
created_by VARCHAR(64) NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
started_at TIMESTAMP,
finished_at TIMESTAMP
);
CREATE TABLE IF NOT EXISTS detect_job_items (
id BIGSERIAL PRIMARY KEY,
job_id BIGINT NOT NULL REFERENCES detect_jobs(id) ON DELETE CASCADE,
domain_id BIGINT NOT NULL,
step_code VARCHAR(64) NOT NULL DEFAULT '',
status VARCHAR(32) NOT NULL DEFAULT 'pending',
claimed_by VARCHAR(64) NOT NULL DEFAULT '',
claim_token VARCHAR(64) NOT NULL DEFAULT '',
lease_expires_at TIMESTAMP,
attempt_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT NOT NULL DEFAULT '',
result_version VARCHAR(64) NOT NULL DEFAULT '',
step_payload_json JSONB,
result_payload_json JSONB,
started_at TIMESTAMP,
finished_at TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_detect_job_items_status_lease
ON detect_job_items(status, lease_expires_at);
ALTER TABLE detect_jobs
ADD COLUMN IF NOT EXISTS task_mode VARCHAR(32) NOT NULL DEFAULT 'domain_pipeline',
ADD COLUMN IF NOT EXISTS step_code VARCHAR(64) NOT NULL DEFAULT '';
ALTER TABLE detect_job_items
ADD COLUMN IF NOT EXISTS step_code VARCHAR(64) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS step_payload_json JSONB,
ADD COLUMN IF NOT EXISTS result_payload_json JSONB;
ALTER TABLE detect_job_items
DROP CONSTRAINT IF EXISTS uq_detect_job_items_job_domain;
CREATE UNIQUE INDEX IF NOT EXISTS idx_detect_job_items_job_domain_step
ON detect_job_items(job_id, domain_id, step_code);
CREATE TABLE IF NOT EXISTS detect_run_events (
id BIGSERIAL PRIMARY KEY,
job_id BIGINT REFERENCES detect_jobs(id) ON DELETE SET NULL,
job_item_id BIGINT REFERENCES detect_job_items(id) ON DELETE SET NULL,
node_code VARCHAR(64) NOT NULL DEFAULT '',
event_type VARCHAR(64) NOT NULL DEFAULT '',
level VARCHAR(16) NOT NULL DEFAULT 'info',
message TEXT NOT NULL DEFAULT '',
payload_json JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS detect_sync_records (
id BIGSERIAL PRIMARY KEY,
sync_type VARCHAR(32) NOT NULL DEFAULT '',
source_region VARCHAR(32) NOT NULL DEFAULT '',
target_region VARCHAR(32) NOT NULL DEFAULT '',
status VARCHAR(32) NOT NULL DEFAULT 'pending',
payload_json JSONB,
error_message TEXT NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
"""
return self.execute(sql)
def register_cluster_node(self, node_code, region, role, status='online', current_load=0, metadata=None):
try:
ip_addr = ''
try:
ip_addr = socket.gethostbyname(socket.gethostname())
except Exception:
pass
sql = """
INSERT INTO detect_worker_nodes (
node_code, region, role, hostname, ip, status, worker_version, current_load, metadata_json, last_heartbeat_at, update_time
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (node_code) DO UPDATE SET
region = EXCLUDED.region,
role = EXCLUDED.role,
hostname = EXCLUDED.hostname,
ip = EXCLUDED.ip,
status = EXCLUDED.status,
worker_version = EXCLUDED.worker_version,
current_load = EXCLUDED.current_load,
metadata_json = EXCLUDED.metadata_json,
last_heartbeat_at = CURRENT_TIMESTAMP,
update_time = CURRENT_TIMESTAMP
"""
return self.execute(
sql,
(
node_code,
region,
role,
socket.gethostname(),
ip_addr,
status,
'0.1.0',
int(current_load or 0),
json.dumps(metadata or {}, ensure_ascii=False),
),
)
except Exception as e:
logger.warning(f"注册检测节点失败: {e}")
return False
def claim_detect_job_items(self, node_code, limit=1000, lease_seconds=3600, job_id=None):
"""
领取一批待执行的任务项。
"""
conn = None
cur = None
claim_token = f"{node_code}-{int(threading.current_thread().ident or 0)}-{int(__import__('time').time())}"
try:
conn, cur = self.connect()
if not conn or not cur:
logger.error("领取检测任务失败: 无法获取数据库连接")
return []
normalized_job_id = int(job_id) if job_id not in (None, "", 0, "0") else None
normalized_limit = max(1, int(limit or 1))
normalized_lease_seconds = max(60, int(lease_seconds or 3600))
step_claim_quota = _resolve_step_claim_quota(normalized_limit)
rows = []
def _claim_rows(batch_limit: int, step_condition_sql: str = "", step_condition_params: tuple = ()) -> list:
safe_batch_limit = max(1, int(batch_limit or 0))
if safe_batch_limit <= 0:
return []
extra_condition = f"\n AND {step_condition_sql}" if step_condition_sql else ""
cur.execute(
f"""
WITH picked AS (
SELECT id
FROM detect_job_items
WHERE status IN ('pending', 'failed')
AND COALESCE(step_code, '') <> ''
AND (%s IS NULL OR job_id = %s)
AND (lease_expires_at IS NULL OR lease_expires_at < CURRENT_TIMESTAMP){extra_condition}
ORDER BY create_time ASC, id ASC
FOR UPDATE SKIP LOCKED
LIMIT %s
),
updated AS (
UPDATE detect_job_items AS item
SET status = 'claimed',
claimed_by = %s,
claim_token = %s,
lease_expires_at = CURRENT_TIMESTAMP + (%s || ' seconds')::interval,
attempt_count = item.attempt_count + 1,
started_at = COALESCE(item.started_at, CURRENT_TIMESTAMP),
updated_at = CURRENT_TIMESTAMP
FROM picked
WHERE item.id = picked.id
RETURNING item.id, item.job_id, item.domain_id, item.claim_token, item.step_code, item.step_payload_json
)
SELECT
updated.id,
updated.job_id,
updated.domain_id,
updated.claim_token,
updated.step_code,
detect_jobs.task_mode,
detect_jobs.job_code,
detect_jobs.step_code,
updated.step_payload_json,
domains.domain,
domains.source_type,
domains.register_status,
domains.detect_status,
domains.use_status,
domains.expire_date,
domains.jucha_status,
domains.juziseo_status
FROM updated
JOIN detect_jobs ON detect_jobs.id = updated.job_id
JOIN domains ON domains.id = updated.domain_id
ORDER BY updated.id ASC
""",
(
normalized_job_id,
normalized_job_id,
*step_condition_params,
safe_batch_limit,
node_code,
claim_token,
normalized_lease_seconds,
),
)
return cur.fetchall() or []
remaining = normalized_limit
for step_code in _ordered_step_claim_codes():
if remaining <= 0:
break
claimed_rows = _claim_rows(
min(remaining, step_claim_quota),
"COALESCE(step_code, '') = %s",
(str(step_code or "").strip(),),
)
if claimed_rows:
rows.extend(claimed_rows)
remaining -= len(claimed_rows)
if remaining > 0:
rows.extend(_claim_rows(remaining))
touched_job_ids = sorted({row[1] for row in rows})
for job_id in touched_job_ids:
cur.execute(
"""
UPDATE detect_jobs
SET status = CASE WHEN status = 'pending' THEN 'running' ELSE status END,
started_at = COALESCE(started_at, CURRENT_TIMESTAMP)
WHERE id = %s
""",
(job_id,),
)
conn.commit()
return [
{
"job_item_id": row[0],
"job_id": row[1],
"id": row[2],
"claim_token": row[3],
"item_step_code": row[4],
"task_mode": row[5],
"job_code": row[6],
"step_code": row[4] or row[7],
"step_payload": row[8],
"domain": row[9],
"source_type": row[10],
"register_status": row[11],
"detect_status": row[12],
"use_status": row[13],
"expire_date": row[14],
"jucha_status": row[15],
"juziseo_status": row[16],
}
for row in rows
]
except Exception as e:
try:
if conn:
conn.rollback()
except Exception:
pass
logger.error(f"领取检测任务失败: {e}")
return []
finally:
self.close(conn, cur)
def has_dispatchable_detect_job_items(self):
"""
是否仍存在可继续派发的标准步骤任务项。
这里显式排除空 step_code 的旧兼容链路项,避免 worker 在 single_step
任务已经大量积压时继续回退到旧 domains 链路补位。
"""
row = self.fetch_one(
"""
SELECT EXISTS (
SELECT 1
FROM detect_job_items
WHERE status IN ('pending', 'failed')
AND COALESCE(step_code, '') <> ''
AND (lease_expires_at IS NULL OR lease_expires_at < CURRENT_TIMESTAMP)
LIMIT 1
) AS has_items
"""
)
if isinstance(row, dict):
return bool(row.get("has_items"))
if isinstance(row, (list, tuple)) and row:
return bool(row[0])
return False
def recycle_expired_detect_job_items(self):
"""
回收租约过期但未完成的任务项,重新放回 pending。
"""
conn = None
cur = None
try:
conn, cur = self.connect()
if not conn or not cur:
return 0
cur.execute(
"""
WITH recycled AS (
UPDATE detect_job_items
SET status = 'pending',
claimed_by = '',
claim_token = '',
lease_expires_at = NULL,
updated_at = CURRENT_TIMESTAMP,
last_error = CASE
WHEN status = 'running' THEN 'lease expired while running'
WHEN status = 'claimed' THEN 'lease expired before running'
ELSE last_error
END
WHERE status IN ('claimed', 'running')
AND lease_expires_at IS NOT NULL
AND lease_expires_at < CURRENT_TIMESTAMP
RETURNING job_id
)
SELECT count(*), array_remove(array_agg(DISTINCT job_id), NULL)
FROM recycled
"""
)
row = cur.fetchone()
recycled_count = int((row or [0])[0] or 0)
touched_job_ids = (row or [0, []])[1] or []
for job_id in touched_job_ids:
self._refresh_detect_job_status_with_cursor(cur, job_id)
conn.commit()
return recycled_count
except Exception as e:
try:
if conn:
conn.rollback()
except Exception:
pass
logger.error(f"回收过期检测任务失败: {e}")
return 0
finally:
self.close(conn, cur)
def release_detect_job_items_for_node(self, node_code):
"""
释放指定节点遗留的 claimed/running 任务项,供节点重启后快速恢复。
"""
conn = None
cur = None
try:
conn, cur = self.connect()
if not conn or not cur:
return 0
cur.execute(
"""
WITH released AS (
UPDATE detect_job_items
SET status = 'pending',
claimed_by = '',
claim_token = '',
lease_expires_at = NULL,
updated_at = CURRENT_TIMESTAMP,
last_error = CASE
WHEN status = 'running' THEN 'released after worker restart'
WHEN status = 'claimed' THEN 'released before execution after worker restart'
ELSE last_error
END
WHERE claimed_by = %s
AND status IN ('claimed', 'running')
RETURNING job_id
)
SELECT count(*), array_remove(array_agg(DISTINCT job_id), NULL)
FROM released
""",
(node_code,),
)
row = cur.fetchone()
released_count = int((row or [0])[0] or 0)
touched_job_ids = (row or [0, []])[1] or []
for job_id in touched_job_ids:
self._refresh_detect_job_status_with_cursor(cur, job_id)
conn.commit()
return released_count
except Exception as e:
try:
if conn:
conn.rollback()
except Exception:
pass
logger.error(f"释放节点遗留任务失败: {e}")
return 0
finally:
self.close(conn, cur)
def get_active_detect_job(self):
"""
获取当前活动中的检测任务摘要,供 Worker 重启后自动回挂。
"""
return self.fetch_one(
"""
SELECT
job.id,
job.job_code,
job.task_mode,
job.status,
count(item.*) FILTER (WHERE item.status = 'pending') AS items_pending,
count(item.*) FILTER (WHERE item.status = 'claimed') AS items_claimed,
count(item.*) FILTER (WHERE item.status = 'running') AS items_running,
count(item.*) FILTER (WHERE item.status IN ('completed', 'blacklisted')) AS items_completed,
count(item.*) FILTER (WHERE item.status = 'failed') AS items_failed,
max(item.updated_at) AS latest_item_updated_at,
max(item.create_time) AS latest_item_created_at
FROM detect_jobs AS job
LEFT JOIN detect_job_items AS item
ON item.job_id = job.id
WHERE job.status IN ('pending', 'running')
GROUP BY job.id, job.job_code, job.task_mode, job.status
ORDER BY
CASE
WHEN count(item.*) FILTER (WHERE item.status IN ('claimed', 'running')) > 0 THEN 0
WHEN count(item.*) FILTER (WHERE item.status = 'pending') > 0 THEN 1
ELSE 2
END ASC,
CASE WHEN job.status = 'running' THEN 0 ELSE 1 END ASC,
COALESCE(max(item.updated_at), max(item.create_time), job.started_at, job.created_at) DESC,
job.id DESC
LIMIT 1
"""
)
def mark_detect_job_item_running(self, job_item_id, claim_token):
return self.execute(
"""
UPDATE detect_job_items
SET status = 'running',
updated_at = CURRENT_TIMESTAMP
WHERE id = %s AND claim_token = %s AND status = 'claimed'
""",
(job_item_id, claim_token),
)
def mark_detect_job_items_running_batch(self, items):
normalized_items = []
for item in list(items or []):
if not isinstance(item, (list, tuple)) or len(item) < 2:
continue
try:
job_item_id = int(item[0] or 0)
except Exception:
continue
claim_token = str(item[1] or "").strip()
if job_item_id <= 0 or not claim_token:
continue
normalized_items.append((job_item_id, claim_token))
if not normalized_items:
return 0
conn = None
cur = None
try:
conn, cur = self.connect()
if not conn or not cur:
return -1
values_sql = ",".join(
cur.mogrify("(%s, %s)", item).decode("utf-8")
for item in normalized_items
)
cur.execute(
f"""
UPDATE detect_job_items AS item
SET status = 'running',
updated_at = CURRENT_TIMESTAMP
FROM (VALUES {values_sql}) AS batch(id, claim_token)
WHERE item.id = batch.id
AND item.claim_token = batch.claim_token
AND item.status = 'claimed'
"""
)
updated_count = int(cur.rowcount or 0)
conn.commit()
return updated_count
except Exception as e:
try:
if conn:
conn.rollback()
except Exception:
pass
logger.error(f"批量标记检测任务项为运行中失败: {e}")
return -1
finally:
self.close(conn, cur)
def renew_detect_job_item_lease(self, job_item_id, claim_token, lease_seconds=3600):
return self.execute(
"""
UPDATE detect_job_items
SET lease_expires_at = CURRENT_TIMESTAMP + (%s || ' seconds')::interval,
updated_at = CURRENT_TIMESTAMP
WHERE id = %s AND claim_token = %s AND status IN ('claimed', 'running')
""",
(max(60, int(lease_seconds or 3600)), job_item_id, claim_token),
)
def append_detect_run_event(self, job_id, job_item_id, node_code, event_type, message, level='info', payload=None):
return self.execute(
"""
INSERT INTO detect_run_events (job_id, job_item_id, node_code, event_type, level, message, payload_json)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""",
(job_id, job_item_id, node_code, event_type, level, message, Json(payload or {})),
)
def complete_detect_job_item(self, job_item_id, claim_token, final_status='completed', result_payload=None, result_version='v1', refresh_job_status=True):
conn = None
cur = None
try:
conn, cur = self.connect()
if not conn or not cur:
return False
payload_json = json.dumps(result_payload, ensure_ascii=False) if result_payload is not None else None
cur.execute(
"""
UPDATE detect_job_items
SET status = %s,
result_payload_json = COALESCE(%s::jsonb, result_payload_json),
result_version = CASE
WHEN %s <> '' THEN %s
ELSE result_version
END,
finished_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP,
lease_expires_at = NULL
WHERE id = %s AND claim_token = %s
RETURNING job_id
""",
(
final_status,
payload_json,
str(result_version or '').strip(),
str(result_version or '').strip(),
job_item_id,
claim_token,
),
)
row = cur.fetchone()
if row and refresh_job_status:
self._refresh_detect_job_status_with_cursor(cur, row[0])
conn.commit()
return bool(row)
except Exception as e:
try:
if conn:
conn.rollback()
except Exception:
pass
logger.error(f"完成检测任务项失败: {e}")
return False
finally:
self.close(conn, cur)
def fail_detect_job_item(self, job_item_id, claim_token, reason='', result_payload=None, result_version='v1', refresh_job_status=True):
conn = None
cur = None
try:
conn, cur = self.connect()
if not conn or not cur:
return False
payload_json = json.dumps(result_payload, ensure_ascii=False) if result_payload is not None else None
cur.execute(
"""
UPDATE detect_job_items
SET status = 'failed',
last_error = %s,
result_payload_json = COALESCE(%s::jsonb, result_payload_json),
result_version = CASE
WHEN %s <> '' THEN %s
ELSE result_version
END,
finished_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP,
lease_expires_at = NULL
WHERE id = %s AND claim_token = %s
RETURNING job_id
""",
(
str(reason or '')[:1000],
payload_json,
str(result_version or '').strip(),
str(result_version or '').strip(),
job_item_id,
claim_token,
),
)
row = cur.fetchone()
if row and refresh_job_status:
self._refresh_detect_job_status_with_cursor(cur, row[0])
conn.commit()
return bool(row)
except Exception as e:
try:
if conn:
conn.rollback()
except Exception:
pass
logger.error(f"标记检测任务项失败: {e}")
return False
finally:
self.close(conn, cur)
def finalize_detect_job_items_batch(self, items):
normalized_items = []
event_rows = []
for item in list(items or []):
if not isinstance(item, dict):
continue
try:
job_item_id = int(item.get("job_item_id") or 0)
except Exception:
continue
claim_token = str(item.get("claim_token") or "").strip()
final_status = str(item.get("final_status") or "").strip() or "failed"
message = str(item.get("message") or "").strip()
result_payload = item.get("result_payload")
result_version = str(item.get("result_version") or "v1").strip()
if job_item_id <= 0 or not claim_token:
continue
payload_json = ""
if result_payload is not None:
try:
payload_json = json.dumps(result_payload, ensure_ascii=False)
except Exception:
payload_json = ""
normalized_items.append(
(
job_item_id,
claim_token,
final_status,
message[:1000],
payload_json,
result_version,
)
)
try:
event_job_id = int(item.get("job_id") or 0)
except Exception:
event_job_id = 0
event_type = str(item.get("event_type") or "").strip()
event_message = str(item.get("event_message") or "").strip()
if event_job_id > 0 and event_type and event_message:
event_payload_json = "{}"
try:
event_payload_json = json.dumps(item.get("event_payload") or {}, ensure_ascii=False)
except Exception:
event_payload_json = "{}"
event_rows.append(
(
job_item_id,
event_job_id,
str(item.get("node_code") or "").strip(),
event_type,
str(item.get("event_level") or "info").strip() or "info",
event_message[:1000],
event_payload_json,
)
)
if not normalized_items:
return 0
conn = None
cur = None
try:
conn, cur = self.connect()
if not conn or not cur:
return -1
values_sql = ",".join(
cur.mogrify("(%s, %s, %s, %s, %s, %s)", item).decode("utf-8")
for item in normalized_items
)
cur.execute(
f"""
UPDATE detect_job_items AS item
SET status = batch.final_status,
last_error = CASE
WHEN batch.final_status = 'failed' AND batch.message <> '' THEN batch.message
ELSE item.last_error
END,
result_payload_json = COALESCE(NULLIF(batch.payload_json, '')::jsonb, item.result_payload_json),
result_version = CASE
WHEN batch.result_version <> '' THEN batch.result_version
ELSE item.result_version
END,
finished_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP,
lease_expires_at = NULL
FROM (
VALUES {values_sql}
) AS batch(id, claim_token, final_status, message, payload_json, result_version)
WHERE item.id = batch.id
AND item.claim_token = batch.claim_token
AND item.status IN ('claimed', 'running')
RETURNING item.id
"""
)
updated_item_ids = {int(row[0]) for row in (cur.fetchall() or [])}
updated_count = len(updated_item_ids)
filtered_event_rows = [row for row in event_rows if int(row[0]) in updated_item_ids]
if filtered_event_rows:
values_sql = ",".join(
cur.mogrify("(%s, %s, %s, %s, %s, %s, %s::jsonb)", row).decode("utf-8")
for row in filtered_event_rows
)
cur.execute(
f"""
INSERT INTO detect_run_events (
job_item_id,
job_id,
node_code,
event_type,
level,
message,
payload_json
)
VALUES {values_sql}
"""
)
conn.commit()
return updated_count
except Exception as e:
try:
if conn:
conn.rollback()
except Exception:
pass
logger.error(f"批量完成检测任务项失败: {e}")
return -1
finally:
self.close(conn, cur)
def _refresh_detect_job_status_with_cursor(self, cur, job_id):
cur.execute(
"""
SELECT
job.task_mode,
count(*) FILTER (WHERE detect_job_items.status = 'pending') AS pending_count,
count(*) FILTER (WHERE detect_job_items.status IN ('claimed', 'running')) AS dispatch_active_count,
count(*) FILTER (WHERE detect_job_items.status = 'failed') AS failed_count,
count(*) FILTER (WHERE detect_job_items.status IN ('completed', 'blacklisted')) AS done_count,
count(*) FILTER (
WHERE detect_job_items.status IN ('completed', 'blacklisted', 'failed')
AND COALESCE(detect_job_items.step_code, '') <> ''
AND COALESCE(result_payload_json->>'controller_processed', 'false') <> 'true'
) AS unprocessed_terminal_count
FROM detect_job_items
JOIN detect_jobs AS job ON job.id = detect_job_items.job_id
WHERE job_id = %s
GROUP BY job.task_mode
""",
(job_id,),
)
row = cur.fetchone()
task_mode = str((row or [""])[0] or "").strip()
pending_count = int((row or ["", 0])[1] or 0)
dispatch_active_count = int((row or ["", 0, 0])[2] or 0)
failed_count = int((row or ["", 0, 0, 0])[3] or 0)
done_count = int((row or ["", 0, 0, 0, 0])[4] or 0)
unprocessed_terminal_count = int((row or ["", 0, 0, 0, 0, 0])[5] or 0)
if dispatch_active_count > 0 or (task_mode == 'domain_pipeline' and unprocessed_terminal_count > 0):
cur.execute(
"UPDATE detect_jobs SET status = 'running', started_at = COALESCE(started_at, CURRENT_TIMESTAMP) WHERE id = %s",
(job_id,),
)
return
if pending_count > 0:
cur.execute(
"""
UPDATE detect_jobs
SET status = 'pending',
finished_at = NULL
WHERE id = %s
""",
(job_id,),
)
return
final_status = 'completed'
if failed_count > 0 and done_count > 0:
final_status = 'partial_failed'
elif failed_count > 0:
final_status = 'failed'
cur.execute(
"""
UPDATE detect_jobs
SET status = %s,
finished_at = CURRENT_TIMESTAMP,
started_at = COALESCE(started_at, CURRENT_TIMESTAMP)
WHERE id = %s
""",
(final_status, job_id),
)
def refresh_detect_job_status(self, job_id):
conn = None
cur = None
try:
conn, cur = self.connect()
if not conn or not cur:
return False
self._refresh_detect_job_status_with_cursor(cur, job_id)
conn.commit()
return True
except Exception as e:
try:
if conn:
conn.rollback()
except Exception:
pass
logger.error(f"刷新检测任务状态失败: {e}")
return False
finally:
self.close(conn, cur)
def fetch_one(self, sql, params=None):
"""
获取单条数据
:param sql: SQL语句
:param params: 参数
:return: dict - 数据
"""
import threading
thread_id = threading.current_thread().ident
for attempt in range(2):
conn = None
cur = None
try:
conn, cur = self.connect(thread_id)
if not conn or not cur:
logger.warning(f"线程 {thread_id} 数据库连接失败返回None")
return None
cur.execute(sql, params)
row = cur.fetchone()
if row:
columns = [desc[0] for desc in cur.description]
return dict(zip(columns, row))
return None
except (psycopg2.OperationalError, psycopg2.InterfaceError) as e:
logger.warning(f"查询数据连接异常,第 {attempt + 1} 次: {sql}, 错误: {e}")
try:
if conn and not conn.closed:
conn.close()
except Exception:
pass
if attempt == 0:
continue
logger.error(f"查询数据失败: {sql}, 错误: {e}")
return None
except Exception as e:
logger.error(f"查询数据失败: {sql}, 错误: {e}")
return None
finally:
self.close(conn, cur)
def fetch_all(self, sql, params=None):
"""
获取多条数据
:param sql: SQL语句
:param params: 参数
:return: list - 数据列表
"""
import threading
thread_id = threading.current_thread().ident
for attempt in range(2):
conn = None
cur = None
try:
conn, cur = self.connect(thread_id)
if not conn or not cur:
logger.warning(f"线程 {thread_id} 数据库连接失败,返回空结果")
return []
cur.execute(sql, params)
try:
rows = cur.fetchall()
if rows and cur.description:
columns = [desc[0] for desc in cur.description]
return [dict(zip(columns, row)) for row in rows]
return []
except Exception as e:
if "no results to fetch" in str(e):
return []
raise
except (psycopg2.OperationalError, psycopg2.InterfaceError) as e:
logger.warning(f"查询数据连接异常,第 {attempt + 1} 次: {sql}, 错误: {e}")
try:
if conn and not conn.closed:
conn.close()
except Exception:
pass
if attempt == 0:
continue
logger.error(f"查询数据失败: {sql}, 错误: {e}")
return []
except Exception as e:
logger.error(f"查询数据失败: {sql}, 错误: {e}")
return []
finally:
self.close(conn, cur)
def domain_exists(self, domain):
"""
检查域名是否存在
:param domain: 域名
:return: bool - 是否存在
"""
# 尝试使用布隆过滤器
if self.use_redis and self.use_bloom_filter:
try:
if not self.redis_client.execute_command('BF.EXISTS', 'domain_bloom', domain):
# 布隆过滤器判断不存在,直接返回 False
return False
except Exception as e:
logger.warning(f"布隆过滤器查询失败: {e}")
# 尝试使用 Redis 缓存
if self.use_redis:
try:
if self.redis_client.exists(f"domain:{domain}"):
return True
except Exception as e:
logger.warning(f"Redis 查询失败: {e}")
# 缓存未命中或 Redis 不可用,查询数据库
sql = "SELECT id FROM domains WHERE domain = %s"
result = self.fetch_one(sql, (domain,))
# 将结果存入缓存和布隆过滤器
if self.use_redis and result:
try:
pipe = self.redis_client.pipeline()
pipe.set(f"domain:{domain}", 1, ex=2592000) # 1个月过期
if self.use_bloom_filter:
pipe.execute_command('BF.ADD', 'domain_bloom', domain)
pipe.execute()
except Exception as e:
logger.warning(f"Redis 存储失败: {e}")
return result is not None
def check_domains_exist(self, domains):
"""
批量检查域名是否存在
:param domains: 域名列表
:return: list - 存在的域名列表
"""
if not domains:
return []
# 尝试使用布隆过滤器快速过滤
if self.use_redis and self.use_bloom_filter:
try:
# 分批使用布隆过滤器过滤不存在的域名
possibly_exist = []
batch_size = 10000
for i in range(0, len(domains), batch_size):
batch = domains[i:i+batch_size]
# 使用管道批量执行布隆过滤器查询
pipe = self.redis_client.pipeline()
for domain in batch:
pipe.execute_command('BF.EXISTS', 'domain_bloom', domain)
results = pipe.execute()
# 处理结果
for domain, exists in zip(batch, results):
if exists:
possibly_exist.append(domain)
# 每处理一批,记录一次进度
if (i + len(batch)) % (batch_size * 10) == 0:
logger.info(f"布隆过滤器已过滤 {min(i+len(batch), len(domains))}/{len(domains)} 个域名")
# 如果布隆过滤器判断所有域名都不存在,直接返回空列表
if not possibly_exist:
logger.info(f"布隆过滤器快速过滤: {len(domains)} 个域名不存在")
return []
# 只查询可能存在的域名
domains = possibly_exist
logger.info(f"布隆过滤器过滤后,剩余 {len(domains)} 个域名需要查询数据库")
except Exception as e:
logger.warning(f"布隆过滤器批量查询失败: {e}")
import threading
thread_id = threading.current_thread().ident
conn = None
cur = None
try:
# 从连接池获取连接
conn, cur = self.connect(thread_id)
if not conn or not cur:
# 数据库连接失败,返回空列表
logger.warning(f"线程 {thread_id} 数据库连接失败,返回空列表")
return []
# 分批查询数据库
existing_domains = []
batch_size = 10000
for i in range(0, len(domains), batch_size):
batch = domains[i:i+batch_size]
# 使用IN子句批量查询
placeholders = ','.join(['%s'] * len(batch))
sql = f"SELECT domain FROM domains WHERE domain IN ({placeholders})"
cur.execute(sql, batch)
rows = cur.fetchall()
batch_existing = [row[0] for row in rows]
existing_domains.extend(batch_existing)
# 每处理一批,记录一次进度
if (i + len(batch)) % (batch_size * 10) == 0:
logger.info(f"数据库已查询 {min(i+len(batch), len(domains))}/{len(domains)} 个域名")
# 将实际存在的域名添加到缓存,分批进行
if self.use_redis and existing_domains:
try:
batch_size = 10000
for i in range(0, len(existing_domains), batch_size):
batch = existing_domains[i:i+batch_size]
pipe = self.redis_client.pipeline()
for domain in batch:
pipe.set(f"domain:{domain}", 1, ex=3600)
pipe.execute()
except Exception as e:
logger.warning(f"Redis 批量存储失败: {e}")
logger.info(f"批量检查域名完成,发现 {len(existing_domains)} 个已存在域名")
return existing_domains
except Exception as e:
logger.error(f"批量检查域名存在失败: {e}")
return []
finally:
# 将连接放回连接池
self.close(conn, cur)
def add_domain(self, domain, tld, source_type):
"""
添加域名
:param domain: 域名
:param tld: 顶级域名
:param source_type: 来源类型
:return: int - 域名ID
"""
sql = """
INSERT INTO domains (domain, tld, source_type, use_status, detect_status, register_status, backlink_count)
VALUES (%s, %s, %s, 0, 0, 0, 0)
ON CONFLICT (domain) DO NOTHING
RETURNING id
"""
import threading
thread_id = threading.current_thread().ident
conn = None
cur = None
try:
# 从连接池获取连接
conn, cur = self.connect(thread_id)
if not conn or not cur:
# 数据库连接失败返回None
logger.warning(f"线程 {thread_id} 数据库连接失败返回None")
return None
cur.execute(sql, (domain, tld, source_type))
result = cur.fetchone()
conn.commit()
# 如果域名已存在返回None
if not result:
return None
domain_id = result[0]
# 将结果存入缓存和布隆过滤器
if self.use_redis:
try:
pipe = self.redis_client.pipeline()
pipe.set(f"domain:{domain}", 1, ex=2592000) # 1个月过期
if self.use_bloom_filter:
pipe.execute_command('BF.ADD', 'domain_bloom', domain)
pipe.execute()
except Exception as e:
logger.warning(f"Redis 存储失败: {e}")
return domain_id
except Exception as e:
logger.error(f"添加域名失败: {e}")
if conn:
try:
conn.rollback()
except:
pass
return None
finally:
# 将连接放回连接池
self.close(conn, cur)
def add_domains_batch(self, domains):
"""
批量添加域名
:param domains: 域名列表,每个元素为 (domain, tld, source_type)
:return: int - 添加成功的数量
"""
if not domains:
return 0
import threading
thread_id = threading.current_thread().ident
conn = None
cur = None
try:
total_added = 0
batch_size = 1000
# 从连接池获取连接
conn, cur = self.connect(thread_id)
if not conn or not cur:
# 数据库连接失败返回0
logger.warning(f"线程 {thread_id} 数据库连接失败返回0")
return 0
# 分批处理
for i in range(0, len(domains), batch_size):
batch = domains[i:i+batch_size]
# 使用批量插入语法
placeholders = ','.join(['(%s, %s, %s, 0, 0, 0, 0)'] * len(batch))
sql = f"""
INSERT INTO domains (domain, tld, source_type, use_status, detect_status, register_status, backlink_count)
VALUES {placeholders}
ON CONFLICT (domain) DO NOTHING
"""
# 扁平化数据
data = []
domain_names = []
for domain, tld, source_type in batch:
data.extend([domain, tld, source_type])
domain_names.append(domain)
cur.execute(sql, data)
added_count = cur.rowcount
total_added += added_count
conn.commit()
# 为新添加的域名创建检测任务
if added_count > 0:
# 获取刚添加的域名ID
placeholders = ','.join(['%s'] * len(batch))
sql = f"SELECT id, domain FROM domains WHERE domain IN ({placeholders})"
cur.execute(sql, domain_names)
rows = cur.fetchall()
domain_ids = [row[0] for row in rows]
# 将新添加的域名添加到 Redis 缓存和布隆过滤器
if self.use_redis:
try:
pipe = self.redis_client.pipeline()
for row in rows:
domain = row[1]
pipe.set(f"domain:{domain}", 1, ex=2592000) # 1个月过期
if self.use_bloom_filter:
pipe.execute_command('BF.ADD', 'domain_bloom', domain)
pipe.execute()
except Exception as e:
logger.warning(f"Redis 批量存储失败: {e}")
# 批量创建检测任务
if domain_ids:
task_placeholders = ','.join(['(%s, 1, 0, 0, 0)'] * len(domain_ids))
task_sql = f"""
INSERT INTO detect_tasks (domain_id, task_type, status, priority, retry_count)
VALUES {task_placeholders}
"""
task_data = []
for domain_id in domain_ids:
task_data.append(domain_id)
cur.execute(task_sql, task_data)
conn.commit()
# 每处理一批,记录一次进度
if (i + len(batch)) % (batch_size * 10) == 0:
logger.info(f"已添加 {i + len(batch)}/{len(domains)} 个域名")
logger.info(f"批量添加域名完成,成功添加 {total_added} 个域名")
return total_added
except Exception as e:
logger.error(f"批量添加域名失败: {e}")
if conn:
try:
conn.rollback()
except:
pass
return 0
finally:
# 将连接放回连接池
self.close(conn, cur)
def get_domain_by_id(self, domain_id):
"""
根据ID获取域名
:param domain_id: 域名ID
:return: dict - 域名信息
"""
sql = "SELECT * FROM domains WHERE id = %s"
return self.fetch_one(sql, (domain_id,))
def get_domain_by_name(self, domain):
"""
根据域名获取信息
:param domain: 域名
:return: dict - 域名信息
"""
sql = "SELECT * FROM domains WHERE domain = %s"
return self.fetch_one(sql, (domain,))
def update_domain_use_status(self, domain_id, status):
"""
更新域名使用状态
:param domain_id: 域名ID
:param status: 状态
:return: bool - 是否更新成功
"""
sql = "UPDATE domains SET use_status = %s WHERE id = %s"
return self.execute(sql, (status, domain_id))
def update_domain_detect_status(self, domain_id, status):
"""
更新域名检测状态
:param domain_id: 域名ID
:param status: 状态
:return: bool - 是否更新成功
"""
if status == DETECT_STATUS_COMPLETED:
sql = "UPDATE domains SET detect_status = %s, detect_time = CURRENT_TIMESTAMP WHERE id = %s"
else:
sql = "UPDATE domains SET detect_status = %s WHERE id = %s"
return self.execute(sql, (status, domain_id))
def update_domain_detect_status_batch(self, items):
"""
批量更新域名检测状态,减少高并发失败风暴时的写库往返。
"""
normalized_items = []
for item in items or []:
try:
domain_id, status = item
normalized_items.append((int(domain_id), int(status)))
except Exception:
continue
if not normalized_items:
return 0
values_sql = ", ".join(["(%s, %s)"] * len(normalized_items))
params = []
for domain_id, status in normalized_items:
params.extend([domain_id, status])
sql = f"""
UPDATE domains AS d
SET detect_status = v.detect_status,
detect_time = CASE
WHEN v.detect_status = %s THEN CURRENT_TIMESTAMP
ELSE d.detect_time
END
FROM (
VALUES {values_sql}
) AS v(domain_id, detect_status)
WHERE d.id = v.domain_id
"""
params = [DETECT_STATUS_COMPLETED] + params
return len(normalized_items) if self.execute(sql, tuple(params)) else -1
def recycle_running_domains(self, target_status):
"""
回收异常中断后遗留的“检测中”状态。
:param target_status: 目标状态,通常使用 DETECT_STATUS_FAILED
:return: int - 影响行数
"""
conn, cur = self.connect()
if not conn or not cur:
logger.error("回收检测中状态失败: 无法获取数据库连接")
return 0
try:
cur.execute(
"UPDATE domains SET detect_status = %s WHERE detect_status = %s",
(target_status, DETECT_STATUS_RUNNING)
)
affected = cur.rowcount or 0
conn.commit()
return affected
except Exception as e:
try:
conn.rollback()
except Exception:
pass
logger.error(f"回收检测中状态失败: {e}")
return 0
finally:
self.close(conn, cur)
def update_domain_third_party_status(self, domain_id, field_name, status):
"""
更新第三方平台检测状态。
"""
if field_name not in {'jucha_status', 'juziseo_status'}:
logger.error(f"不支持的第三方状态字段: {field_name}")
return False
sql = f"UPDATE domains SET {field_name} = %s WHERE id = %s"
return self.execute(sql, (status, domain_id))
def mark_jucha_detected(self, domain_id):
return self.update_domain_third_party_status(domain_id, 'jucha_status', THIRD_PARTY_STATUS_DONE)
def mark_juziseo_detected(self, domain_id):
return self.update_domain_third_party_status(domain_id, 'juziseo_status', THIRD_PARTY_STATUS_DONE)
def reset_optional_detection_statuses(self, domain_id, *, jucha=False, juziseo=False):
fields = []
params = []
if jucha:
fields.append("jucha_status = 0")
if juziseo:
fields.append("juziseo_status = 0")
if not fields:
return True
sql = f"UPDATE domains SET {', '.join(fields)} WHERE id = %s"
params.append(domain_id)
return self.execute(sql, tuple(params))
def update_domain_expire_date(self, domain_id, expire_date):
"""
更新域名过期时间
:param domain_id: 域名ID
:param expire_date: 过期时间
:return: bool - 是否更新成功
"""
sql = "UPDATE domains SET expire_date = %s WHERE id = %s"
return self.execute(sql, (expire_date, domain_id))
def get_domains_to_detect(self, limit=1000, detect_options=None):
"""
获取需要检测的域名
:param limit: 限制数量
:return: list - 域名列表
"""
detect_options = detect_options or {}
conditions = [
"detect_status IN (%s, %s)",
"(use_status = 0 AND detect_status = %s AND register_status = %s AND expire_date < CURRENT_DATE)",
]
params = [
DETECT_STATUS_PENDING,
DETECT_STATUS_FAILED,
DETECT_STATUS_COMPLETED,
REGISTER_STATUS_REGISTERED,
]
if detect_options.get('detect_jucha'):
conditions.append("(detect_status <> %s AND jucha_status = 0)")
params.append(DETECT_STATUS_BLACKLISTED)
if detect_options.get('detect_juziseo'):
conditions.append("(detect_status <> %s AND juziseo_status = 0)")
params.append(DETECT_STATUS_BLACKLISTED)
sql = f"""
SELECT id, domain, source_type, register_status, detect_status, use_status, expire_date, jucha_status, juziseo_status
FROM domains
WHERE {" OR ".join(conditions)}
ORDER BY id ASC
LIMIT %s
"""
params.append(limit)
return self.fetch_all(sql, tuple(params))
def get_all_sensitive_words(self):
"""
获取所有敏感词
:return: list - 敏感词列表
"""
try:
sql = "SELECT word FROM sensitive_words"
results = self.fetch_all(sql)
sensitive_words = []
for row in results:
if isinstance(row, dict) and 'word' in row:
sensitive_words.append(row['word'])
return sensitive_words
except Exception as e:
logger.error(f"获取敏感词失败: {e}")
return []
def add_to_blacklist(self, domain, reason):
"""
将域名加入黑名单
:param domain: 域名
:param reason: 加入黑名单的原因
:return: bool - 是否操作成功
"""
sql = """
INSERT INTO blacklist (domain, reason, created_at)
VALUES (%s, %s, NOW())
ON CONFLICT (domain) DO UPDATE
SET reason = %s, updated_at = NOW()
"""
return self.execute(sql, (domain, reason, reason))
def mark_domain_blacklisted(self, domain_id, domain, reason):
"""
在一个事务中同时更新域名黑名单状态和黑名单表,减少热路径往返。
"""
conn = None
cur = None
try:
conn, cur = self.connect()
if not conn or not cur:
logger.error("标记域名黑名单失败: 无法获取数据库连接")
return False
cur.execute(
"UPDATE domains SET detect_status = %s WHERE id = %s",
(DETECT_STATUS_BLACKLISTED, domain_id),
)
cur.execute(
"""
INSERT INTO blacklist (domain, reason, created_at)
VALUES (%s, %s, NOW())
ON CONFLICT (domain) DO UPDATE
SET reason = %s, updated_at = NOW()
""",
(domain, reason, reason),
)
conn.commit()
return True
except Exception as e:
try:
if conn:
conn.rollback()
except Exception:
pass
logger.error(f"标记域名黑名单失败: {e}")
return False
finally:
self.close(conn, cur)
def update_domain_register_status(self, domain_id, status):
"""
更新域名注册状态
:param domain_id: 域名ID
:param status: 状态
:return: bool - 是否更新成功
"""
sql = "UPDATE domains SET register_status = %s WHERE id = %s"
return self.execute(sql, (status, domain_id))
def update_domain_register_result(self, domain_id, status, expire_date=None):
"""
一次性更新注册状态及过期时间,减少热路径往返次数。
:param domain_id: 域名ID
:param status: 注册状态
:param expire_date: 过期时间;为空时保持原值
:return: bool - 是否更新成功
"""
normalized_expire_date = str(expire_date).strip() if expire_date not in (None, "") else None
sql = """
UPDATE domains
SET register_status = %s,
expire_date = COALESCE(%s, expire_date)
WHERE id = %s
"""
return self.execute(sql, (status, normalized_expire_date, domain_id))
def update_domain_beian_info(self, domain_id, company_type, website_url, has_beian, beian_year):
"""
更新域名备案信息
:param domain_id: 域名ID
:param company_type: 单位性质
:param website_url: 网站首页网址
:param has_beian: 是否备案
:param beian_year: 备案年份
:return: bool - 是否更新成功
"""
sql = "UPDATE domains SET company_type = %s, website_url = %s, has_beian = %s, beian_year = %s WHERE id = %s"
return self.execute(sql, (company_type, website_url, has_beian, beian_year, domain_id))
def update_domain_beian_info_and_mark_jucha_detected(
self,
domain_id,
company_type=None,
website_url=None,
has_beian=None,
beian_year=None,
):
"""
一次性更新备案信息并标记聚查已完成,减少热路径写库次数。
"""
sql = """
UPDATE domains
SET company_type = COALESCE(%s, company_type),
website_url = COALESCE(%s, website_url),
has_beian = COALESCE(%s, has_beian),
beian_year = COALESCE(%s, beian_year),
jucha_status = %s
WHERE id = %s
"""
return self.execute(
sql,
(company_type, website_url, has_beian, beian_year, THIRD_PARTY_STATUS_DONE, domain_id),
)
def update_domain_review_status(self, domain_id, review_status):
"""
更新域名复核状态
:param domain_id: 域名ID
:param review_status: 复核状态
:return: bool - 是否更新成功
"""
sql = "UPDATE domains SET review_status = %s WHERE id = %s"
return self.execute(sql, (review_status, domain_id))
def update_domain_review_status_batch(self, items):
"""
批量更新域名复核状态。
"""
normalized_items = []
for item in items or []:
try:
domain_id, review_status = item
normalized_items.append((int(domain_id), int(review_status)))
except Exception:
continue
if not normalized_items:
return 0
values_sql = ", ".join(["(%s, %s)"] * len(normalized_items))
params = []
for domain_id, review_status in normalized_items:
params.extend([domain_id, review_status])
sql = f"""
UPDATE domains AS d
SET review_status = v.review_status
FROM (
VALUES {values_sql}
) AS v(domain_id, review_status)
WHERE d.id = v.domain_id
"""
return len(normalized_items) if self.execute(sql, tuple(params)) else -1
def update_domain_snapshot_years(self, domain_id, years):
"""
更新域名快照年份
:param domain_id: 域名ID
:param years: 年份字符串
:return: bool - 是否更新成功
"""
sql = "UPDATE domains SET snapshot_years = %s WHERE id = %s"
return self.execute(sql, (years, domain_id))
def update_domain_wayback_summary(self, domain_id, years=None, backlink_count=None):
"""
一次性更新时光机摘要字段,减少 domains 表写入次数。
"""
sql = """
UPDATE domains
SET snapshot_years = COALESCE(%s, snapshot_years),
backlink_count = COALESCE(%s, backlink_count)
WHERE id = %s
"""
return self.execute(sql, (years, backlink_count, domain_id))
def complete_domain_detection(self, domain_id, *, register_status, use_status, expire_date):
"""
一次性完成域名完成态、待复核态、过期时间归零逻辑。
:param domain_id: 域名ID
:param register_status: 当前注册状态
:param use_status: 当前使用状态
:param expire_date: 当前过期时间
:return: bool - 是否更新成功
"""
has_expire_date = bool(expire_date)
sql = """
UPDATE domains
SET detect_status = %s,
detect_time = CURRENT_TIMESTAMP,
expire_date = CASE
WHEN %s = %s AND %s = 0 AND %s THEN NULL
ELSE expire_date
END,
review_status = CASE
WHEN %s = %s THEN %s
ELSE review_status
END
WHERE id = %s
"""
return self.execute(
sql,
(
DETECT_STATUS_COMPLETED,
register_status,
REGISTER_STATUS_AVAILABLE,
use_status,
has_expire_date,
register_status,
REGISTER_STATUS_AVAILABLE,
REVIEW_STATUS_PENDING,
domain_id,
),
)
def complete_domain_detection_batch(self, items):
"""
批量更新域名完成态,减少 completed 尾部的单条写库往返。
"""
normalized_items = []
for item in items or []:
try:
domain_id, register_status, use_status, has_expire_date = item
normalized_items.append(
(
int(domain_id),
int(register_status),
int(use_status),
bool(has_expire_date),
)
)
except Exception:
continue
if not normalized_items:
return 0
values_sql = ", ".join(["(%s, %s, %s, %s)"] * len(normalized_items))
params = []
for domain_id, register_status, use_status, has_expire_date in normalized_items:
params.extend([domain_id, register_status, use_status, has_expire_date])
sql = f"""
UPDATE domains AS d
SET detect_status = %s,
detect_time = CURRENT_TIMESTAMP,
expire_date = CASE
WHEN v.register_status = %s AND v.use_status = 0 AND v.has_expire_date THEN NULL
ELSE d.expire_date
END,
review_status = CASE
WHEN v.register_status = %s THEN %s
ELSE d.review_status
END
FROM (
VALUES {values_sql}
) AS v(domain_id, register_status, use_status, has_expire_date)
WHERE d.id = v.domain_id
"""
params = [
DETECT_STATUS_COMPLETED,
REGISTER_STATUS_AVAILABLE,
REGISTER_STATUS_AVAILABLE,
REVIEW_STATUS_PENDING,
] + params
return len(normalized_items) if self.execute(sql, tuple(params)) else -1
def create_detect_task(self, domain_id, task_type, priority=0):
"""
创建检测任务
:param domain_id: 域名ID
:param task_type: 任务类型
:param priority: 优先级
:return: int - 任务ID
"""
sql = """
INSERT INTO detect_tasks (domain_id, task_type, status, priority, retry_count)
VALUES (%s, %s, 0, %s, 0)
RETURNING id
"""
import threading
thread_id = threading.current_thread().ident
conn = None
cur = None
try:
conn, cur = self.connect(thread_id)
if not conn or not cur:
return None
cur.execute(sql, (domain_id, task_type, priority))
row = cur.fetchone()
conn.commit()
return row[0] if row else None
except Exception as e:
logger.error(f"创建检测任务失败: {e}")
if conn:
conn.rollback()
return None
finally:
self.close(conn, cur)
def get_pending_task(self):
"""
获取待执行的任务
:return: dict - 任务信息
"""
sql = """
SELECT * FROM detect_tasks
WHERE status = 0
ORDER BY priority DESC, create_time ASC
LIMIT 1
"""
return self.fetch_one(sql)
def get_task_by_id(self, task_id):
"""
根据ID获取任务
:param task_id: 任务ID
:return: dict - 任务信息
"""
sql = "SELECT * FROM detect_tasks WHERE id = %s"
return self.fetch_one(sql, (task_id,))
def update_task_status(self, task_id, status):
"""
更新任务状态
:param task_id: 任务ID
:param status: 状态
:return: bool - 是否更新成功
"""
sql = "UPDATE detect_tasks SET status = %s WHERE id = %s"
return self.execute(sql, (status, task_id))
def update_task_retry_count(self, task_id, retry_count):
"""
更新任务重试次数
:param task_id: 任务ID
:param retry_count: 重试次数
:return: bool - 是否更新成功
"""
sql = "UPDATE detect_tasks SET retry_count = %s WHERE id = %s"
return self.execute(sql, (retry_count, task_id))
def get_failed_tasks(self):
"""
获取失败的任务
:return: list - 任务列表
"""
sql = "SELECT * FROM detect_tasks WHERE status = 3"
return self.fetch_all(sql)
def clear_completed_tasks(self, days):
"""
清理已完成的任务
:param days: 保留天数
:return: int - 清理的任务数量
"""
sql = "DELETE FROM detect_tasks WHERE status = 2 AND create_time < NOW() - (%s * INTERVAL '1 day')"
import threading
thread_id = threading.current_thread().ident
conn = None
cur = None
try:
conn, cur = self.connect(thread_id)
if not conn or not cur:
return 0
cur.execute(sql, (days,))
count = cur.rowcount
conn.commit()
return count
except Exception as e:
logger.error(f"清理已完成任务失败: {e}")
if conn:
conn.rollback()
return 0
finally:
self.close(conn, cur)
def add_to_blacklist(self, domain, reason):
"""
添加到黑名单
:param domain: 域名
:param reason: 原因
:return: bool - 是否添加成功
"""
sql = """
INSERT INTO domain_blacklist (domain, reason)
VALUES (%s, %s)
ON CONFLICT (domain) DO NOTHING
"""
return self.execute(sql, (domain, reason))
def is_blacklisted(self, domain):
"""
检查域名是否在黑名单中
:param domain: 域名
:return: bool - 是否在黑名单中
"""
sql = "SELECT id FROM domain_blacklist WHERE domain = %s"
result = self.fetch_one(sql, (domain,))
return result is not None
def add_detection_result(self, domain_id, baidu_history, baidu_site, qihu360_site, google_site, chinaz_info, aizhan_info, juziseo_info, jucha_info):
"""
添加检测结果
:param domain_id: 域名ID
:param baidu_history: 百度历史
:param baidu_site: 百度site
:param qihu360_site: 360 site
:param google_site: Google site
:param chinaz_info: 站长之家信息
:param aizhan_info: 爱站网信息
:param juziseo_info: 桔子SEO信息
:param jucha_info: 聚查信息
:return: bool - 是否添加成功
"""
import threading
thread_id = threading.current_thread().ident
conn = None
cur = None
try:
conn, cur = self.connect(thread_id)
if not conn or not cur:
return False
payload = (
json.dumps(baidu_history) if baidu_history is not None else None,
json.dumps(baidu_site) if baidu_site is not None else None,
json.dumps(qihu360_site) if qihu360_site is not None else None,
json.dumps(google_site) if google_site is not None else None,
json.dumps(chinaz_info) if chinaz_info is not None else None,
json.dumps(aizhan_info) if aizhan_info is not None else None,
json.dumps(juziseo_info) if juziseo_info is not None else None,
json.dumps(jucha_info) if jucha_info is not None else None,
)
cur.execute("SELECT id FROM domain_detections WHERE domain_id = %s ORDER BY id ASC LIMIT 1", (domain_id,))
exists = cur.fetchone()
if exists:
sql = """
UPDATE domain_detections
SET baidu_history = %s, baidu_site = %s, qihu360_site = %s, google_site = %s,
chinaz_info = %s, aizhan_info = %s, juziseo_info = %s, jucha_info = %s,
update_time = CURRENT_TIMESTAMP
WHERE domain_id = %s
"""
cur.execute(sql, payload + (domain_id,))
else:
sql = """
INSERT INTO domain_detections (
domain_id, baidu_history, baidu_site, qihu360_site, google_site,
chinaz_info, aizhan_info, juziseo_info, jucha_info
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
cur.execute(sql, (domain_id,) + payload)
conn.commit()
return True
except Exception as e:
logger.error(f"添加检测结果失败: {e}")
if conn:
conn.rollback()
return False
finally:
self.close(conn, cur)
def get_domains_by_conditions(self, conditions, page=1, page_size=1000):
"""
根据条件获取域名
:param conditions: 条件
:param page: 页码从1开始
:param page_size: 每页数量
:return: list - 域名列表
"""
# 构建SQL语句
sql = "SELECT * FROM domains WHERE 1=1"
params = []
if 'register_status' in conditions:
sql += " AND register_status = %s"
params.append(conditions['register_status'])
if 'use_status' in conditions:
sql += " AND use_status = %s"
params.append(conditions['use_status'])
if 'detect_status' in conditions:
sql += " AND detect_status = %s"
params.append(conditions['detect_status'])
if 'review_status' in conditions:
sql += " AND review_status = %s"
params.append(conditions['review_status'])
if 'has_beian' in conditions:
sql += " AND has_beian = %s"
params.append(conditions['has_beian'])
if conditions.get('company_type'):
sql += " AND company_type LIKE %s"
params.append(f"%{conditions['company_type']}%")
if conditions.get('beian_year'):
sql += " AND beian_year = %s"
params.append(conditions['beian_year'])
if conditions.get('snapshot_year'):
sql += " AND snapshot_years LIKE %s"
params.append(f"%{conditions['snapshot_year']}%")
# 域名搜索
if conditions.get('domain'):
sql += " AND domain LIKE %s"
params.append(f"%{conditions['domain']}%")
# 计算偏移量
offset = (page - 1) * page_size
sql += " LIMIT %s OFFSET %s"
params.extend([page_size, offset])
return self.fetch_all(sql, params)
def get_domains_count(self, conditions):
"""
获取符合条件的域名总数
:param conditions: 条件
:return: int - 域名总数
"""
# 构建SQL语句
sql = """
SELECT COUNT(DISTINCT d.id)
FROM domains d
LEFT JOIN domain_detections dd ON d.id = dd.domain_id
WHERE 1=1
"""
params = []
# 只添加非空条件
if 'register_status' in conditions and conditions['register_status'] is not None:
sql += " AND d.register_status = %s"
params.append(conditions['register_status'])
if 'use_status' in conditions and conditions['use_status'] is not None:
sql += " AND d.use_status = %s"
params.append(conditions['use_status'])
if 'detect_status' in conditions and conditions['detect_status'] is not None:
sql += " AND d.detect_status = %s"
params.append(conditions['detect_status'])
if 'review_status' in conditions and conditions['review_status'] is not None:
sql += " AND d.review_status = %s"
params.append(conditions['review_status'])
if 'has_beian' in conditions and conditions['has_beian'] is not None:
sql += " AND d.has_beian = %s"
params.append(conditions['has_beian'])
# 其他条件保持不变
if conditions.get('beian_year'):
sql += " AND d.beian_year = %s"
params.append(conditions['beian_year'])
if conditions.get('snapshot_year'):
sql += " AND d.snapshot_years LIKE %s"
params.append(f"%{conditions['snapshot_year']}%")
# 域名搜索
if conditions.get('search_keyword'):
sql += " AND d.domain LIKE %s"
params.append(f"%{conditions['search_keyword']}%")
# 首页网址搜索
if conditions.get('website_url'):
sql += " AND d.website_url LIKE %s"
params.append(f"%{conditions['website_url']}%")
if conditions.get('backlink_gt_10') is True:
sql += " AND COALESCE(dd.backlink_count_gt_10, FALSE) = TRUE"
# 打印查询信息
logger.info(f"执行计数SQL: {sql}")
logger.info(f"计数参数: {params}")
result = self.fetch_one(sql, params)
if result:
count = result.get('count', 0)
logger.info(f"符合条件的域名总数: {count}")
return count
return 0
def get_domains_with_details(self, conditions, page=1, page_size=1000):
"""
获取域名及其详细信息
:param conditions: 条件
:param page: 页码从1开始
:param page_size: 每页数量
:return: list - 域名列表
"""
# 构建SQL语句
sql = """
SELECT DISTINCT d.*, dd.baidu_site, dd.google_site, dd.qihu360_site, dd.baidu_history,
dd.chinaz_info, dd.aizhan_info, dd.juziseo_info, dd.jucha_info,
dd.is_chinese_title, dd.same_url, dd.backlink_count_gt_10
FROM domains d
LEFT JOIN domain_detections dd ON d.id = dd.domain_id
WHERE 1=1
"""
params = []
# 只添加非空条件
if 'register_status' in conditions and conditions['register_status'] is not None:
sql += " AND d.register_status = %s"
params.append(conditions['register_status'])
if 'use_status' in conditions and conditions['use_status'] is not None:
sql += " AND d.use_status = %s"
params.append(conditions['use_status'])
if 'detect_status' in conditions and conditions['detect_status'] is not None:
sql += " AND d.detect_status = %s"
params.append(conditions['detect_status'])
if 'review_status' in conditions and conditions['review_status'] is not None:
sql += " AND d.review_status = %s"
params.append(conditions['review_status'])
if 'has_beian' in conditions and conditions['has_beian'] is not None:
sql += " AND d.has_beian = %s"
params.append(conditions['has_beian'])
# 其他条件保持不变
if conditions.get('beian_year'):
sql += " AND d.beian_year = %s"
params.append(conditions['beian_year'])
if conditions.get('snapshot_year'):
sql += " AND d.snapshot_years LIKE %s"
params.append(f"%{conditions['snapshot_year']}%")
# 域名搜索
if conditions.get('search_keyword'):
sql += " AND d.domain LIKE %s"
params.append(f"%{conditions['search_keyword']}%")
# 首页网址搜索
if conditions.get('website_url'):
sql += " AND d.website_url LIKE %s"
params.append(f"%{conditions['website_url']}%")
if conditions.get('backlink_gt_10') is True:
sql += " AND COALESCE(dd.backlink_count_gt_10, FALSE) = TRUE"
sql += " ORDER BY d.id ASC"
# 计算偏移量
offset = (page - 1) * page_size
sql += " LIMIT %s OFFSET %s"
params.extend([page_size, offset])
# 打印查询信息
logger.info(f"执行查询SQL: {sql}")
logger.info(f"查询参数: {params}")
result = self.fetch_all(sql, params)
logger.info(f"查询结果数量: {len(result)}")
# 如果没有结果,尝试执行一个简单的查询来检查数据库是否有数据
if not result:
simple_sql = "SELECT COUNT(*) FROM domains"
count_result = self.fetch_one(simple_sql)
if count_result:
logger.info(f"数据库中总域名数量: {count_result.get('count', 0)}")
else:
logger.warning("无法获取数据库中域名数量")
return result
def get_domain_statistics(self):
"""
获取域名统计信息
:return: dict - 统计信息
"""
sql = """
SELECT
COUNT(*) AS total,
SUM(CASE WHEN register_status = %s THEN 1 ELSE 0 END) AS available,
SUM(CASE WHEN register_status = %s THEN 1 ELSE 0 END) AS registered,
SUM(CASE WHEN detect_status = %s THEN 1 ELSE 0 END) AS blacklisted
FROM domains
"""
result = self.fetch_one(sql, (REGISTER_STATUS_AVAILABLE, REGISTER_STATUS_REGISTERED, DETECT_STATUS_BLACKLISTED))
if result:
return {
'total': result.get('total', 0),
'available': result.get('available', 0),
'registered': result.get('registered', 0),
'blacklisted': result.get('blacklisted', 0)
}
return {}
def get_task_statistics(self):
"""
获取任务统计信息
:return: dict - 统计信息
"""
sql = """
SELECT
COUNT(*) AS total,
SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) AS pending,
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS running,
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) AS completed,
SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) AS failed
FROM detect_tasks
"""
result = self.fetch_one(sql)
if result:
return {
'total': result.get('total', 0),
'pending': result.get('pending', 0),
'running': result.get('running', 0),
'completed': result.get('completed', 0),
'failed': result.get('failed', 0)
}
return {}
def update_domain_status(self, domain_id, status_type, status_value):
"""
更新域名状态
:param domain_id: 域名ID
:param status_type: 状态类型
:param status_value: 状态值
:return: bool - 是否更新成功
"""
if status_type == 'use_status':
return self.update_domain_use_status(domain_id, status_value)
elif status_type == 'detect_status':
return self.update_domain_detect_status(domain_id, status_value)
elif status_type == 'register_status':
return self.update_domain_register_status(domain_id, status_value)
else:
logger.error(f"未知的状态类型: {status_type}")
return False
def is_ykj_domain(self, domain_id):
"""
检查域名是否为一口价域名
:param domain_id: 域名ID
:return: bool - 是否为一口价域名
"""
import threading
thread_id = threading.current_thread().ident
conn = None
cur = None
try:
conn, cur = self.connect(thread_id)
if not conn or not cur:
logger.warning(f"线程 {thread_id} 数据库连接失败,返回默认值")
return False
sql = "SELECT source_type FROM domains WHERE id = %s"
cur.execute(sql, (domain_id,))
try:
result = cur.fetchone()
if result:
# 1 表示聚名一口价
return result[0] == 1
return False
except Exception as e:
# 处理查询结果为空的情况
if "no results to fetch" in str(e):
return False
raise
except Exception as e:
logger.error(f"检查一口价域名出错: {e}")
return False
finally:
# 将连接放回连接池
self.close(conn, cur)