Files
GX-gp-notify/gx_gp_monitor/core/database.py
T
v6ole c548a8b5bd ```
feat(core): 添加大化县政府网采购公告数据表和相关功能

- 创建 dahuagov_announcements 表用于存储大化县政府网采购公告
- 添加相关索引以提高查询性能
- 实现 save_dahuagov_announcements、get_new_dahuagov_announcements
  和 mark_dahuagov_announcements_sent 方法
- 修改统计查询以包含大化县公告数据
- 更新内容哈希检查逻辑以支持新表

feat(cron): 集成大化县政府网采购公告爬取功能

- 导入大化县政府网爬虫模块
- 修改定时任务流程以同时爬取广西政府采购网和大化县政府网
- 对不同来源公告采用不同处理策略:
  - 广西政府采购网:关键词筛选后推送
  - 大化县政府网:全部推送,不过滤关键词
- 分别处理和统计两个来源的公告数据
- 实现独立的通知发送和状态更新机制

feat(notification): 优化企业微信通知显示大化县来源标识

- 为不同来源公告添加前缀标识(【大化县政府网】或【广西政府采购网】)
- 根据公告来源动态调整通知标题:
  - 单一来源显示具体来源
  - 双来源显示"双源监控"标识
- 改进通知卡片的来源区分度,便于用户识别公告来源
```
2026-05-09 10:55:00 +08:00

976 lines
32 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.
"""
PostgreSQL数据库连接和操作模块
提供数据库连接池、CRUD操作、数据清理等功能
"""
import psycopg2
from psycopg2 import pool, extras
from psycopg2.extras import RealDictCursor
from contextlib import contextmanager
from typing import List, Dict, Any, Optional, Generator
from datetime import datetime, timedelta
import threading
from dataclasses import asdict
from .models import Announcement, AnnouncementSource, AnnouncementType, CrawlResult, CrawlStatus
from .config_manager import get_config
from .logger import get_logger
from .reliability import retry_on_exception, RetryConfig
logger = get_logger(__name__)
class DatabaseConnectionPool:
"""数据库连接池管理器"""
_instance = None
_pool = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if self._pool is None:
self._pool = None
self._config = None
def init_pool(self, config):
"""
初始化连接池
Args:
config: 数据库配置
"""
if self._pool is not None:
return
try:
self._config = config
self._pool = psycopg2.pool.SimpleConnectionPool(
minconn=config.pool_size,
maxconn=config.pool_size + config.max_overflow,
host=config.host,
port=config.port,
database=config.name,
user=config.user,
password=config.password,
connect_timeout=config.pool_timeout
)
logger.info("数据库连接池初始化成功")
except Exception as e:
logger.error(f"数据库连接池初始化失败: {str(e)}")
raise
def get_connection(self):
"""
获取数据库连接
Returns:
数据库连接对象
Raises:
Exception: 获取连接失败
"""
if self._pool is None:
raise Exception("数据库连接池未初始化")
try:
conn = self._pool.getconn()
# 设置自动提交为False,需要手动提交
conn.autocommit = False
return conn
except Exception as e:
logger.error(f"获取数据库连接失败: {str(e)}")
raise
def return_connection(self, conn):
"""
返回数据库连接到连接池
Args:
conn: 数据库连接对象
"""
if self._pool and conn:
try:
self._pool.putconn(conn)
except Exception as e:
logger.warning(f"返回数据库连接失败: {str(e)}")
def close_all(self):
"""关闭所有连接"""
if self._pool:
try:
self._pool.closeall()
logger.info("数据库连接池已关闭")
except Exception as e:
logger.error(f"关闭数据库连接池失败: {str(e)}")
# 全局连接池实例
_connection_pool = DatabaseConnectionPool()
@contextmanager
def get_db_connection():
"""
获取数据库连接的上下文管理器
Yields:
数据库连接对象
"""
conn = None
try:
conn = _connection_pool.get_connection()
yield conn
except Exception as e:
logger.error(f"数据库连接错误: {str(e)}")
raise
finally:
if conn:
_connection_pool.return_connection(conn)
@contextmanager
def get_db_cursor(commit: bool = True):
"""
获取数据库游标的上下文管理器
Args:
commit: 是否自动提交事务
Yields:
数据库游标对象
"""
with get_db_connection() as conn:
cursor = None
try:
cursor = conn.cursor(cursor_factory=RealDictCursor)
yield cursor
if commit:
conn.commit()
except Exception as e:
conn.rollback()
logger.error(f"数据库操作错误: {str(e)}")
raise
finally:
if cursor:
cursor.close()
class DatabaseManager:
"""数据库管理器"""
def __init__(self):
self.config = get_config()
if self.config.database.enabled:
_connection_pool.init_pool(self.config.database)
else:
logger.warning("数据库功能已禁用")
def init_database(self):
"""初始化数据库表结构"""
if not self.config.database.enabled:
return
logger.info("开始初始化数据库表结构")
# 创建表的SQL语句
create_tables_sql = """
-- 定时搜索公告表(关键词匹配专用)
CREATE TABLE IF NOT EXISTS auto_announcements (
id SERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
publish_date TIMESTAMP NOT NULL,
purchase_name VARCHAR(200),
content_url TEXT,
source_code VARCHAR(50) NOT NULL,
source_name VARCHAR(100) NOT NULL,
announcement_type VARCHAR(50) NOT NULL,
crawled_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
content_hash VARCHAR(32) UNIQUE,
keyword_matched BOOLEAN DEFAULT FALSE,
date_filtered BOOLEAN DEFAULT TRUE,
is_new BOOLEAN DEFAULT TRUE,
is_today BOOLEAN DEFAULT FALSE
);
-- 手动搜索公告表(全量数据专用)
CREATE TABLE IF NOT EXISTS manual_announcements (
id SERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
publish_date TIMESTAMP NOT NULL,
purchase_name VARCHAR(200),
content_url TEXT,
source_code VARCHAR(50) NOT NULL,
source_name VARCHAR(100) NOT NULL,
announcement_type VARCHAR(50) NOT NULL,
crawled_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
content_hash VARCHAR(32),
keyword_matched BOOLEAN DEFAULT FALSE,
date_filtered BOOLEAN DEFAULT TRUE,
is_new BOOLEAN DEFAULT TRUE,
is_today BOOLEAN DEFAULT FALSE
);
-- 原公告表(保留兼容性)
CREATE TABLE IF NOT EXISTS announcements (
id SERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
publish_date TIMESTAMP NOT NULL,
purchase_name VARCHAR(200),
content_url TEXT,
source_code VARCHAR(50) NOT NULL,
source_name VARCHAR(100) NOT NULL,
announcement_type VARCHAR(50) NOT NULL,
crawled_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
crawl_mode VARCHAR(20) DEFAULT 'auto',
content_hash VARCHAR(32) UNIQUE,
keyword_matched BOOLEAN DEFAULT FALSE,
date_filtered BOOLEAN DEFAULT TRUE,
is_new BOOLEAN DEFAULT TRUE,
is_today BOOLEAN DEFAULT FALSE
);
-- 公告来源表
CREATE TABLE IF NOT EXISTS announcement_sources (
code VARCHAR(50) PRIMARY KEY,
category_id INTEGER NOT NULL,
name VARCHAR(100) NOT NULL,
type VARCHAR(50) NOT NULL
);
-- 爬取结果表
CREATE TABLE IF NOT EXISTS crawl_results (
id SERIAL PRIMARY KEY,
source_code VARCHAR(50) NOT NULL,
status VARCHAR(20) NOT NULL,
total_count INTEGER DEFAULT 0,
new_count INTEGER DEFAULT 0,
error_message TEXT,
crawled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
duration FLOAT DEFAULT 0.0,
FOREIGN KEY (source_code) REFERENCES announcement_sources(code)
);
-- 大化县政府网采购公告表(全部推送,不筛选)
CREATE TABLE IF NOT EXISTS dahuagov_announcements (
id SERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
publish_date TIMESTAMP NOT NULL,
purchase_name VARCHAR(200),
content_url TEXT,
source_code VARCHAR(50) NOT NULL DEFAULT 'dahuagov',
source_name VARCHAR(100) NOT NULL DEFAULT '大化县政府网采购公告',
announcement_type VARCHAR(50) NOT NULL DEFAULT 'purchase',
crawled_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
content_hash VARCHAR(32) UNIQUE,
is_new BOOLEAN DEFAULT TRUE
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_announcements_publish_date ON announcements(publish_date DESC);
CREATE INDEX IF NOT EXISTS idx_announcements_source_code ON announcements(source_code);
CREATE INDEX IF NOT EXISTS idx_announcements_content_hash ON announcements(content_hash);
CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_crawl_results_crawled_at ON crawl_results(crawled_at DESC);
CREATE INDEX IF NOT EXISTS idx_dahuagov_publish_date ON dahuagov_announcements(publish_date DESC);
CREATE INDEX IF NOT EXISTS idx_dahuagov_content_hash ON dahuagov_announcements(content_hash);
CREATE INDEX IF NOT EXISTS idx_dahuagov_created_at ON dahuagov_announcements(created_at DESC);
-- 创建更新时间触发器
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
DROP TRIGGER IF EXISTS update_announcements_updated_at ON announcements;
CREATE TRIGGER update_announcements_updated_at
BEFORE UPDATE ON announcements
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
"""
with get_db_cursor() as cursor:
cursor.execute(create_tables_sql)
logger.info("数据库表结构初始化完成")
@retry_on_exception(RetryConfig(max_retries=3))
def save_announcement(self, announcement: Announcement) -> bool:
"""
保存公告到数据库
Args:
announcement: 公告对象
Returns:
bool: 保存是否成功
"""
if not self.config.database.enabled:
return False
# 生成内容哈希(如果还没有)
if not announcement.content_hash:
announcement.generate_content_hash()
sql = """
INSERT INTO announcements (
title, publish_date, purchase_name, content_url, source_code, source_name,
announcement_type, crawled_at, content_hash, keyword_matched,
date_filtered, is_new, is_today
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (content_hash) DO NOTHING
"""
values = (
announcement.title,
announcement.publish_date,
announcement.purchase_name,
announcement.content_url,
announcement.source_code,
announcement.source_name,
announcement.announcement_type.value,
announcement.crawled_at,
announcement.content_hash,
announcement.keyword_matched,
announcement.date_filtered,
announcement.is_new,
announcement.is_today
)
try:
with get_db_cursor() as cursor:
cursor.execute(sql, values)
affected_rows = cursor.rowcount
if affected_rows > 0:
logger.debug(f"成功保存公告: {announcement.title[:50]}...")
return True
else:
logger.debug(f"公告已存在,跳过保存: {announcement.title[:50]}...")
return False
except Exception as e:
logger.error(f"保存公告失败: {str(e)}")
return False
@retry_on_exception(RetryConfig(max_retries=3))
def save_announcements_batch(self, announcements: List[Announcement]) -> int:
"""
批量保存公告
Args:
announcements: 公告列表
Returns:
int: 成功保存的数量
"""
if not self.config.database.enabled:
return 0
if not announcements:
return 0
# 为没有哈希的公告生成哈希
for announcement in announcements:
if not announcement.content_hash:
announcement.generate_content_hash()
sql = """
INSERT INTO announcements (
title, publish_date, purchase_name, content_url, source_code, source_name,
announcement_type, crawled_at, crawl_mode, content_hash, keyword_matched,
date_filtered, is_new, is_today
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (content_hash) DO NOTHING
"""
values = []
for announcement in announcements:
values.append((
announcement.title,
announcement.publish_date,
announcement.purchase_name,
announcement.content_url,
announcement.source_code,
announcement.source_name,
announcement.announcement_type.value,
announcement.crawled_at,
announcement.crawl_mode,
announcement.content_hash,
announcement.keyword_matched,
announcement.date_filtered,
announcement.is_new,
announcement.is_today
))
try:
with get_db_cursor() as cursor:
extras.execute_batch(cursor, sql, values)
affected_rows = cursor.rowcount
logger.info(f"批量保存公告完成,成功保存 {affected_rows} 条")
return affected_rows
except Exception as e:
logger.error(f"批量保存公告失败: {str(e)}")
return 0
def save_announcements_batch_to_table(self, announcements: List[Announcement], table_name: str) -> int:
"""
批量保存公告到指定表
Args:
announcements: 公告列表
table_name: 目标表名 ("auto_announcements" 或 "manual_announcements")
Returns:
int: 成功保存的数量
"""
if not self.config.database.enabled:
return 0
if not announcements:
return 0
# 为没有哈希的公告生成哈希
for announcement in announcements:
if not announcement.content_hash:
announcement.generate_content_hash()
# 根据表名决定是否使用ON CONFLICT
if table_name == "manual_announcements":
# 手动搜索表不使用唯一约束(允许重复)
sql = f"""
INSERT INTO {table_name} (
title, publish_date, purchase_name, content_url, source_code, source_name,
announcement_type, crawled_at, content_hash, keyword_matched,
date_filtered, is_new, is_today
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
values = []
for announcement in announcements:
values.append((
announcement.title,
announcement.publish_date,
announcement.purchase_name,
announcement.content_url,
announcement.source_code,
announcement.source_name,
announcement.announcement_type.value,
announcement.crawled_at,
announcement.content_hash,
announcement.keyword_matched,
announcement.date_filtered,
announcement.is_new,
announcement.is_today
))
else:
# 自动搜索表使用唯一约束
sql = f"""
INSERT INTO {table_name} (
title, publish_date, purchase_name, content_url, source_code, source_name,
announcement_type, crawled_at, content_hash, keyword_matched,
date_filtered, is_new, is_today
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (content_hash) DO NOTHING
"""
values = []
for announcement in announcements:
values.append((
announcement.title,
announcement.publish_date,
announcement.purchase_name,
announcement.content_url,
announcement.source_code,
announcement.source_name,
announcement.announcement_type.value,
announcement.crawled_at,
announcement.content_hash,
announcement.keyword_matched,
announcement.date_filtered,
announcement.is_new,
announcement.is_today
))
try:
with get_db_cursor() as cursor:
extras.execute_batch(cursor, sql, values)
affected_rows = cursor.rowcount
logger.info(f"批量保存公告到{table_name}完成,影响行数: {affected_rows}")
return affected_rows
except Exception as e:
logger.error(f"批量保存公告到{table_name}失败: {str(e)}")
return 0
@retry_on_exception(RetryConfig(max_retries=3))
def get_announcements(self,
source_code: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
limit: int = 100,
offset: int = 0) -> List[Announcement]:
"""
查询公告
Args:
source_code: 来源代码过滤
start_date: 开始日期
end_date: 结束日期
limit: 限制数量
offset: 偏移量
Returns:
List[Announcement]: 公告列表
"""
if not self.config.database.enabled:
return []
sql = """
SELECT * FROM announcements
WHERE 1=1
"""
params = []
if source_code:
sql += " AND source_code = %s"
params.append(source_code)
if start_date:
sql += " AND publish_date >= %s"
params.append(start_date)
if end_date:
sql += " AND publish_date <= %s"
params.append(end_date)
sql += " ORDER BY publish_date DESC LIMIT %s OFFSET %s"
params.extend([limit, offset])
try:
with get_db_cursor() as cursor:
cursor.execute(sql, params)
rows = cursor.fetchall()
announcements = []
for row in rows:
# 转换数据类型
row_dict = dict(row)
row_dict['announcement_type'] = AnnouncementType(row_dict['announcement_type'])
announcements.append(Announcement.from_dict(row_dict))
return announcements
except Exception as e:
logger.error(f"查询公告失败: {str(e)}")
return []
@retry_on_exception(RetryConfig(max_retries=3))
def save_crawl_result(self, result: CrawlResult) -> bool:
"""
保存爬取结果
Args:
result: 爬取结果对象
Returns:
bool: 保存是否成功
"""
if not self.config.database.enabled:
return False
sql = """
INSERT INTO crawl_results (
source_code, status, total_count, new_count, error_message,
crawled_at, duration
) VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
values = (
result.source.code,
result.status.value,
result.total_count,
result.new_count,
result.error_message,
result.crawled_at,
result.duration
)
try:
with get_db_cursor() as cursor:
cursor.execute(sql, values)
logger.debug(f"保存爬取结果: {result.source.name}")
return True
except Exception as e:
logger.error(f"保存爬取结果失败: {str(e)}")
return False
@retry_on_exception(RetryConfig(max_retries=3))
def cleanup_expired_data(self, days: int = 90) -> int:
"""
清理过期数据
Args:
days: 保留天数
Returns:
int: 清理的记录数
"""
if not self.config.database.enabled:
return 0
cutoff_date = datetime.now() - timedelta(days=days)
sql = "DELETE FROM announcements WHERE created_at < %s"
try:
with get_db_cursor() as cursor:
cursor.execute(sql, (cutoff_date,))
deleted_count = cursor.rowcount
logger.info(f"清理过期数据完成,删除 {deleted_count} 条记录")
return deleted_count
except Exception as e:
logger.error(f"清理过期数据失败: {str(e)}")
return 0
@retry_on_exception(RetryConfig(max_retries=3))
def get_statistics(self) -> Dict[str, Any]:
"""
获取统计信息
Returns:
Dict[str, Any]: 统计数据
"""
if not self.config.database.enabled:
return {}
# 统计所有表的综合信息
sql = """
SELECT
SUM(total_count) as total_announcements,
SUM(today_count) as today_announcements,
SUM(new_count) as new_announcements,
COUNT(DISTINCT source_code) as sources_count,
MAX(last_crawl_time) as last_crawl_time
FROM (
SELECT
COUNT(*) as total_count,
COUNT(CASE WHEN is_today THEN 1 END) as today_count,
COUNT(CASE WHEN is_new THEN 1 END) as new_count,
source_code,
MAX(crawled_at) as last_crawl_time
FROM announcements
GROUP BY source_code
UNION ALL
SELECT
COUNT(*) as total_count,
COUNT(CASE WHEN is_today THEN 1 END) as today_count,
COUNT(CASE WHEN is_new THEN 1 END) as new_count,
source_code,
MAX(crawled_at) as last_crawl_time
FROM auto_announcements
GROUP BY source_code
UNION ALL
SELECT
COUNT(*) as total_count,
COUNT(CASE WHEN is_today THEN 1 END) as today_count,
COUNT(CASE WHEN is_new THEN 1 END) as new_count,
source_code,
MAX(crawled_at) as last_crawl_time
FROM manual_announcements
GROUP BY source_code
) as combined_stats
"""
try:
with get_db_cursor() as cursor:
cursor.execute(sql)
result = cursor.fetchone()
stats = dict(result) if result else {}
# 添加各表详细统计
detail_sql = """
SELECT
'announcements' as table_name,
COUNT(*) as count,
COUNT(DISTINCT source_code) as sources,
MAX(crawled_at) as last_crawl
FROM announcements
UNION ALL
SELECT
'auto_announcements' as table_name,
COUNT(*) as count,
COUNT(DISTINCT source_code) as sources,
MAX(crawled_at) as last_crawl
FROM auto_announcements
UNION ALL
SELECT
'manual_announcements' as table_name,
COUNT(*) as count,
COUNT(DISTINCT source_code) as sources,
MAX(crawled_at) as last_crawl
FROM manual_announcements
UNION ALL
SELECT
'dahuagov_announcements' as table_name,
COUNT(*) as count,
COUNT(DISTINCT source_code) as sources,
MAX(crawled_at) as last_crawl
FROM dahuagov_announcements
"""
cursor.execute(detail_sql)
detail_results = cursor.fetchall()
stats['table_details'] = {row['table_name']: {
'count': row['count'],
'sources': row['sources'],
'last_crawl': row['last_crawl']
} for row in detail_results}
return stats
except Exception as e:
logger.error(f"获取统计信息失败: {str(e)}")
return {}
def is_announcement_exists(self, content_hash: str) -> bool:
"""
检查公告是否已存在
Args:
content_hash: 内容哈希
Returns:
bool: 是否存在
"""
if not self.config.database.enabled:
return False
# 检查所有表中是否存在
sql = """
SELECT 1 FROM (
SELECT content_hash FROM announcements WHERE content_hash = %s
UNION ALL
SELECT content_hash FROM auto_announcements WHERE content_hash = %s
UNION ALL
SELECT content_hash FROM manual_announcements WHERE content_hash = %s
UNION ALL
SELECT content_hash FROM dahuagov_announcements WHERE content_hash = %s
) as combined_check LIMIT 1
"""
try:
with get_db_cursor() as cursor:
cursor.execute(sql, (content_hash, content_hash, content_hash, content_hash))
return cursor.fetchone() is not None
except Exception as e:
logger.error(f"检查公告存在性失败: {str(e)}")
return False
def get_recent_announcements(self, hours: int = 24) -> List[Announcement]:
"""
获取最近的公告
Args:
hours: 最近小时数
Returns:
List[Announcement]: 公告列表
"""
if not self.config.database.enabled:
return []
cutoff_time = datetime.now() - timedelta(hours=hours)
sql = """
SELECT * FROM announcements
WHERE crawled_at >= %s
ORDER BY crawled_at DESC
"""
try:
with get_db_cursor() as cursor:
cursor.execute(sql, (cutoff_time,))
rows = cursor.fetchall()
announcements = []
for row in rows:
row_dict = dict(row)
row_dict['announcement_type'] = AnnouncementType(row_dict['announcement_type'])
announcements.append(Announcement.from_dict(row_dict))
return announcements
except Exception as e:
logger.error(f"获取最近公告失败: {str(e)}")
return []
@retry_on_exception(RetryConfig(max_retries=3))
def save_dahuagov_announcements(self, announcements: List[Announcement]) -> int:
"""
保存大化县政府网公告(全部推送,不筛选关键词)
Args:
announcements: 公告列表
Returns:
int: 成功保存的新公告数量
"""
if not self.config.database.enabled:
return 0
if not announcements:
return 0
# 为没有哈希的公告生成哈希
for announcement in announcements:
if not announcement.content_hash:
announcement.generate_content_hash()
sql = """
INSERT INTO dahuagov_announcements (
title, publish_date, purchase_name, content_url, source_code, source_name,
announcement_type, crawled_at, content_hash, is_new
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (content_hash) DO NOTHING
"""
values = []
for announcement in announcements:
values.append((
announcement.title,
announcement.publish_date,
announcement.purchase_name,
announcement.content_url,
announcement.source_code,
announcement.source_name,
announcement.announcement_type.value,
announcement.crawled_at,
announcement.content_hash,
announcement.is_new
))
try:
with get_db_cursor() as cursor:
extras.execute_batch(cursor, sql, values)
affected_rows = cursor.rowcount
logger.info(f"保存大化县公告完成,新增 {affected_rows} 条")
return affected_rows
except Exception as e:
logger.error(f"保存大化县公告失败: {str(e)}")
return 0
@retry_on_exception(RetryConfig(max_retries=3))
def get_new_dahuagov_announcements(self) -> List[Announcement]:
"""
获取大化县未推送的新公告(is_new = TRUE
Returns:
List[Announcement]: 未推送的公告列表
"""
if not self.config.database.enabled:
return []
sql = """
SELECT * FROM dahuagov_announcements
WHERE is_new = TRUE
ORDER BY publish_date DESC
"""
try:
with get_db_cursor() as cursor:
cursor.execute(sql)
rows = cursor.fetchall()
announcements = []
for row in rows:
row_dict = dict(row)
row_dict['announcement_type'] = AnnouncementType(row_dict['announcement_type'])
announcements.append(Announcement.from_dict(row_dict))
return announcements
except Exception as e:
logger.error(f"获取大化县新公告失败: {str(e)}")
return []
@retry_on_exception(RetryConfig(max_retries=3))
def mark_dahuagov_announcements_sent(self, announcements: List[Announcement]) -> int:
"""
标记大化县公告已发送(is_new = FALSE
Args:
announcements: 已发送的公告列表
Returns:
int: 更新的记录数
"""
if not self.config.database.enabled:
return 0
if not announcements:
return 0
# 获取所有公告的哈希值
hashes = [ann.content_hash for ann in announcements if ann.content_hash]
if not hashes:
return 0
sql = """
UPDATE dahuagov_announcements
SET is_new = FALSE, updated_at = CURRENT_TIMESTAMP
WHERE content_hash = ANY(%s)
"""
try:
with get_db_cursor() as cursor:
cursor.execute(sql, (hashes,))
affected_rows = cursor.rowcount
logger.info(f"标记大化县公告已发送完成,更新 {affected_rows} 条")
return affected_rows
except Exception as e:
logger.error(f"标记大化县公告已发送失败: {str(e)}")
return 0
# 全局数据库管理器实例
_db_manager = None
def get_database_manager() -> DatabaseManager:
"""
获取数据库管理器实例
Returns:
DatabaseManager: 数据库管理器实例
"""
global _db_manager
if _db_manager is None:
_db_manager = DatabaseManager()
return _db_manager
def init_database():
"""初始化数据库"""
manager = get_database_manager()
manager.init_database()
def cleanup_database():
"""清理数据库连接"""
_connection_pool.close_all()