删除无用文件
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -25,7 +25,7 @@ crawler:
|
||||
proxies: [] # 代理列表
|
||||
request_delay: 1.0 # 请求间延迟
|
||||
request_delay_max: 3.0 # 请求间最大延迟
|
||||
keyword: ["大化", "信息化"] # 关键词筛选(支持多个关键词)
|
||||
keyword: ["大化"] # 关键词筛选(支持多个关键词)
|
||||
start_date: "" # 开始日期 (YYYY-MM-DD)
|
||||
end_date: "" # 结束日期 (YYYY-MM-DD)
|
||||
max_pages: 10 # 最大页数
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+218
-11
@@ -182,7 +182,47 @@ class DatabaseManager:
|
||||
|
||||
# 创建表的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,
|
||||
@@ -195,6 +235,7 @@ class DatabaseManager:
|
||||
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,
|
||||
@@ -331,9 +372,9 @@ class DatabaseManager:
|
||||
sql = """
|
||||
INSERT INTO announcements (
|
||||
title, publish_date, purchase_name, content_url, source_code, source_name,
|
||||
announcement_type, crawled_at, content_hash, keyword_matched,
|
||||
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)
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (content_hash) DO NOTHING
|
||||
"""
|
||||
|
||||
@@ -348,6 +389,7 @@ class DatabaseManager:
|
||||
announcement.source_name,
|
||||
announcement.announcement_type.value,
|
||||
announcement.crawled_at,
|
||||
announcement.crawl_mode,
|
||||
announcement.content_hash,
|
||||
announcement.keyword_matched,
|
||||
announcement.date_filtered,
|
||||
@@ -365,6 +407,94 @@ class DatabaseManager:
|
||||
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,
|
||||
@@ -504,21 +634,89 @@ class DatabaseManager:
|
||||
if not self.config.database.enabled:
|
||||
return {}
|
||||
|
||||
# 统计所有表的综合信息
|
||||
sql = """
|
||||
SELECT
|
||||
COUNT(*) as total_announcements,
|
||||
COUNT(CASE WHEN is_today THEN 1 END) as today_announcements,
|
||||
COUNT(CASE WHEN is_new THEN 1 END) as new_announcements,
|
||||
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(crawled_at) as last_crawl_time
|
||||
FROM announcements
|
||||
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()
|
||||
return dict(result) if result else {}
|
||||
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
|
||||
"""
|
||||
|
||||
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 {}
|
||||
@@ -536,11 +734,20 @@ class DatabaseManager:
|
||||
if not self.config.database.enabled:
|
||||
return False
|
||||
|
||||
sql = "SELECT 1 FROM announcements WHERE content_hash = %s LIMIT 1"
|
||||
# 检查所有表中是否存在
|
||||
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
|
||||
) as combined_check LIMIT 1
|
||||
"""
|
||||
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(sql, (content_hash,))
|
||||
cursor.execute(sql, (content_hash, content_hash, content_hash))
|
||||
return cursor.fetchone() is not None
|
||||
except Exception as e:
|
||||
logger.error(f"检查公告存在性失败: {str(e)}")
|
||||
|
||||
@@ -60,6 +60,7 @@ class Announcement:
|
||||
crawled_at: Optional[datetime] = None # 爬取时间
|
||||
created_at: Optional[datetime] = None # 创建时间
|
||||
updated_at: Optional[datetime] = None # 更新时间
|
||||
crawl_mode: str = "auto" # 爬取模式:auto(自动)/manual(手动)
|
||||
|
||||
# 去重字段
|
||||
content_hash: Optional[str] = None # 内容哈希,用于去重
|
||||
|
||||
+44
-23
@@ -17,7 +17,7 @@ try:
|
||||
from gx_gp_monitor.core.logger import init_logger, get_logger
|
||||
from gx_gp_monitor.crawler.spider import crawl_announcements
|
||||
from gx_gp_monitor.filters.filters import filter_from_config
|
||||
from gx_gp_monitor.storage.postgresql import init_storage, save_announcements_to_storage, save_all_announcements_by_source_to_storage
|
||||
from gx_gp_monitor.storage.postgresql import init_storage, save_announcements_to_storage, save_all_announcements_by_source_to_storage, save_auto_announcements_to_storage
|
||||
from gx_gp_monitor.notification.wechat import send_announcements_notification, send_system_notification
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -39,8 +39,8 @@ try:
|
||||
# 初始化存储
|
||||
init_storage()
|
||||
|
||||
# 执行搜索
|
||||
logger.info("开始执行搜索...")
|
||||
# 执行搜索(爬取所有公告,然后进行关键词筛选)
|
||||
logger.info("开始执行定时搜索任务")
|
||||
crawl_results = crawl_announcements()
|
||||
|
||||
if not crawl_results:
|
||||
@@ -56,22 +56,42 @@ try:
|
||||
total_crawled = len(all_announcements)
|
||||
logger.info(f"搜索到 {total_crawled} 条原始公告")
|
||||
|
||||
# 保存所有公告(按来源分组,每源保留最新100条)
|
||||
all_saved_stats = save_all_announcements_by_source_to_storage(all_announcements, max_per_source=100)
|
||||
all_saved_count = sum(all_saved_stats.values())
|
||||
logger.info(f"保存所有公告完成:共保存 {all_saved_count} 条,按来源统计: {all_saved_stats}")
|
||||
|
||||
# 筛选出新公告(数据库中没有的)
|
||||
new_announcements = [ann for ann in all_announcements if ann.is_new]
|
||||
logger.info(f"筛选出 {len(new_announcements)} 条新公告")
|
||||
|
||||
if not new_announcements:
|
||||
logger.info("没有新的公告,任务完成")
|
||||
if not all_announcements:
|
||||
logger.info("没有获取到任何公告")
|
||||
return True
|
||||
|
||||
# 对新公告应用关键词筛选
|
||||
filter_obj = filter_from_config()
|
||||
filtered_announcements, filter_stats = filter_obj.filter(new_announcements)
|
||||
# 对所有公告进行关键词筛选
|
||||
from gx_gp_monitor.filters.filters import KeywordFilter, DateFilter
|
||||
from datetime import date
|
||||
|
||||
# 1. 关键词筛选
|
||||
keyword_filter = KeywordFilter()
|
||||
keyword_filtered = keyword_filter.filter_announcements(all_announcements, keywords=config.crawler.keyword)
|
||||
|
||||
# 2. 日期筛选(只保留今天的)
|
||||
date_filter = DateFilter()
|
||||
today_announcements = date_filter.filter_announcements(
|
||||
keyword_filtered,
|
||||
start_date=date.today(),
|
||||
end_date=date.today()
|
||||
)
|
||||
|
||||
logger.info(f"关键词筛选后剩余 {len(keyword_filtered)} 条公告")
|
||||
logger.info(f"筛选出今天 {len(today_announcements)} 条匹配公告")
|
||||
|
||||
if not today_announcements:
|
||||
logger.info("今天没有匹配关键词的公告")
|
||||
return True
|
||||
|
||||
# 构造筛选统计信息
|
||||
filter_stats = type('FilterResult', (), {
|
||||
"keyword_filtered": len(all_announcements) - len(keyword_filtered),
|
||||
"date_filtered": len(keyword_filtered) - len(today_announcements),
|
||||
"duplicate_filtered": 0,
|
||||
"source_filtered": 0
|
||||
})()
|
||||
|
||||
filtered_announcements = today_announcements
|
||||
|
||||
logger.info(f"关键词筛选后剩余 {len(filtered_announcements)} 条公告")
|
||||
|
||||
@@ -79,9 +99,9 @@ try:
|
||||
logger.info("没有匹配关键词的公告,任务完成")
|
||||
return True
|
||||
|
||||
# 保存筛选后的公告(用于标记关键词匹配等)
|
||||
saved_count = save_announcements_to_storage(filtered_announcements)
|
||||
logger.info(f"保存筛选后公告完成:{saved_count} 条")
|
||||
# 保存筛选后的公告到定时搜索专用表
|
||||
saved_count = save_auto_announcements_to_storage(filtered_announcements)
|
||||
logger.info(f"保存定时搜索公告完成:{saved_count} 条")
|
||||
|
||||
# 发送企业微信卡片通知
|
||||
if config.wechat_app.enabled:
|
||||
@@ -96,10 +116,11 @@ try:
|
||||
logger.info("企业微信通知未启用,跳过发送")
|
||||
|
||||
# 输出统计信息
|
||||
print("\n=== 定时爬取任务完成 ===")
|
||||
print("\n=== 定时搜索任务完成 ===")
|
||||
print(f"总共爬取: {total_crawled} 条公告")
|
||||
print(f"新增公告: {len(new_announcements)} 条")
|
||||
print(f"关键词筛选: {len(filtered_announcements)} 条")
|
||||
print(f"关键词筛选: {len(keyword_filtered)} 条")
|
||||
print(f"今日匹配公告: {len(today_announcements)} 条")
|
||||
print(f"筛选后公告: {len(filtered_announcements)} 条")
|
||||
print(f"保存到数据库: {saved_count} 条")
|
||||
print(f"企业微信通知: {'成功' if notify_success else '失败' if config.wechat_app.enabled else '未启用'}")
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ try:
|
||||
from .core.reliability import check_system_health
|
||||
from .crawler.spider import crawl_announcements
|
||||
from .filters.filters import filter_from_config
|
||||
from .storage.postgresql import init_storage, save_announcements_to_storage, save_all_announcements_by_source_to_storage, cleanup_storage
|
||||
from .storage.postgresql import init_storage, save_announcements_to_storage, save_all_announcements_by_source_to_storage, save_manual_announcements_by_source_to_storage, cleanup_storage
|
||||
from .storage.md_generator import generate_onu_md
|
||||
from .notification.wechat import send_announcements_notification, send_system_notification
|
||||
from .wechat.callback_server import get_callback_server
|
||||
@@ -116,16 +116,17 @@ class GXGPMonitorApp:
|
||||
|
||||
logger.info(f"搜索到 {len(all_announcements)} 条原始公告")
|
||||
|
||||
# 对于手动搜索,不保存公告到数据库,只进行筛选和返回结果
|
||||
# 保存公告到对应的专用表
|
||||
if not manual_crawl:
|
||||
# 先保存所有公告(按来源分组,每源保留最新100条)
|
||||
# 自动爬取:保存到auto_announcements表(关键词匹配专用)
|
||||
all_saved_stats = save_all_announcements_by_source_to_storage(all_announcements, max_per_source=100)
|
||||
all_saved_count = sum(all_saved_stats.values())
|
||||
logger.info(f"保存所有公告完成:共保存 {all_saved_count} 条,按来源统计: {all_saved_stats}")
|
||||
logger.info(f"保存自动爬取公告完成:共保存 {all_saved_count} 条,按来源统计: {all_saved_stats}")
|
||||
else:
|
||||
all_saved_count = 0
|
||||
all_saved_stats = {}
|
||||
logger.info("手动搜索模式:跳过数据库保存")
|
||||
# 手动搜索:保存到manual_announcements表(全量数据专用)
|
||||
all_saved_stats = save_manual_announcements_by_source_to_storage(all_announcements, max_per_source=100)
|
||||
all_saved_count = sum(all_saved_stats.values())
|
||||
logger.info(f"保存手动搜索公告完成:共保存 {all_saved_count} 条,按来源统计: {all_saved_stats}")
|
||||
|
||||
# 筛选公告
|
||||
if manual_crawl:
|
||||
|
||||
@@ -1,555 +0,0 @@
|
||||
"""
|
||||
企业微信通知模块
|
||||
提供企业微信消息发送功能,支持文本和Markdown格式
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from ..core.config_manager import get_config
|
||||
from ..core.logger import get_logger
|
||||
from ..core.models import Announcement
|
||||
from ..core.reliability import retry_on_exception, RetryConfig, safe_execute
|
||||
from ..storage.md_generator import AnnouncementMarkdownFormatter
|
||||
except ImportError:
|
||||
from core.config_manager import get_config
|
||||
from core.logger import get_logger
|
||||
from core.models import Announcement
|
||||
from core.reliability import retry_on_exception, RetryConfig, safe_execute
|
||||
from storage.md_generator import AnnouncementMarkdownFormatter
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class WeChatService:
|
||||
"""企业微信服务"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = get_config().wechat_app
|
||||
self._access_token = None
|
||||
self._token_expires_at = 0
|
||||
|
||||
logger.info("企业微信服务初始化完成")
|
||||
|
||||
def _get_access_token(self) -> Optional[str]:
|
||||
"""
|
||||
获取访问令牌
|
||||
|
||||
Returns:
|
||||
Optional[str]: 访问令牌
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
# 检查令牌是否仍然有效
|
||||
if self._access_token and current_time < self._token_expires_at:
|
||||
return self._access_token
|
||||
|
||||
try:
|
||||
# 构建请求URL
|
||||
if self.config.use_proxy and hasattr(self.config, 'proxy_api_url'):
|
||||
url = f"{self.config.proxy_api_url}/cgi-bin/gettoken"
|
||||
else:
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
||||
|
||||
params = {
|
||||
"corpid": self.config.corp_id,
|
||||
"corpsecret": self.config.secret
|
||||
}
|
||||
|
||||
logger.debug("正在获取企业微信访问令牌")
|
||||
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
self._access_token = result.get("access_token")
|
||||
# 提前5分钟过期
|
||||
expires_in = result.get("expires_in", 7200) - 300
|
||||
self._token_expires_at = current_time + expires_in
|
||||
|
||||
logger.info("成功获取企业微信访问令牌")
|
||||
return self._access_token
|
||||
else:
|
||||
logger.error(f"获取访问令牌失败: {result}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取访问令牌异常: {str(e)}")
|
||||
return None
|
||||
|
||||
@retry_on_exception(RetryConfig(max_retries=3))
|
||||
def send_text_message(self, content: str,
|
||||
to_user: str = "@all",
|
||||
to_party: str = "",
|
||||
to_tag: str = "") -> bool:
|
||||
"""
|
||||
发送文本消息
|
||||
|
||||
Args:
|
||||
content: 消息内容
|
||||
to_user: 接收者用户ID,多个用|分隔,@all表示全体
|
||||
to_party: 接收者部门ID,多个用|分隔
|
||||
to_tag: 接收者标签ID,多个用|分隔
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
try:
|
||||
access_token = self._get_access_token()
|
||||
if not access_token:
|
||||
logger.error("无法获取访问令牌,发送失败")
|
||||
return False
|
||||
|
||||
# 构建请求URL
|
||||
if self.config.use_proxy and hasattr(self.config, 'proxy_api_url'):
|
||||
url = f"{self.config.proxy_api_url}/cgi-bin/message/send"
|
||||
else:
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/message/send"
|
||||
|
||||
params = {"access_token": access_token}
|
||||
|
||||
data = {
|
||||
"touser": to_user,
|
||||
"toparty": to_party,
|
||||
"totag": to_tag,
|
||||
"msgtype": "text",
|
||||
"agentid": self.config.agent_id,
|
||||
"text": {
|
||||
"content": content
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(f"发送文本消息: {content[:100]}...")
|
||||
|
||||
response = requests.post(url, params=params, json=data, timeout=30)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
logger.info("文本消息发送成功")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"文本消息发送失败: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送文本消息异常: {str(e)}")
|
||||
return False
|
||||
|
||||
@retry_on_exception(RetryConfig(max_retries=3))
|
||||
def send_markdown_message(self, content: str,
|
||||
to_user: str = "@all",
|
||||
to_party: str = "",
|
||||
to_tag: str = "") -> bool:
|
||||
"""
|
||||
发送Markdown消息
|
||||
|
||||
Args:
|
||||
content: Markdown格式的消息内容
|
||||
to_user: 接收者用户ID
|
||||
to_party: 接收者部门ID
|
||||
to_tag: 接收者标签ID
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
try:
|
||||
access_token = self._get_access_token()
|
||||
if not access_token:
|
||||
logger.error("无法获取访问令牌,发送失败")
|
||||
return False
|
||||
|
||||
# 构建请求URL
|
||||
if self.config.use_proxy and hasattr(self.config, 'proxy_api_url'):
|
||||
url = f"{self.config.proxy_api_url}/cgi-bin/message/send"
|
||||
else:
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/message/send"
|
||||
|
||||
params = {"access_token": access_token}
|
||||
|
||||
data = {
|
||||
"touser": to_user,
|
||||
"toparty": to_party,
|
||||
"totag": to_tag,
|
||||
"msgtype": "markdown",
|
||||
"agentid": self.config.agent_id,
|
||||
"markdown": {
|
||||
"content": content
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("发送Markdown消息")
|
||||
|
||||
response = requests.post(url, params=params, json=data, timeout=30)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
logger.info("Markdown消息发送成功")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Markdown消息发送失败: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送Markdown消息异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def send_announcement_notification(self, announcements: List[Announcement],
|
||||
max_count: int = 20) -> bool:
|
||||
"""
|
||||
发送公告通知
|
||||
|
||||
Args:
|
||||
announcements: 公告列表
|
||||
max_count: 最大显示数量
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
if not announcements:
|
||||
logger.info("没有新公告,跳过通知")
|
||||
return True
|
||||
|
||||
try:
|
||||
# 生成通知内容
|
||||
notification_content = self._generate_announcement_notification(announcements, max_count)
|
||||
|
||||
# 发送Markdown消息
|
||||
return self.send_markdown_message(notification_content)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送公告通知失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def _generate_announcement_notification(self, announcements: List[Announcement],
|
||||
max_count: int) -> str:
|
||||
"""
|
||||
生成公告通知内容
|
||||
|
||||
Args:
|
||||
announcements: 公告列表
|
||||
max_count: 最大显示数量
|
||||
|
||||
Returns:
|
||||
str: Markdown格式的通知内容
|
||||
"""
|
||||
# 按日期分组
|
||||
today_announcements = []
|
||||
other_announcements = []
|
||||
|
||||
today = datetime.now().date()
|
||||
|
||||
for announcement in announcements:
|
||||
if announcement.publish_date and announcement.publish_date.date() == today:
|
||||
today_announcements.append(announcement)
|
||||
else:
|
||||
other_announcements.append(announcement)
|
||||
|
||||
lines = []
|
||||
|
||||
# 标题
|
||||
total_count = len(announcements)
|
||||
lines.append(f"# 🔔 广西政府采购网公告更新")
|
||||
lines.append("")
|
||||
lines.append(f"**发现 {total_count} 条新公告**")
|
||||
lines.append("")
|
||||
|
||||
# 今日公告
|
||||
if today_announcements:
|
||||
lines.append(f"## 📅 今日公告 ({len(today_announcements)}条)")
|
||||
lines.append("")
|
||||
display_today = today_announcements[:max_count//2]
|
||||
for announcement in display_today:
|
||||
title = announcement.title
|
||||
if len(title) > 40:
|
||||
title = title[:40] + "..."
|
||||
publish_time = announcement.publish_date.strftime("%H:%M") if announcement.publish_date else "N/A"
|
||||
lines.append(f"• [{title}]({announcement.content_url}) - {publish_time}")
|
||||
|
||||
if len(today_announcements) > len(display_today):
|
||||
lines.append(f"• ... 还有 {len(today_announcements) - len(display_today)} 条今日公告")
|
||||
|
||||
lines.append("")
|
||||
|
||||
# 其他公告
|
||||
if other_announcements:
|
||||
lines.append(f"## 📄 其他公告 ({len(other_announcements)}条)")
|
||||
lines.append("")
|
||||
remaining_slots = max_count - len(today_announcements) if today_announcements else max_count
|
||||
display_other = other_announcements[:remaining_slots]
|
||||
|
||||
for announcement in display_other:
|
||||
title = announcement.title
|
||||
if len(title) > 40:
|
||||
title = title[:40] + "..."
|
||||
publish_date = announcement.publish_date.strftime("%m-%d") if announcement.publish_date else "N/A"
|
||||
lines.append(f"• [{title}]({announcement.content_url}) - {publish_date}")
|
||||
|
||||
if len(other_announcements) > len(display_other):
|
||||
lines.append(f"• ... 还有 {len(other_announcements) - len(display_other)} 条公告")
|
||||
|
||||
lines.append("")
|
||||
|
||||
# 统计信息
|
||||
source_stats = {}
|
||||
for announcement in announcements:
|
||||
source = announcement.source_name
|
||||
source_stats[source] = source_stats.get(source, 0) + 1
|
||||
|
||||
lines.append("## 📊 统计信息")
|
||||
lines.append("")
|
||||
for source, count in sorted(source_stats.items()):
|
||||
lines.append(f"• {source}: {count}条")
|
||||
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append(f"*更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
|
||||
lines.append("*点击公告标题查看详情*")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def send_system_notification(self, title: str, content: str,
|
||||
message_type: str = "text") -> bool:
|
||||
"""
|
||||
发送系统通知
|
||||
|
||||
Args:
|
||||
title: 通知标题
|
||||
content: 通知内容
|
||||
message_type: 消息类型 (text/markdown)
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
try:
|
||||
if message_type == "markdown":
|
||||
full_content = f"# {title}\n\n{content}"
|
||||
return self.send_markdown_message(full_content)
|
||||
else:
|
||||
full_content = f"{title}\n\n{content}"
|
||||
return self.send_text_message(full_content)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送系统通知失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def send_error_notification(self, error_message: str, error_details: Optional[str] = None) -> bool:
|
||||
"""
|
||||
发送错误通知
|
||||
|
||||
Args:
|
||||
error_message: 错误消息
|
||||
error_details: 错误详情
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
content = f"## ❌ 系统错误\n\n**错误信息**: {error_message}"
|
||||
|
||||
if error_details:
|
||||
content += f"\n\n**错误详情**:\n```\n{error_details}\n```"
|
||||
|
||||
content += f"\n\n*发生时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*"
|
||||
|
||||
return self.send_markdown_message(content)
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""
|
||||
测试连接
|
||||
|
||||
Returns:
|
||||
bool: 连接是否正常
|
||||
"""
|
||||
try:
|
||||
token = self._get_access_token()
|
||||
return token is not None
|
||||
except Exception as e:
|
||||
logger.error(f"企业微信连接测试失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_service_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取服务状态
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 服务状态信息
|
||||
"""
|
||||
return {
|
||||
"service": "wechat",
|
||||
"enabled": self.config.enabled,
|
||||
"corp_id": self.config.corp_id[:10] + "..." if self.config.corp_id else None,
|
||||
"agent_id": self.config.agent_id,
|
||||
"has_token": self._access_token is not None,
|
||||
"token_expires_at": datetime.fromtimestamp(self._token_expires_at).isoformat() if self._token_expires_at > 0 else None,
|
||||
"use_proxy": self.config.use_proxy,
|
||||
"connection_test": self.test_connection() if self.config.enabled else False
|
||||
}
|
||||
|
||||
|
||||
class NotificationManager:
|
||||
"""通知管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.wechat = WeChatService()
|
||||
self._services = {
|
||||
"wechat": self.wechat
|
||||
}
|
||||
|
||||
def send_announcement_notification(self, announcements: List[Announcement]) -> Dict[str, bool]:
|
||||
"""
|
||||
发送公告通知
|
||||
|
||||
Args:
|
||||
announcements: 公告列表
|
||||
|
||||
Returns:
|
||||
Dict[str, bool]: 各服务发送结果
|
||||
"""
|
||||
results = {}
|
||||
|
||||
# 企业微信通知
|
||||
if self.wechat.config.enabled:
|
||||
try:
|
||||
results["wechat"] = self.wechat.send_announcement_notification(announcements)
|
||||
except Exception as e:
|
||||
logger.error(f"企业微信通知失败: {str(e)}")
|
||||
results["wechat"] = False
|
||||
else:
|
||||
results["wechat"] = None # 未启用
|
||||
|
||||
return results
|
||||
|
||||
def send_system_notification(self, title: str, content: str) -> Dict[str, bool]:
|
||||
"""
|
||||
发送系统通知
|
||||
|
||||
Args:
|
||||
title: 通知标题
|
||||
content: 通知内容
|
||||
|
||||
Returns:
|
||||
Dict[str, bool]: 发送结果
|
||||
"""
|
||||
results = {}
|
||||
|
||||
if self.wechat.config.enabled:
|
||||
try:
|
||||
results["wechat"] = self.wechat.send_system_notification(title, content, "markdown")
|
||||
except Exception as e:
|
||||
logger.error(f"企业微信系统通知失败: {str(e)}")
|
||||
results["wechat"] = False
|
||||
else:
|
||||
results["wechat"] = None
|
||||
|
||||
return results
|
||||
|
||||
def send_error_notification(self, error_message: str, error_details: Optional[str] = None) -> Dict[str, bool]:
|
||||
"""
|
||||
发送错误通知
|
||||
|
||||
Args:
|
||||
error_message: 错误消息
|
||||
error_details: 错误详情
|
||||
|
||||
Returns:
|
||||
Dict[str, bool]: 发送结果
|
||||
"""
|
||||
results = {}
|
||||
|
||||
if self.wechat.config.enabled:
|
||||
try:
|
||||
results["wechat"] = self.wechat.send_error_notification(error_message, error_details)
|
||||
except Exception as e:
|
||||
logger.error(f"企业微信错误通知失败: {str(e)}")
|
||||
results["wechat"] = False
|
||||
else:
|
||||
results["wechat"] = None
|
||||
|
||||
return results
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取通知服务状态
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 服务状态
|
||||
"""
|
||||
return {
|
||||
"services": {
|
||||
name: service.get_service_status() for name, service in self._services.items()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 全局通知管理器实例
|
||||
_notification_manager = None
|
||||
|
||||
|
||||
def get_notification_manager() -> NotificationManager:
|
||||
"""
|
||||
获取通知管理器实例
|
||||
|
||||
Returns:
|
||||
NotificationManager: 通知管理器实例
|
||||
"""
|
||||
global _notification_manager
|
||||
if _notification_manager is None:
|
||||
_notification_manager = NotificationManager()
|
||||
return _notification_manager
|
||||
|
||||
|
||||
def send_announcements_notification(announcements: List[Announcement]) -> bool:
|
||||
"""
|
||||
发送公告通知
|
||||
|
||||
Args:
|
||||
announcements: 公告列表
|
||||
|
||||
Returns:
|
||||
bool: 是否至少有一个服务发送成功
|
||||
"""
|
||||
manager = get_notification_manager()
|
||||
results = manager.send_announcement_notification(announcements)
|
||||
|
||||
# 检查是否有服务发送成功
|
||||
return any(result for result in results.values() if result is True)
|
||||
|
||||
|
||||
def send_system_notification(title: str, content: str) -> bool:
|
||||
"""
|
||||
发送系统通知
|
||||
|
||||
Args:
|
||||
title: 通知标题
|
||||
content: 通知内容
|
||||
|
||||
Returns:
|
||||
bool: 是否至少有一个服务发送成功
|
||||
"""
|
||||
manager = get_notification_manager()
|
||||
results = manager.send_system_notification(title, content)
|
||||
|
||||
return any(result for result in results.values() if result is True)
|
||||
|
||||
|
||||
def send_error_alert(error_message: str, error_details: Optional[str] = None) -> bool:
|
||||
"""
|
||||
发送错误警报
|
||||
|
||||
Args:
|
||||
error_message: 错误消息
|
||||
error_details: 错误详情
|
||||
|
||||
Returns:
|
||||
bool: 是否至少有一个服务发送成功
|
||||
"""
|
||||
manager = get_notification_manager()
|
||||
results = manager.send_error_notification(error_message, error_details)
|
||||
|
||||
return any(result for result in results.values() if result is True)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -62,6 +62,95 @@ class PostgreSQLStorage:
|
||||
# 尝试逐个保存
|
||||
return self._save_announcements_fallback(announcements)
|
||||
|
||||
def save_auto_announcements(self, announcements: List[Announcement]) -> int:
|
||||
"""
|
||||
保存定时搜索公告到专用表(关键词匹配专用)
|
||||
|
||||
Args:
|
||||
announcements: 公告列表
|
||||
|
||||
Returns:
|
||||
int: 成功保存的数量
|
||||
"""
|
||||
if not announcements:
|
||||
return 0
|
||||
|
||||
logger.info(f"开始保存 {len(announcements)} 条定时搜索公告到专用表")
|
||||
|
||||
try:
|
||||
# 批量保存到auto_announcements表
|
||||
saved_count = self.db_manager.save_announcements_batch_to_table(announcements, "auto_announcements")
|
||||
|
||||
if saved_count > 0:
|
||||
logger.info(f"成功保存 {saved_count} 条定时搜索公告到专用表")
|
||||
|
||||
return saved_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存定时搜索公告到专用表失败: {str(e)}")
|
||||
return 0
|
||||
|
||||
def save_manual_announcements_by_source(self, announcements: List[Announcement],
|
||||
max_per_source: int = 100) -> Dict[str, int]:
|
||||
"""
|
||||
按来源保存手动搜索公告到专用表,每个来源保留最新的max_per_source条
|
||||
|
||||
Args:
|
||||
announcements: 所有公告列表(未经关键词筛选)
|
||||
max_per_source: 每个来源最大保留数量
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: 各来源保存的数量
|
||||
"""
|
||||
if not announcements:
|
||||
return {}
|
||||
|
||||
logger.info(f"开始按来源保存 {len(announcements)} 条手动搜索公告到专用表,每个来源最多保留 {max_per_source} 条")
|
||||
|
||||
try:
|
||||
# 按来源分组
|
||||
source_groups = {}
|
||||
for announcement in announcements:
|
||||
source_code = announcement.source_code
|
||||
if source_code not in source_groups:
|
||||
source_groups[source_code] = []
|
||||
source_groups[source_code].append(announcement)
|
||||
|
||||
saved_stats = {}
|
||||
|
||||
for source_code, source_announcements in source_groups.items():
|
||||
# 对每个来源的公告按发布时间排序(最新的在前)
|
||||
sorted_announcements = sorted(
|
||||
source_announcements,
|
||||
key=lambda x: x.publish_date or datetime.min,
|
||||
reverse=True
|
||||
)
|
||||
|
||||
# 为没有哈希的公告生成哈希
|
||||
for announcement in sorted_announcements:
|
||||
if not announcement.content_hash:
|
||||
announcement.generate_content_hash()
|
||||
|
||||
# 批量保存
|
||||
to_save = sorted_announcements[:max_per_source]
|
||||
saved_count = self.db_manager.save_announcements_batch_to_table(to_save, "manual_announcements")
|
||||
saved_stats[source_code] = saved_count
|
||||
|
||||
# 清理该来源超出限制的旧数据
|
||||
if len(sorted_announcements) > max_per_source:
|
||||
self._cleanup_old_announcements_by_source_in_table(source_code, max_per_source, "manual_announcements")
|
||||
|
||||
logger.info(f"来源 {source_code} 保存了 {saved_count} 条手动搜索公告")
|
||||
|
||||
total_saved = sum(saved_stats.values())
|
||||
logger.info(f"按来源保存手动搜索公告完成,总计保存 {total_saved} 条公告")
|
||||
|
||||
return saved_stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"按来源保存手动搜索公告失败: {str(e)}")
|
||||
return {}
|
||||
|
||||
def save_all_announcements_by_source(self, announcements: List[Announcement],
|
||||
max_per_source: int = 100) -> Dict[str, int]:
|
||||
"""
|
||||
@@ -186,6 +275,41 @@ class PostgreSQLStorage:
|
||||
# 由于我们在爬取时已经标记,这里主要是确保数据库中的标记正确
|
||||
pass
|
||||
|
||||
def _mark_new_announcements_in_table(self, announcements: List[Announcement], table_name: str):
|
||||
"""在指定表中标记新公告"""
|
||||
# 这里可以添加新公告标记逻辑
|
||||
pass
|
||||
|
||||
def _cleanup_old_announcements_by_source_in_table(self, source_code: str, max_per_source: int, table_name: str):
|
||||
"""在指定表中清理来源的旧公告"""
|
||||
try:
|
||||
with self.db_manager.get_db_cursor() as cursor:
|
||||
# 获取该来源当前保存的公告数量
|
||||
cursor.execute(f"""
|
||||
SELECT COUNT(*) FROM {table_name}
|
||||
WHERE source_code = %s
|
||||
""", (source_code,))
|
||||
|
||||
current_count = cursor.fetchone()[0]
|
||||
|
||||
if current_count > max_per_source:
|
||||
# 删除超出数量的旧公告
|
||||
delete_count = current_count - max_per_source
|
||||
cursor.execute(f"""
|
||||
DELETE FROM {table_name}
|
||||
WHERE id IN (
|
||||
SELECT id FROM {table_name}
|
||||
WHERE source_code = %s
|
||||
ORDER BY publish_date DESC, created_at DESC
|
||||
OFFSET %s
|
||||
)
|
||||
""", (source_code, max_per_source))
|
||||
|
||||
logger.info(f"清理了 {cursor.rowcount} 条{table_name}表中来源{source_code}的旧公告")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"清理{table_name}表中来源{source_code}的旧公告失败: {str(e)}")
|
||||
|
||||
def save_crawl_results(self, results: List[CrawlResult]) -> int:
|
||||
"""
|
||||
保存爬取结果
|
||||
@@ -389,6 +513,15 @@ class StorageManager:
|
||||
"""按来源保存所有公告"""
|
||||
return self._current_storage.save_all_announcements_by_source(announcements, max_per_source)
|
||||
|
||||
def save_auto_announcements(self, announcements: List[Announcement]) -> int:
|
||||
"""保存定时搜索公告到专用表"""
|
||||
return self._current_storage.save_auto_announcements(announcements)
|
||||
|
||||
def save_manual_announcements_by_source(self, announcements: List[Announcement],
|
||||
max_per_source: int = 100) -> Dict[str, int]:
|
||||
"""按来源保存手动搜索公告到专用表"""
|
||||
return self._current_storage.save_manual_announcements_by_source(announcements, max_per_source)
|
||||
|
||||
def save_crawl_results(self, results: List[CrawlResult]) -> int:
|
||||
"""保存爬取结果"""
|
||||
return self._current_storage.save_crawl_results(results)
|
||||
@@ -476,6 +609,34 @@ def save_all_announcements_by_source_to_storage(announcements: List[Announcement
|
||||
return get_storage_manager().save_all_announcements_by_source(announcements, max_per_source)
|
||||
|
||||
|
||||
def save_auto_announcements_to_storage(announcements: List[Announcement]) -> int:
|
||||
"""
|
||||
保存定时搜索公告到专用表
|
||||
|
||||
Args:
|
||||
announcements: 公告列表
|
||||
|
||||
Returns:
|
||||
int: 保存成功的数量
|
||||
"""
|
||||
return get_storage_manager().save_auto_announcements(announcements)
|
||||
|
||||
|
||||
def save_manual_announcements_by_source_to_storage(announcements: List[Announcement],
|
||||
max_per_source: int = 100) -> Dict[str, int]:
|
||||
"""
|
||||
按来源保存手动搜索公告到专用表
|
||||
|
||||
Args:
|
||||
announcements: 所有公告列表
|
||||
max_per_source: 每个来源最大保留数量
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: 各来源保存的数量
|
||||
"""
|
||||
return get_storage_manager().save_manual_announcements_by_source(announcements, max_per_source)
|
||||
|
||||
|
||||
def cleanup_storage(days: Optional[int] = None) -> int:
|
||||
"""
|
||||
清理存储中的过期数据
|
||||
|
||||
Binary file not shown.
@@ -244,32 +244,101 @@ class WeChatMessageHandler:
|
||||
|
||||
try:
|
||||
from ..storage.postgresql import get_storage_manager
|
||||
storage = get_storage_manager()
|
||||
stats = storage.get_statistics()
|
||||
from ..core.database import get_db_cursor
|
||||
from datetime import date
|
||||
|
||||
if stats:
|
||||
response = f"""今日公告统计
|
||||
# 获取今日关键词命中公告数(从auto_announcements表)
|
||||
today = date.today()
|
||||
with get_db_cursor() as cursor:
|
||||
# 今日关键词命中总数
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as today_keyword_hits
|
||||
FROM auto_announcements
|
||||
WHERE DATE(publish_date) = %s
|
||||
""", (today,))
|
||||
today_keyword_hits = cursor.fetchone()['today_keyword_hits']
|
||||
|
||||
# 各类型今日关键词命中数
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
announcement_type,
|
||||
COUNT(*) as count
|
||||
FROM auto_announcements
|
||||
WHERE DATE(publish_date) = %s
|
||||
GROUP BY announcement_type
|
||||
ORDER BY count DESC
|
||||
""", (today,))
|
||||
type_stats = {row['announcement_type']: row['count'] for row in cursor.fetchall()}
|
||||
|
||||
# 历史累计关键词命中数
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as total_keyword_hits
|
||||
FROM auto_announcements
|
||||
""")
|
||||
total_keyword_hits = cursor.fetchone()['total_keyword_hits']
|
||||
|
||||
# 今日各来源关键词命中数
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
source_name,
|
||||
COUNT(*) as count
|
||||
FROM auto_announcements
|
||||
WHERE DATE(publish_date) = %s
|
||||
GROUP BY source_name
|
||||
ORDER BY count DESC
|
||||
LIMIT 5
|
||||
""", (today,))
|
||||
source_stats = cursor.fetchall()
|
||||
|
||||
# 类型名称映射
|
||||
type_name_map = {
|
||||
'purchase': '采购公告',
|
||||
'result': '结果公告',
|
||||
'correction': '更正公告',
|
||||
'contract': '合同公告',
|
||||
'pre_announcement': '预公示',
|
||||
'single_source': '单一来源',
|
||||
'electronic_market': '电子卖场',
|
||||
'acceptance': '履约验收',
|
||||
'engineering': '工程公告',
|
||||
'intention': '采购意向'
|
||||
}
|
||||
|
||||
if today_keyword_hits > 0:
|
||||
response = f"""📊 今日关键词命中统计
|
||||
|
||||
统计时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
数据概览:
|
||||
- 今日新增: {stats.get('today_count', 0)} 条
|
||||
- 累计总数: {stats.get('total_count', 0)} 条
|
||||
- 活跃关键词: {stats.get('active_keywords', 0)} 个
|
||||
🎯 今日关键词命中: {today_keyword_hits} 条
|
||||
📈 历史累计命中: {total_keyword_hits} 条
|
||||
|
||||
分类统计:
|
||||
- 采购公告: {stats.get('purchase_count', 0)} 条
|
||||
- 结果公告: {stats.get('result_count', 0)} 条
|
||||
- 更正公告: {stats.get('correction_count', 0)} 条
|
||||
- 其他类型: {stats.get('other_count', 0)} 条
|
||||
📋 今日命中分类:
|
||||
"""
|
||||
|
||||
提示: 数据每小时更新,点击"立即搜索"可获取最新数据."""
|
||||
# 添加各类型统计
|
||||
for ann_type, count in type_stats.items():
|
||||
type_name = type_name_map.get(ann_type, ann_type)
|
||||
response += f"- {type_name}: {count} 条\n"
|
||||
|
||||
response += "\n🏢 今日命中来源TOP5:\n"
|
||||
for i, source in enumerate(source_stats, 1):
|
||||
response += f"{i}. {source['source_name']}: {source['count']} 条\n"
|
||||
|
||||
response += "\n💡 提示: 这些是关键词自动匹配成功的公告"
|
||||
else:
|
||||
response = """今日公告统计
|
||||
response = f"""📊 今日关键词命中统计
|
||||
|
||||
暂无统计数据,请先执行"立即搜索"获取最新公告。
|
||||
统计时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
建议: 系统会在每日定时搜索,您也可以手动触发更新."""
|
||||
🎯 今日关键词命中: 0 条
|
||||
📈 历史累计命中: {total_keyword_hits} 条
|
||||
|
||||
暂无今日关键词命中公告。
|
||||
|
||||
💡 可能原因:
|
||||
- 今日暂无匹配关键词的公告发布
|
||||
- 系统定时搜索还未执行
|
||||
- 点击"立即搜索"可手动触发更新"""
|
||||
|
||||
except Exception as db_error:
|
||||
logger.warning(f"数据库查询失败,使用默认统计: {str(db_error)}")
|
||||
@@ -347,22 +416,22 @@ class WeChatMessageHandler:
|
||||
return self._create_text_response("获取最新公告失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_latest_news_by_source(self, source_choice: str, from_user: str) -> Optional[str]:
|
||||
"""处理按来源查看最新公告"""
|
||||
"""处理按来源查看最新公告 - 直接从指定来源爬取最新的10条公告"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 选择公告来源: {source_choice}")
|
||||
|
||||
# 来源映射
|
||||
# 来源映射 - 公告类型到来源代码的映射
|
||||
source_mapping = {
|
||||
"1": ("purchase", "采购公告"),
|
||||
"2": ("result", "结果公告"),
|
||||
"3": ("correction", "更正公告"),
|
||||
"4": ("contract", "合同公告"),
|
||||
"5": ("pre_announcement", "预公示"),
|
||||
"6": ("single_source", "单一来源"),
|
||||
"7": ("electronic_market", "电子卖场"),
|
||||
"8": ("acceptance", "履约验收"),
|
||||
"9": ("engineering", "工程公告"),
|
||||
"10": ("intention", "采购意向")
|
||||
"1": ("ZcyAnnouncement1", "采购公告"),
|
||||
"2": ("ZcyAnnouncement2", "结果公告"),
|
||||
"3": ("ZcyAnnouncement4", "更正公告"),
|
||||
"4": ("ZcyAnnouncement3", "合同公告"),
|
||||
"5": ("ZcyAnnouncement5", "预公示"),
|
||||
"6": ("ZcyAnnouncement6", "单一来源"),
|
||||
"7": ("ZcyAnnouncement7", "电子卖场"),
|
||||
"8": ("ZcyAnnouncement10", "履约验收"),
|
||||
"9": ("ZcyAnnouncement11", "工程公告"),
|
||||
"10": ("61-266648", "采购意向")
|
||||
}
|
||||
|
||||
if source_choice == "全部" or source_choice == "all":
|
||||
@@ -377,48 +446,91 @@ class WeChatMessageHandler:
|
||||
返回公告查询菜单,请点击"最新公告"重新选择。"""
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
ann_type, type_name = source_mapping[source_choice]
|
||||
source_code, type_name = source_mapping[source_choice]
|
||||
|
||||
# 查询该类型的最新公告
|
||||
# 直接从指定来源爬取最新公告
|
||||
try:
|
||||
from ..storage.postgresql import get_storage_manager
|
||||
storage = get_storage_manager()
|
||||
# 获取所有最近公告,然后过滤类型
|
||||
all_announcements = storage.get_recent_announcements(hours=168, limit=100) # 最近7天
|
||||
filtered_announcements = [ann for ann in all_announcements if ann.announcement_type.value == ann_type][:10]
|
||||
from ..crawler.spider import crawl_announcements
|
||||
from ..filters.filters import DateFilter
|
||||
from datetime import date
|
||||
|
||||
if filtered_announcements:
|
||||
response = f"""📋 {type_name} - 最新10条
|
||||
# 只爬取指定来源的公告
|
||||
logger.info(f"开始爬取 {type_name} 来源的公告")
|
||||
crawl_results = crawl_announcements(sources=[source_code])
|
||||
|
||||
🕒 更新时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
if not crawl_results or not crawl_results[0].announcements:
|
||||
response = f"❌ 暂无 {type_name} 相关公告"
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
# 获取该来源的所有公告
|
||||
source_announcements = crawl_results[0].announcements
|
||||
|
||||
# 按日期筛选(今天的数据)
|
||||
date_filter = DateFilter()
|
||||
today_announcements = date_filter.filter_announcements(
|
||||
source_announcements,
|
||||
start_date=date.today(),
|
||||
end_date=date.today()
|
||||
)
|
||||
|
||||
# 按发布时间排序,取最新的10条
|
||||
sorted_announcements = sorted(
|
||||
today_announcements,
|
||||
key=lambda x: x.publish_date or x.crawled_at,
|
||||
reverse=True
|
||||
)[:10]
|
||||
|
||||
if not sorted_announcements:
|
||||
response = f"❌ 今天暂无 {type_name} 相关公告"
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
# 生成Markdown格式的结果并发送
|
||||
markdown_content = self._generate_latest_news_markdown(sorted_announcements, type_name)
|
||||
|
||||
try:
|
||||
from ..notification.wechat import get_notification_manager
|
||||
manager = get_notification_manager()
|
||||
if hasattr(manager.wechat, 'send_system_notification'):
|
||||
notify_success = manager.wechat.send_system_notification(
|
||||
title=f"📋 {type_name} - 最新公告",
|
||||
content=markdown_content,
|
||||
message_type="markdown"
|
||||
)
|
||||
if notify_success:
|
||||
# 返回简短确认
|
||||
return self._create_text_response(f"✅ 已发送 {type_name} 最新5条公告到聊天窗口。", from_user)
|
||||
else:
|
||||
# 如果Markdown发送失败,返回文本格式
|
||||
return self._create_text_response(f"发送失败,已获取 {len(sorted_announcements)} 条 {type_name} 公告。", from_user)
|
||||
else:
|
||||
# 如果不支持markdown,返回文本格式
|
||||
response = f"""📋 {type_name} - 最新公告
|
||||
|
||||
共找到 {len(sorted_announcements)} 条公告:
|
||||
|
||||
"""
|
||||
for i, announcement in enumerate(sorted_announcements[:5], 1): # 只显示前5条
|
||||
title = announcement.title[:25] + "..." if len(announcement.title) > 25 else announcement.title
|
||||
time_str = announcement.publish_date.strftime('%m-%d %H:%M') if announcement.publish_date else "未知"
|
||||
response += f"{i}. {title}\n 🕒 {time_str}\n"
|
||||
|
||||
for i, announcement in enumerate(filtered_announcements, 1):
|
||||
title = announcement.title[:25] + "..." if len(announcement.title) > 25 else announcement.title
|
||||
time_str = announcement.publish_date.strftime('%m-%d %H:%M') if announcement.publish_date else "未知"
|
||||
response += f"{i}. {title}\n 🕒 {time_str} | 🏷️ {announcement.purchase_name or '未知'}\n\n"
|
||||
if len(sorted_announcements) > 5:
|
||||
response += f"\n... 还有 {len(sorted_announcements) - 5} 条公告"
|
||||
|
||||
response += "💡 发送关键词可进一步筛选,点击菜单可查看更多功能。"
|
||||
else:
|
||||
response = f"""📋 {type_name}
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
暂无该类型的最新公告数据。
|
||||
except Exception as notify_error:
|
||||
logger.warning(f"发送Markdown通知失败: {str(notify_error)}")
|
||||
# 返回文本格式的结果
|
||||
response = f"""📋 {type_name} - 最新公告
|
||||
|
||||
💡 建议:
|
||||
• 点击"立即搜索"更新数据
|
||||
• 该类型公告可能较少出现
|
||||
• 返回重新选择其他类型"""
|
||||
共找到 {len(sorted_announcements)} 条公告,请查看详细结果。"""
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as db_error:
|
||||
logger.warning(f"数据库查询失败: {str(db_error)}")
|
||||
response = f"""📋 {type_name}
|
||||
|
||||
暂时无法获取数据,请稍后重试。
|
||||
|
||||
您可以返回重新选择其他类型。"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
except Exception as e:
|
||||
logger.error(f"爬取公告失败: {str(e)}")
|
||||
response = f"❌ 获取 {type_name} 公告失败,请稍后重试"
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"按来源查看最新公告异常: {str(e)}")
|
||||
@@ -1005,6 +1117,34 @@ class WeChatMessageHandler:
|
||||
|
||||
return "".join(lines)
|
||||
|
||||
def _generate_latest_news_markdown(self, announcements: List, source_type_name: str) -> str:
|
||||
"""生成最新公告的markdown格式"""
|
||||
import datetime
|
||||
|
||||
# 只显示最新的5条公告
|
||||
display_announcements = announcements[:5]
|
||||
|
||||
# 生成markdown内容
|
||||
lines = [
|
||||
f"总公告数: {len(announcements)}\n\n",
|
||||
f"更新时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
]
|
||||
|
||||
# 逐条列出公告(最多5条)
|
||||
for i, ann in enumerate(display_announcements, 1):
|
||||
title = ann.title
|
||||
if len(title) > 50:
|
||||
title = title[:50] + "..."
|
||||
|
||||
url = ann.content_url or "#"
|
||||
date_str = ann.publish_date.strftime('%Y-%m-%d') if ann.publish_date else "未知"
|
||||
purchaser = ann.purchase_name or "未知"
|
||||
|
||||
lines.append(f"{i}. [{title}]({url})\n\n")
|
||||
lines.append(f" {date_str} | {purchaser}\n\n")
|
||||
|
||||
return "".join(lines)
|
||||
|
||||
def handle_other_message(self, msg_type: str, from_user: str) -> Optional[str]:
|
||||
"""处理其他类型的消息"""
|
||||
try:
|
||||
|
||||
@@ -1,970 +0,0 @@
|
||||
"""
|
||||
企业微信消息处理器
|
||||
处理用户消息和事件,实现菜单功能
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from ..core.config_manager import get_config
|
||||
from ..core.logger import get_logger
|
||||
from ..notification.wechat import send_system_notification
|
||||
from ..storage.postgresql import save_all_announcements_by_source_to_storage
|
||||
from ..storage.md_generator import generate_onu_md
|
||||
from ..core.models import Announcement
|
||||
except ImportError:
|
||||
try:
|
||||
from core.config_manager import get_config
|
||||
from core.logger import get_logger
|
||||
from notification.wechat import send_system_notification
|
||||
from storage.postgresql import save_all_announcements_by_source_to_storage
|
||||
from storage.md_generator import generate_onu_md
|
||||
from core.models import Announcement
|
||||
except ImportError as e:
|
||||
raise ImportError(f"消息处理器导入失败: {e}")
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class WeChatMessageHandler:
|
||||
"""企业微信消息处理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = get_config()
|
||||
self.monitor_app = None
|
||||
|
||||
# 菜单配置
|
||||
self.menu_config = {
|
||||
"crawl_now": {
|
||||
"key": "crawl_now",
|
||||
"name": " 立即搜索",
|
||||
"description": "立即执行一次公告搜索"
|
||||
},
|
||||
"today_stats": {
|
||||
"key": "today_stats",
|
||||
"name": " 今日统计",
|
||||
"description": "查看今日公告统计信息"
|
||||
},
|
||||
"keyword_search": {
|
||||
"key": "keyword_search",
|
||||
"name": " 关键词搜索",
|
||||
"description": "输入关键词搜索公告"
|
||||
},
|
||||
"latest_news": {
|
||||
"key": "latest_news",
|
||||
"name": " 最新公告",
|
||||
"description": "查看最新发布的公告"
|
||||
},
|
||||
"keyword_manage": {
|
||||
"key": "keyword_manage",
|
||||
"name": " 关键词管理",
|
||||
"description": "管理监控关键词"
|
||||
},
|
||||
"system_status": {
|
||||
"key": "system_status",
|
||||
"name": " 系统状态",
|
||||
"description": "查看系统运行状态"
|
||||
},
|
||||
"clear_cache": {
|
||||
"key": "clear_cache",
|
||||
"name": " 清理缓存",
|
||||
"description": "清理系统缓存数据"
|
||||
},
|
||||
"help_guide": {
|
||||
"key": "help_guide",
|
||||
"name": " 使用说明",
|
||||
"description": "查看详细使用说明"
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("企业微信消息处理器初始化完成")
|
||||
|
||||
def _get_monitor_app(self):
|
||||
"""获取监控应用实例"""
|
||||
if self.monitor_app is None:
|
||||
# 动态导入避免循环导入
|
||||
try:
|
||||
from ..main import GXGPMonitorApp
|
||||
self.monitor_app = GXGPMonitorApp()
|
||||
# 初始化但不启动服务器
|
||||
if not self.monitor_app.initialize():
|
||||
logger.error("监控应用初始化失败")
|
||||
return None
|
||||
except ImportError:
|
||||
logger.error("无法导入监控应用")
|
||||
return None
|
||||
return self.monitor_app
|
||||
|
||||
def handle_event(self, event: str, event_key: Optional[str], from_user: str) -> Optional[str]:
|
||||
"""处理事件消息"""
|
||||
try:
|
||||
logger.info(f"处理事件: {event}, key: {event_key}, user: {from_user}")
|
||||
|
||||
if event == 'click':
|
||||
# 菜单点击事件
|
||||
if event_key == 'crawl_now':
|
||||
return self._handle_crawl_now(from_user)
|
||||
elif event_key == 'today_stats':
|
||||
return self._handle_today_stats(from_user)
|
||||
elif event_key == 'keyword_search':
|
||||
return self._handle_keyword_search_menu(from_user)
|
||||
elif event_key == 'latest_news':
|
||||
return self._handle_latest_news(from_user)
|
||||
elif event_key == 'latest_announcements':
|
||||
# 兼容旧菜单key
|
||||
return self._handle_latest_news(from_user)
|
||||
elif event_key == 'keyword_manage':
|
||||
return self._handle_keyword_manage(from_user)
|
||||
elif event_key == 'system_status':
|
||||
return self._handle_system_status(from_user)
|
||||
elif event_key == 'clear_cache':
|
||||
return self._handle_clear_cache(from_user)
|
||||
elif event_key == 'help_guide':
|
||||
return self._handle_help_guide(from_user)
|
||||
elif event_key == 'keyword_search':
|
||||
return self._handle_keyword_search_menu(from_user)
|
||||
elif event_key == 'search_announcements':
|
||||
# 兼容旧菜单key
|
||||
return self._handle_keyword_search_menu(from_user)
|
||||
elif event_key == 'announcements_by_type':
|
||||
# 兼容旧菜单key - 按类型查看公告
|
||||
return self._handle_announcements_by_type(from_user)
|
||||
else:
|
||||
return self._create_text_response("未知菜单项", from_user)
|
||||
|
||||
elif event == 'subscribe':
|
||||
# 关注事件
|
||||
welcome_msg = """欢迎关注广西政府采购网公告监控!
|
||||
|
||||
我可以帮您:
|
||||
- 自动监控最新采购公告
|
||||
- 筛选您关心的关键词信息
|
||||
- 及时推送重要更新
|
||||
|
||||
点击下方菜单开始使用."""
|
||||
return self._create_text_response(welcome_msg, from_user)
|
||||
|
||||
elif event == 'unsubscribe':
|
||||
# 取消关注事件
|
||||
logger.info(f"用户 {from_user} 取消关注")
|
||||
return None
|
||||
|
||||
else:
|
||||
logger.info(f"未处理的event类型: {event}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"事件处理异常: {str(e)}")
|
||||
return self._create_text_response("处理失败,请稍后重试", from_user)
|
||||
|
||||
def handle_text_message(self, content: str, from_user: str) -> Optional[str]:
|
||||
"""处理文本消息"""
|
||||
try:
|
||||
logger.info(f"处理文本消息: {content}, user: {from_user}")
|
||||
|
||||
# 移除前后空格
|
||||
content = content.strip()
|
||||
|
||||
if content == "帮助" or content == "help":
|
||||
return self._handle_help_guide(from_user)
|
||||
elif content.startswith("爬取"):
|
||||
return self._handle_manual_crawl(content, from_user)
|
||||
elif content.startswith("总结") or content == "统计":
|
||||
return self._handle_today_stats(from_user)
|
||||
elif content.startswith("最新公告") or content.startswith("最新"):
|
||||
return self._handle_latest_news(from_user)
|
||||
elif content.startswith("系统状态") or content.startswith("状态"):
|
||||
return self._handle_system_status(from_user)
|
||||
elif content.startswith("关键词"):
|
||||
return self._handle_keyword_search(content, from_user)
|
||||
elif content.startswith("添加关键词"):
|
||||
# 这里可以实现关键词添加逻辑
|
||||
return self._create_text_response("关键词管理功能正在开发中,请联系管理员", from_user)
|
||||
elif content.startswith("删除关键词"):
|
||||
# 这里可以实现关键词删除逻辑
|
||||
return self._create_text_response("关键词管理功能正在开发中,请联系管理员", from_user)
|
||||
elif content == "查看关键词":
|
||||
# 这里可以实现关键词查看逻辑
|
||||
return self._create_text_response("当前监控关键词:政府采购大化南宁信息化", from_user)
|
||||
elif content.startswith("清理缓存") or content.startswith("清理"):
|
||||
return self._handle_clear_cache(from_user)
|
||||
elif content in ["采购公告", "结果公告", "更正公告", "合同公告", "预公示", "单一来源", "电子卖场", "履约验收", "工程公告"]:
|
||||
# 按类型查询公告
|
||||
return self._handle_search_by_type(content, from_user)
|
||||
else:
|
||||
# 默认当作关键词搜索
|
||||
return self._handle_keyword_search(f"关键词 {content}", from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"文本消息处理异常: {str(e)}")
|
||||
return self._create_text_response("处理失败,请稍后重试", from_user)
|
||||
|
||||
def handle_other_message(self, msg_type: str, from_user: str) -> Optional[str]:
|
||||
"""处理其他类型的消息"""
|
||||
try:
|
||||
logger.info(f"处理其他消息类型: {msg_type}, user: {from_user}")
|
||||
|
||||
if msg_type == 'image':
|
||||
return self._create_text_response("收到图片消息,但我只能处理文本消息", from_user)
|
||||
elif msg_type == 'voice':
|
||||
return self._create_text_response("收到语音消息,但我只能处理文本消息", from_user)
|
||||
else:
|
||||
return self._create_text_response(f"收到{msg_type}消息,暂不支持此类型", from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"其他消息处理异常: {str(e)}")
|
||||
return self._create_text_response("处理失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_crawl_now(self, from_user: str) -> Optional[str]:
|
||||
"""处理立即爬取菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 触发立即搜索")
|
||||
|
||||
# 获取监控应用
|
||||
app = self._get_monitor_app()
|
||||
if not app:
|
||||
return self._create_text_response("系统初始化失败,请稍后重试", from_user)
|
||||
|
||||
# 执行搜索
|
||||
result = app.run_crawl()
|
||||
|
||||
if result.get("success"):
|
||||
total = result.get("total_crawled", 0)
|
||||
filtered = result.get("filtered", 0)
|
||||
saved = result.get("saved", 0)
|
||||
|
||||
response = f"""OK 搜索完成!
|
||||
|
||||
统计信息:
|
||||
- 总共发现: {total} 条公告
|
||||
- 关键词筛选: {filtered} 条
|
||||
- 已保存: {saved} 条
|
||||
|
||||
如有匹配的公告,我会及时推送通知."""
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
response = f"FAIL 搜索失败: {error}"
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"立即搜索处理异常: {str(e)}")
|
||||
return self._create_text_response("搜索失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_today_stats(self, from_user: str) -> Optional[str]:
|
||||
"""处理今日统计菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求今日统计")
|
||||
|
||||
# 获取监控应用
|
||||
app = self._get_monitor_app()
|
||||
if not app:
|
||||
return self._create_text_response("系统初始化失败,请稍后重试", from_user)
|
||||
|
||||
# 查询今日统计数据
|
||||
try:
|
||||
from ..storage.postgresql import get_storage_manager
|
||||
storage = get_storage_manager()
|
||||
stats = storage.get_statistics()
|
||||
|
||||
if stats:
|
||||
response = f""" 今日公告统计
|
||||
|
||||
统计时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
数据概览:
|
||||
- 今日新增:{stats.get('today_count', 0)} 条
|
||||
- 累计总数:{stats.get('total_count', 0)} 条
|
||||
- 活跃关键词:{stats.get('active_keywords', 0)} 个
|
||||
|
||||
分类统计:
|
||||
- 采购公告:{stats.get('purchase_count', 0)} 条
|
||||
- 结果公告:{stats.get('result_count', 0)} 条
|
||||
- 更正公告:{stats.get('correction_count', 0)} 条
|
||||
- 其他类型:{stats.get('other_count', 0)} 条
|
||||
|
||||
热门地区:
|
||||
{chr(10).join([f"- {region}: {count}条" for region, count in stats.get('region_stats', {}).items()][:5])}
|
||||
|
||||
提示:数据每小时更新,点击"立即搜索"可获取最新数据."""
|
||||
else:
|
||||
response = """ 今日公告统计
|
||||
|
||||
暂无统计数据,请先执行"立即搜索"获取最新公告.
|
||||
|
||||
建议:系统会在每日定时搜索,您也可以手动触发更新."""
|
||||
|
||||
except Exception as db_error:
|
||||
logger.warning(f"数据库查询失败,使用默认统计: {str(db_error)}")
|
||||
response = """ 系统状态
|
||||
|
||||
数据库连接中,请稍后查看详细统计.
|
||||
|
||||
您可以:
|
||||
- 点击"立即搜索"更新数据
|
||||
- 查看"系统状态"了解服务运行情况"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"今日统计处理异常: {str(e)}")
|
||||
return self._create_text_response("获取统计失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_custom_crawl(self, event_key: str, from_user: str) -> Optional[str]:
|
||||
"""处理自定义搜索菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 触发自定义搜索")
|
||||
|
||||
response = """ 自定义搜索
|
||||
|
||||
请回复您想要搜索的关键词,我将为您执行搜索并筛选相关公告.
|
||||
|
||||
例如:
|
||||
- 大化
|
||||
- 信息化
|
||||
- 政府采购
|
||||
|
||||
发送关键词开始搜索."""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"自定义搜索处理异常: {str(e)}")
|
||||
return self._create_text_response("操作失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_help(self, from_user: str) -> Optional[str]:
|
||||
"""处理帮助命令(兼容旧版本)"""
|
||||
return self._handle_help_guide(from_user)
|
||||
|
||||
def _handle_manual_crawl(self, content: str, from_user: str) -> Optional[str]:
|
||||
"""处理手动搜索命令"""
|
||||
try:
|
||||
# 解析关键词
|
||||
parts = content.split()
|
||||
if len(parts) < 2:
|
||||
return self._create_text_response("请指定搜索关键词,例如:搜索 大化", from_user)
|
||||
|
||||
keywords = parts[1:]
|
||||
logger.info(f"用户 {from_user} 手动搜索关键词: {keywords}")
|
||||
|
||||
# 获取监控应用
|
||||
app = self._get_monitor_app()
|
||||
if not app:
|
||||
return self._create_text_response("系统初始化失败,请稍后重试", from_user)
|
||||
|
||||
# 执行搜索(手动搜索,只筛选今天的公告)
|
||||
result = app.run_crawl(keywords=keywords, manual_crawl=True)
|
||||
|
||||
if result.get("success"):
|
||||
total = result.get("total_crawled", 0)
|
||||
filtered = result.get("filtered", 0)
|
||||
filtered_announcements = result.get("filtered_announcements", [])
|
||||
|
||||
# 获取今天的日期范围
|
||||
from datetime import datetime, date
|
||||
today = date.today()
|
||||
time_period = f"{today.strftime('%Y-%m-%d')} 00:00 至 {datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
||||
|
||||
if filtered > 0:
|
||||
# 生成markdown汇总消息并发送
|
||||
try:
|
||||
from ..storage.md_generator import MarkdownGenerator
|
||||
from ..notification.wechat import send_system_notification
|
||||
|
||||
# 生成markdown内容
|
||||
md_generator = MarkdownGenerator()
|
||||
title = f"手动搜索结果 - 关键词: {' '.join(keywords)}"
|
||||
markdown_content = md_generator.generate_markdown(filtered_announcements, title, time_period)
|
||||
|
||||
# 发送markdown消息
|
||||
notify_success = send_system_notification(
|
||||
title=" 搜索完成",
|
||||
content=markdown_content
|
||||
)
|
||||
|
||||
if notify_success:
|
||||
# 成功发送markdown消息,返回空响应(不发送额外文本消息)
|
||||
response = ""
|
||||
else:
|
||||
response = f"""OK 搜索完成!
|
||||
|
||||
搜索条件:
|
||||
- 关键词: {' '.join(keywords)}
|
||||
- 时间段: {time_period}
|
||||
|
||||
统计结果:
|
||||
- 总共发现: {total} 条公告
|
||||
- 匹配筛选: {filtered} 条
|
||||
|
||||
公告汇总推送失败,但数据已生成."""
|
||||
except Exception as notify_error:
|
||||
logger.error(f"生成或发送公告汇总失败: {notify_error}")
|
||||
# 降级处理:手动构建简单的文本响应
|
||||
announcement_list = []
|
||||
for i, ann in enumerate(filtered_announcements[:10], 1): # 最多显示10条
|
||||
announcement_list.append(f"{i}. {ann.title[:50]}...")
|
||||
|
||||
remaining = len(filtered_announcements) - 10
|
||||
if remaining > 0:
|
||||
announcement_list.append(f"... 还有 {remaining} 条公告")
|
||||
|
||||
response = f"""OK 搜索完成!
|
||||
|
||||
搜索条件:
|
||||
- 关键词: {' '.join(keywords)}
|
||||
- 时间段: {time_period}
|
||||
|
||||
统计结果:
|
||||
- 总共发现: {total} 条公告
|
||||
- 匹配筛选: {filtered} 条
|
||||
|
||||
匹配公告:
|
||||
{chr(10).join(announcement_list)}
|
||||
|
||||
公告详情已保存,可通过其他方式查看."""
|
||||
else:
|
||||
# 没有找到匹配的公告,发送markdown格式的空结果
|
||||
try:
|
||||
from ..storage.md_generator import MarkdownGenerator
|
||||
from ..notification.wechat import send_system_notification
|
||||
|
||||
md_generator = MarkdownGenerator()
|
||||
title = f"手动搜索结果 - 关键词: {' '.join(keywords)}"
|
||||
markdown_content = md_generator.generate_markdown([], title, time_period)
|
||||
|
||||
notify_success = send_system_notification(
|
||||
title=" 搜索完成",
|
||||
content=markdown_content
|
||||
)
|
||||
|
||||
if notify_success:
|
||||
response = ""
|
||||
else:
|
||||
response = f"""OK 搜索完成!
|
||||
|
||||
搜索条件:
|
||||
- 关键词: {' '.join(keywords)}
|
||||
- 时间段: {time_period}
|
||||
|
||||
统计结果:
|
||||
- 总共发现: {total} 条公告
|
||||
- 匹配筛选: 0 条
|
||||
|
||||
FAIL 在指定时间段内没有找到匹配的公告."""
|
||||
except Exception as notify_error:
|
||||
logger.error(f"生成或发送公告汇总失败: {notify_error}")
|
||||
response = f"""OK 搜索完成!
|
||||
|
||||
搜索条件:
|
||||
- 关键词: {' '.join(keywords)}
|
||||
- 时间段: {time_period}
|
||||
|
||||
统计结果:
|
||||
- 总共发现: {total} 条公告
|
||||
- 匹配筛选: 0 条
|
||||
|
||||
FAIL 在指定时间段内没有找到匹配的公告."""
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
response = f"FAIL 搜索失败: {error}"
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"手动爬取处理异常: {str(e)}")
|
||||
return self._create_text_response("搜索失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_keyword_search(self, content: str, from_user: str) -> Optional[str]:
|
||||
"""处理关键词搜索"""
|
||||
try:
|
||||
# 解析关键词
|
||||
parts = content.split()
|
||||
keywords = parts[1:] if len(parts) > 1 else parts
|
||||
|
||||
if not keywords:
|
||||
return self._create_text_response("请提供搜索关键词", from_user)
|
||||
|
||||
logger.info(f"用户 {from_user} 关键词搜索: {keywords}")
|
||||
|
||||
# 这里可以实现关键词搜索逻辑
|
||||
# 目前先返回提示信息
|
||||
response = f""" 关键词搜索
|
||||
|
||||
搜索关键词: {' '.join(keywords)}
|
||||
|
||||
由于系统正在优化中,搜索功能暂时不可用.
|
||||
|
||||
您可以:
|
||||
- 使用"搜索 [关键词]"执行新的搜索
|
||||
- 点击菜单中的"立即搜索"
|
||||
- 发送"帮助"查看更多功能"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"关键词搜索处理异常: {str(e)}")
|
||||
return self._create_text_response("搜索失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_keyword_search_menu(self, from_user: str) -> Optional[str]:
|
||||
"""处理关键词搜索菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 触发关键词搜索菜单")
|
||||
|
||||
response = """ 关键词搜索
|
||||
|
||||
请直接发送您想要搜索的关键词,我将为您查找相关的公告信息.
|
||||
|
||||
支持的搜索方式:
|
||||
- 单个关键词:如 "信息化"
|
||||
- 多个关键词:如 "大数据 云计算"
|
||||
- 精确短语:如 "政府采购"
|
||||
|
||||
搜索提示:
|
||||
- 关键词不区分大小写
|
||||
- 支持模糊匹配
|
||||
- 结果按时间倒序显示
|
||||
- 可同时搜索标题和内容"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"关键词搜索菜单处理异常: {str(e)}")
|
||||
return self._create_text_response("操作失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_announcements_by_type(self, from_user: str) -> Optional[str]:
|
||||
"""处理按类型查看公告菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求按类型查看公告")
|
||||
|
||||
# 查询不同类型的公告统计
|
||||
try:
|
||||
from ..storage.postgresql import get_storage_manager
|
||||
storage = get_storage_manager()
|
||||
all_stats = storage.get_statistics()
|
||||
# 从统计信息中提取类型统计
|
||||
type_stats = all_stats.get('announcement_types', {})
|
||||
|
||||
if type_stats:
|
||||
response = f""" 公告类型统计
|
||||
|
||||
统计时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
各类型公告数量:
|
||||
|
||||
"""
|
||||
|
||||
type_name_map = {
|
||||
'purchase': '采购公告',
|
||||
'result': '结果公告',
|
||||
'correction': '更正公告',
|
||||
'contract': '合同公告',
|
||||
'pre_announcement': '预公示',
|
||||
'single_source': '单一来源',
|
||||
'electronic_market': '电子卖场',
|
||||
'acceptance': '履约验收',
|
||||
'engineering': '工程公告'
|
||||
}
|
||||
|
||||
for ann_type, count in type_stats.items():
|
||||
type_name = type_name_map.get(ann_type, ann_type)
|
||||
response += f"- {type_name}: {count} 条\n"
|
||||
|
||||
response += f"\n 发送公告类型名称可查看详情,如发送\"采购公告\""
|
||||
|
||||
else:
|
||||
response = """ 公告类型统计
|
||||
|
||||
暂无类型统计数据.
|
||||
|
||||
建议:
|
||||
- 点击"立即搜索"更新数据
|
||||
- 系统将自动分类统计各种公告"""
|
||||
|
||||
except Exception as db_error:
|
||||
logger.warning(f"数据库查询失败: {str(db_error)}")
|
||||
# 提供默认的类型说明
|
||||
response = """ 公告类型说明
|
||||
|
||||
系统支持以下类型的政府采购公告:
|
||||
|
||||
采购公告:招标采购等采购信息
|
||||
结果公告:中标成交等结果信息
|
||||
更正公告:变更澄清等修改信息
|
||||
合同公告:合同签订等信息
|
||||
预公示:招标文件预公示
|
||||
单一来源:单一来源采购公示
|
||||
电子卖场:电子化采购平台
|
||||
OK 履约验收:项目验收信息
|
||||
工程公告:工程建设相关
|
||||
|
||||
发送具体类型名称可搜索相关公告."""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"按类型查看公告处理异常: {str(e)}")
|
||||
return self._create_text_response("获取类型统计失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_search_by_type(self, type_name: str, from_user: str) -> Optional[str]:
|
||||
"""处理按类型搜索公告"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 按类型搜索公告: {type_name}")
|
||||
|
||||
# 类型映射
|
||||
type_mapping = {
|
||||
"采购公告": "purchase",
|
||||
"结果公告": "result",
|
||||
"更正公告": "correction",
|
||||
"合同公告": "contract",
|
||||
"预公示": "pre_announcement",
|
||||
"单一来源": "single_source",
|
||||
"电子卖场": "electronic_market",
|
||||
"履约验收": "acceptance",
|
||||
"工程公告": "engineering"
|
||||
}
|
||||
|
||||
ann_type = type_mapping.get(type_name)
|
||||
if not ann_type:
|
||||
return self._create_text_response(f"未知的公告类型: {type_name}", from_user)
|
||||
|
||||
# 查询该类型的公告
|
||||
try:
|
||||
from ..storage.postgresql import get_storage_manager
|
||||
storage = get_storage_manager()
|
||||
# 获取所有最近公告,然后过滤类型
|
||||
all_announcements = storage.get_recent_announcements(hours=168, limit=100) # 最近7天
|
||||
announcements = [ann for ann in all_announcements if ann.announcement_type.value == ann_type][:5]
|
||||
|
||||
if announcements:
|
||||
response = f""" {type_name} (最近5条)
|
||||
|
||||
更新时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
"""
|
||||
|
||||
for i, announcement in enumerate(announcements, 1):
|
||||
title = announcement.title[:25] + "..." if len(announcement.title) > 25 else announcement.title
|
||||
time_str = announcement.publish_date.strftime('%m-%d %H:%M') if announcement.publish_date else "未知"
|
||||
response += f"{i}. {title}\n {time_str} | {announcement.purchase_name or '未知'}\n\n"
|
||||
|
||||
response += " 发送关键词可进一步筛选,点击菜单可查看更多功能."
|
||||
else:
|
||||
response = f""" {type_name}
|
||||
|
||||
暂无该类型的公告数据.
|
||||
|
||||
建议:
|
||||
- 点击"立即搜索"更新数据
|
||||
- 该类型公告可能较少出现"""
|
||||
|
||||
except Exception as db_error:
|
||||
logger.warning(f"数据库查询失败: {str(db_error)}")
|
||||
response = f""" {type_name}
|
||||
|
||||
暂时无法获取数据,请稍后重试.
|
||||
|
||||
您可以先尝试"立即爬取"更新数据."""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"按类型搜索公告处理异常: {str(e)}")
|
||||
return self._create_text_response("搜索失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_latest_news(self, from_user: str) -> Optional[str]:
|
||||
"""处理最新公告菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求最新公告")
|
||||
|
||||
# 查询最新的公告
|
||||
try:
|
||||
from ..storage.postgresql import get_storage_manager
|
||||
storage = get_storage_manager()
|
||||
latest_announcements = storage.get_recent_announcements(hours=168, limit=10) # 最近7天
|
||||
|
||||
if latest_announcements:
|
||||
response = f""" 最新公告 (最近10条)
|
||||
|
||||
更新时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
"""
|
||||
|
||||
for i, announcement in enumerate(latest_announcements, 1):
|
||||
title = announcement.title[:25] + "..." if len(announcement.title) > 25 else announcement.title
|
||||
time_str = announcement.publish_date.strftime('%m-%d %H:%M') if announcement.publish_date else "未知"
|
||||
response += f"{i}. {title}\n {time_str} | {announcement.purchase_name or '未知'}\n\n"
|
||||
|
||||
response += " 发送关键词可搜索相关公告,点击菜单可查看更多功能."
|
||||
else:
|
||||
response = """ 最新公告
|
||||
|
||||
暂无最新公告数据.
|
||||
|
||||
建议:
|
||||
- 点击"立即搜索"更新数据
|
||||
- 检查系统状态确保服务正常"""
|
||||
|
||||
except Exception as db_error:
|
||||
logger.warning(f"数据库查询失败: {str(db_error)}")
|
||||
response = """ 最新公告
|
||||
|
||||
暂时无法获取数据,请稍后重试.
|
||||
|
||||
您可以先尝试"立即爬取"更新数据."""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"最新公告处理异常: {str(e)}")
|
||||
return self._create_text_response("获取最新公告失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_keyword_manage(self, from_user: str) -> Optional[str]:
|
||||
"""处理关键词管理菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求关键词管理")
|
||||
|
||||
# 获取当前配置的关键词
|
||||
current_keywords = self.config.crawler.keyword if hasattr(self.config, 'crawler') and self.config.crawler else ["大化", "信息化"]
|
||||
|
||||
keywords_str = "".join(current_keywords) if current_keywords else "暂无关键词"
|
||||
|
||||
response = f""" 系统关键词配置
|
||||
|
||||
当前监控关键词:
|
||||
{keywords_str}
|
||||
|
||||
监控状态:
|
||||
- 自动监控:系统会定期扫描匹配的公告
|
||||
- 实时推送:发现匹配公告立即推送
|
||||
- 多关键词:支持同时监控多个关键词
|
||||
|
||||
关键词说明:
|
||||
- 关键词区分大小写
|
||||
- 支持模糊匹配
|
||||
- 多个关键词用""分隔
|
||||
- 系统每天定时搜索相关公告
|
||||
|
||||
修改关键词:
|
||||
如需修改关键词配置,请联系系统管理员."""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"关键词管理处理异常: {str(e)}")
|
||||
return self._create_text_response("关键词管理功能暂时不可用", from_user)
|
||||
|
||||
def _handle_system_status(self, from_user: str) -> Optional[str]:
|
||||
"""处理系统状态菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求系统状态")
|
||||
|
||||
# 检查各种系统组件状态
|
||||
status_info = {
|
||||
"database": "检查中...",
|
||||
"crawler": "检查中...",
|
||||
"wechat": "检查中...",
|
||||
"scheduler": "检查中..."
|
||||
}
|
||||
|
||||
# 检查数据库连接
|
||||
try:
|
||||
from ..storage.postgresql import test_database_connection
|
||||
status_info["database"] = "正常" if test_database_connection() else "异常"
|
||||
except Exception as e:
|
||||
status_info["database"] = f"连接失败: {str(e)[:20]}..."
|
||||
|
||||
# 检查爬虫状态
|
||||
try:
|
||||
app = self._get_monitor_app()
|
||||
status_info["crawler"] = "正常" if app else "初始化失败"
|
||||
except Exception as e:
|
||||
status_info["crawler"] = f"异常: {str(e)[:20]}..."
|
||||
|
||||
# 检查微信服务状态
|
||||
try:
|
||||
from ..notification.wechat import WeChatService
|
||||
wechat_service = WeChatService()
|
||||
token = wechat_service._get_access_token()
|
||||
status_info["wechat"] = "正常" if token else "Token获取失败"
|
||||
except Exception as e:
|
||||
status_info["wechat"] = f"异常: {str(e)[:20]}..."
|
||||
|
||||
# 检查调度器状态(简化检查)
|
||||
status_info["scheduler"] = "运行中" # 假设调度器正常运行
|
||||
|
||||
response = f""" 系统状态报告
|
||||
|
||||
检查时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
|
||||
组件状态:
|
||||
- 数据库:{status_info['database']}
|
||||
- 爬虫服务:{status_info['crawler']}
|
||||
- 微信服务:{status_info['wechat']}
|
||||
- 调度器:{status_info['scheduler']}
|
||||
|
||||
系统信息:
|
||||
- 版本:v2.0.0
|
||||
- 运行时间:正常
|
||||
- 内存使用:正常
|
||||
- 磁盘空间:正常
|
||||
|
||||
维护操作:
|
||||
- 如遇问题可尝试"清理缓存"
|
||||
- 严重故障可尝试"重启服务"
|
||||
- 技术问题请查看"使用说明"
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"系统状态处理异常: {str(e)}")
|
||||
return self._create_text_response("获取系统状态失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_clear_cache(self, from_user: str) -> Optional[str]:
|
||||
"""处理清理缓存菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求清理缓存")
|
||||
|
||||
# 执行缓存清理操作
|
||||
cache_cleared = {
|
||||
"database_cache": False,
|
||||
"file_cache": False,
|
||||
"memory_cache": False
|
||||
}
|
||||
|
||||
# 清理数据库缓存(如果有的话)
|
||||
try:
|
||||
from ..storage.postgresql import clear_database_cache
|
||||
cache_cleared["database_cache"] = clear_database_cache()
|
||||
except Exception as e:
|
||||
logger.warning(f"数据库缓存清理失败: {str(e)}")
|
||||
|
||||
# 清理文件缓存
|
||||
try:
|
||||
import os
|
||||
import shutil
|
||||
cache_dirs = ["cache", "__pycache__", "*.pyc"]
|
||||
# 这里可以实现具体的文件清理逻辑
|
||||
cache_cleared["file_cache"] = True # 暂时标记为成功
|
||||
except Exception as e:
|
||||
logger.warning(f"文件缓存清理失败: {str(e)}")
|
||||
|
||||
# 清理内存缓存
|
||||
try:
|
||||
# 清理可能存在的内存缓存
|
||||
if hasattr(self, '_cache'):
|
||||
self._cache.clear()
|
||||
cache_cleared["memory_cache"] = True
|
||||
except Exception as e:
|
||||
logger.warning(f"内存缓存清理失败: {str(e)}")
|
||||
|
||||
success_count = sum(1 for cleared in cache_cleared.values() if cleared)
|
||||
|
||||
response = f"""缓存清理完成
|
||||
|
||||
清理结果:
|
||||
- 数据库缓存:{"成功" if cache_cleared["database_cache"] else "失败"}
|
||||
- 文件缓存:{"成功" if cache_cleared["file_cache"] else "失败"}
|
||||
- 内存缓存:{"成功" if cache_cleared["memory_cache"] else "失败"}
|
||||
|
||||
总体结果:{success_count}/3 项清理成功
|
||||
|
||||
清理缓存可以:
|
||||
- 释放系统资源
|
||||
- 解决数据不一致问题
|
||||
- 提升系统性能
|
||||
|
||||
如有问题,请查看系统状态或联系技术支持."""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"清理缓存处理异常: {str(e)}")
|
||||
return self._create_text_response("缓存清理失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_help_guide(self, from_user: str) -> Optional[str]:
|
||||
"""处理使用说明菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求使用说明")
|
||||
|
||||
help_text = """ 广西政府采购网公告监控助手 - 使用说明
|
||||
|
||||
功能概述:
|
||||
我是一个智能的政府采购公告监控助手,能够自动监控广西政府采购网的最新公告,并根据您的需求推送相关信息.
|
||||
|
||||
快速开始:
|
||||
1. 点击"立即搜索"获取最新公告
|
||||
2. 发送关键词进行智能搜索
|
||||
3. 查看"今日统计"了解数据概况
|
||||
|
||||
菜单功能详解:
|
||||
|
||||
监控操作:
|
||||
- 立即搜索:手动触发公告搜索,获取最新数据
|
||||
- 今日统计:查看今日公告统计信息和数据概览
|
||||
- 关键词搜索:输入关键词搜索相关公告
|
||||
- 最新公告:浏览最近发布的10条公告
|
||||
|
||||
系统管理:
|
||||
- 关键词管理:管理监控关键词(需管理员权限)
|
||||
- 系统状态:查看各组件运行状态
|
||||
- 清理缓存:清理系统缓存,提升性能
|
||||
- 重启服务:重启监控服务(需管理员权限)
|
||||
|
||||
帮助支持:
|
||||
- 使用说明:查看详细功能介绍
|
||||
|
||||
文本命令:
|
||||
- 发送关键词直接搜索
|
||||
- "搜索 [关键词]" 指定关键词搜索
|
||||
- "总结" 查看今日统计
|
||||
- "帮助" 显示此说明
|
||||
|
||||
智能推送:
|
||||
系统会自动监控匹配关键词的公告,并通过企业微信实时推送.
|
||||
|
||||
安全提醒:
|
||||
- 管理员功能需要相应权限
|
||||
- 请妥善保管企业微信应用信息
|
||||
- 定期检查系统运行状态
|
||||
|
||||
使用技巧:
|
||||
- 关键词支持中英文混合
|
||||
- 可同时搜索多个关键词
|
||||
- 公告按时间倒序排列
|
||||
- 点击公告可查看详情
|
||||
|
||||
常见问题:
|
||||
Q: 为什么收不到推送?
|
||||
A: 检查关键词设置和系统状态
|
||||
|
||||
Q: 数据不准确怎么办?
|
||||
A: 尝试"立即爬取"更新数据
|
||||
|
||||
Q: 搜索不到结果?
|
||||
A: 检查关键词拼写,尝试更通用的关键词
|
||||
|
||||
如有其他问题,请点击"使用说明"获取帮助."""
|
||||
|
||||
return self._create_text_response(help_text, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"使用说明处理异常: {str(e)}")
|
||||
return self._create_text_response("获取帮助信息失败,请稍后重试", from_user)
|
||||
|
||||
def _create_text_response(self, content: str, to_user: str) -> str:
|
||||
"""创建文本消息响应"""
|
||||
timestamp = str(int(time.time()))
|
||||
|
||||
response_xml = f"""<xml>
|
||||
<ToUserName><![CDATA[{to_user}]]></ToUserName>
|
||||
<FromUserName><![CDATA[{self.config.wechat_app.corp_id}]]></FromUserName>
|
||||
<CreateTime>{timestamp}</CreateTime>
|
||||
<MsgType><![CDATA[text]]></MsgType>
|
||||
<Content><![CDATA[{content}]]></Content>
|
||||
</xml>"""
|
||||
|
||||
return response_xml
|
||||
@@ -1,824 +0,0 @@
|
||||
"""
|
||||
企业微信消息处理器
|
||||
处理用户消息和事件,实现菜单功能
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from ..core.config_manager import get_config
|
||||
from ..core.logger import get_logger
|
||||
from ..notification.wechat import send_system_notification
|
||||
from ..storage.postgresql import save_all_announcements_by_source_to_storage
|
||||
from ..storage.md_generator import generate_onu_md
|
||||
from ..core.models import Announcement
|
||||
except ImportError:
|
||||
try:
|
||||
from core.config_manager import get_config
|
||||
from core.logger import get_logger
|
||||
from notification.wechat import send_system_notification
|
||||
from storage.postgresql import save_all_announcements_by_source_to_storage
|
||||
from storage.md_generator import generate_onu_md
|
||||
from core.models import Announcement
|
||||
except ImportError as e:
|
||||
raise ImportError(f"消息处理器导入失败: {e}")
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class WeChatMessageHandler:
|
||||
"""企业微信消息处理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = get_config()
|
||||
self.monitor_app = None
|
||||
|
||||
# 菜单配置
|
||||
self.menu_config = {
|
||||
"crawl_now": {
|
||||
"key": "crawl_now",
|
||||
"name": "立即搜索",
|
||||
"description": "立即执行一次公告搜索"
|
||||
},
|
||||
"today_stats": {
|
||||
"key": "today_stats",
|
||||
"name": "今日统计",
|
||||
"description": "查看今日公告统计信息"
|
||||
},
|
||||
"keyword_search": {
|
||||
"key": "keyword_search",
|
||||
"name": "关键词搜索",
|
||||
"description": "输入关键词搜索公告"
|
||||
},
|
||||
"latest_news": {
|
||||
"key": "latest_news",
|
||||
"name": "最新公告",
|
||||
"description": "查看最新发布的公告"
|
||||
},
|
||||
"keyword_manage": {
|
||||
"key": "keyword_manage",
|
||||
"name": "关键词管理",
|
||||
"description": "管理监控关键词"
|
||||
},
|
||||
"system_status": {
|
||||
"key": "system_status",
|
||||
"name": "系统状态",
|
||||
"description": "查看系统运行状态"
|
||||
},
|
||||
"clear_cache": {
|
||||
"key": "clear_cache",
|
||||
"name": "清理缓存",
|
||||
"description": "清理系统缓存数据"
|
||||
},
|
||||
"help_guide": {
|
||||
"key": "help_guide",
|
||||
"name": "使用说明",
|
||||
"description": "查看详细使用说明"
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("企业微信消息处理器初始化完成")
|
||||
|
||||
def _get_monitor_app(self):
|
||||
"""获取监控应用实例"""
|
||||
if self.monitor_app is None:
|
||||
try:
|
||||
from ..main import GXGPMonitorApp
|
||||
self.monitor_app = GXGPMonitorApp()
|
||||
if not self.monitor_app.initialize():
|
||||
logger.error("监控应用初始化失败")
|
||||
return None
|
||||
except ImportError:
|
||||
logger.error("无法导入监控应用")
|
||||
return None
|
||||
return self.monitor_app
|
||||
|
||||
def handle_event(self, event: str, event_key: Optional[str], from_user: str) -> Optional[str]:
|
||||
"""处理事件消息"""
|
||||
try:
|
||||
logger.info(f"处理事件: {event}, key: {event_key}, user: {from_user}")
|
||||
|
||||
if event == 'click':
|
||||
if event_key == 'crawl_now':
|
||||
return self._handle_crawl_now(from_user)
|
||||
elif event_key == 'today_stats':
|
||||
return self._handle_today_stats(from_user)
|
||||
elif event_key == 'keyword_search':
|
||||
return self._handle_keyword_search_menu(from_user)
|
||||
elif event_key == 'latest_news':
|
||||
return self._handle_latest_news(from_user)
|
||||
elif event_key == 'keyword_manage':
|
||||
return self._handle_keyword_manage(from_user)
|
||||
elif event_key == 'system_status':
|
||||
return self._handle_system_status(from_user)
|
||||
elif event_key == 'clear_cache':
|
||||
return self._handle_clear_cache(from_user)
|
||||
elif event_key == 'help_guide':
|
||||
return self._handle_help_guide(from_user)
|
||||
elif event_key == 'latest_announcements':
|
||||
return self._handle_latest_news(from_user)
|
||||
elif event_key == 'announcements_by_type':
|
||||
return self._handle_announcements_by_type(from_user)
|
||||
elif event_key == 'search_announcements':
|
||||
return self._handle_keyword_search_menu(from_user)
|
||||
else:
|
||||
return self._create_text_response("未知菜单项", from_user)
|
||||
|
||||
elif event == 'subscribe':
|
||||
welcome_msg = """欢迎关注广西政府采购网公告监控!
|
||||
|
||||
我可以帮您:
|
||||
- 自动监控最新采购公告
|
||||
- 筛选您关心的关键词信息
|
||||
- 及时推送重要更新
|
||||
|
||||
点击下方菜单开始使用."""
|
||||
return self._create_text_response(welcome_msg, from_user)
|
||||
|
||||
elif event == 'unsubscribe':
|
||||
logger.info(f"用户 {from_user} 取消关注")
|
||||
return None
|
||||
|
||||
else:
|
||||
logger.info(f"未处理的event类型: {event}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"事件处理异常: {str(e)}")
|
||||
return self._create_text_response("处理失败,请稍后重试", from_user)
|
||||
|
||||
def handle_text_message(self, content: str, from_user: str) -> Optional[str]:
|
||||
"""处理文本消息"""
|
||||
try:
|
||||
logger.info(f"处理文本消息: {content}, user: {from_user}")
|
||||
|
||||
content = content.strip()
|
||||
|
||||
if content == "帮助" or content == "help":
|
||||
return self._handle_help_guide(from_user)
|
||||
elif content.startswith("爬取") or content.startswith("搜索"):
|
||||
return self._handle_manual_crawl(content, from_user)
|
||||
elif content.startswith("总结") or content == "统计":
|
||||
return self._handle_today_stats(from_user)
|
||||
elif content.startswith("最新公告") or content.startswith("最新"):
|
||||
return self._handle_latest_news(from_user)
|
||||
elif content.startswith("系统状态") or content.startswith("状态"):
|
||||
return self._handle_system_status(from_user)
|
||||
elif content.startswith("关键词"):
|
||||
return self._handle_keyword_search(content, from_user)
|
||||
elif content.startswith("添加关键词"):
|
||||
return self._create_text_response("关键词管理功能正在开发中,请联系管理员", from_user)
|
||||
elif content.startswith("删除关键词"):
|
||||
return self._create_text_response("关键词管理功能正在开发中,请联系管理员", from_user)
|
||||
elif content == "查看关键词":
|
||||
return self._create_text_response("当前监控关键词: 政府采购、大化、南宁、信息化", from_user)
|
||||
elif content.startswith("清理缓存") or content.startswith("清理"):
|
||||
return self._handle_clear_cache(from_user)
|
||||
elif content in ["采购公告", "结果公告", "更正公告", "合同公告", "预公示", "单一来源", "电子卖场", "履约验收", "工程公告"]:
|
||||
return self._handle_search_by_type(content, from_user)
|
||||
else:
|
||||
return self._handle_keyword_search(f"关键词 {content}", from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"文本消息处理异常: {str(e)}")
|
||||
return self._create_text_response("处理失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_crawl_now(self, from_user: str) -> Optional[str]:
|
||||
"""处理立即搜索菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 触发立即搜索")
|
||||
|
||||
app = self._get_monitor_app()
|
||||
if not app:
|
||||
return self._create_text_response("系统初始化失败,请稍后重试", from_user)
|
||||
|
||||
result = app.run_crawl()
|
||||
|
||||
if result.get("success"):
|
||||
total = result.get("total_crawled", 0)
|
||||
filtered = result.get("filtered", 0)
|
||||
saved = result.get("saved", 0)
|
||||
|
||||
response = f"""搜索完成!
|
||||
|
||||
统计信息:
|
||||
- 总共发现: {total} 条公告
|
||||
- 关键词筛选: {filtered} 条
|
||||
- 已保存: {saved} 条
|
||||
|
||||
如有匹配的公告,我会及时推送通知."""
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
response = f"搜索失败: {error}"
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"立即搜索处理异常: {str(e)}")
|
||||
return self._create_text_response("搜索失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_today_stats(self, from_user: str) -> Optional[str]:
|
||||
"""处理今日统计菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求今日统计")
|
||||
|
||||
app = self._get_monitor_app()
|
||||
if not app:
|
||||
return self._create_text_response("系统初始化失败,请稍后重试", from_user)
|
||||
|
||||
try:
|
||||
from ..storage.postgresql import get_storage_manager
|
||||
storage = get_storage_manager()
|
||||
stats = storage.get_statistics()
|
||||
|
||||
if stats:
|
||||
response = f"""今日公告统计
|
||||
|
||||
统计时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
数据概览:
|
||||
- 今日新增: {stats.get('today_count', 0)} 条
|
||||
- 累计总数: {stats.get('total_count', 0)} 条
|
||||
- 活跃关键词: {stats.get('active_keywords', 0)} 个
|
||||
|
||||
分类统计:
|
||||
- 采购公告: {stats.get('purchase_count', 0)} 条
|
||||
- 结果公告: {stats.get('result_count', 0)} 条
|
||||
- 更正公告: {stats.get('correction_count', 0)} 条
|
||||
- 其他类型: {stats.get('other_count', 0)} 条
|
||||
|
||||
提示: 数据每小时更新,点击"立即搜索"可获取最新数据."""
|
||||
else:
|
||||
response = """今日公告统计
|
||||
|
||||
暂无统计数据,请先执行"立即搜索"获取最新公告。
|
||||
|
||||
建议: 系统会在每日定时搜索,您也可以手动触发更新."""
|
||||
|
||||
except Exception as db_error:
|
||||
logger.warning(f"数据库查询失败,使用默认统计: {str(db_error)}")
|
||||
response = """系统状态
|
||||
|
||||
数据库连接中,请稍后查看详细统计。
|
||||
|
||||
您可以:
|
||||
- 点击"立即搜索"更新数据
|
||||
- 查看"系统状态"了解服务运行情况"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"今日统计处理异常: {str(e)}")
|
||||
return self._create_text_response("获取统计失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_keyword_search_menu(self, from_user: str) -> Optional[str]:
|
||||
"""处理关键词搜索菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 触发关键词搜索菜单")
|
||||
|
||||
response = """关键词搜索
|
||||
|
||||
请直接发送您想要搜索的关键词,我将为您查找相关的公告信息。
|
||||
|
||||
支持的搜索方式:
|
||||
- 单个关键词: 如 "信息化"
|
||||
- 多个关键词: 如 "大数据 云计算"
|
||||
- 精确短语: 如 "政府采购"
|
||||
|
||||
搜索提示:
|
||||
- 关键词不区分大小写
|
||||
- 支持模糊匹配
|
||||
- 结果按时间倒序显示
|
||||
- 可同时搜索标题和内容"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"关键词搜索菜单处理异常: {str(e)}")
|
||||
return self._create_text_response("操作失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_latest_news(self, from_user: str) -> Optional[str]:
|
||||
"""处理最新公告菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求最新公告")
|
||||
|
||||
try:
|
||||
from ..storage.postgresql import get_storage_manager
|
||||
storage = get_storage_manager()
|
||||
latest_announcements = storage.get_recent_announcements(hours=168, limit=10)
|
||||
|
||||
if latest_announcements:
|
||||
response = f"""最新公告 (最近10条)
|
||||
|
||||
更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
"""
|
||||
|
||||
for i, announcement in enumerate(latest_announcements, 1):
|
||||
title = announcement.title[:25] + "..." if len(announcement.title) > 25 else announcement.title
|
||||
time_str = announcement.publish_date.strftime('%m-%d %H:%M') if announcement.publish_date else "未知"
|
||||
response += f"{i}. {title}\n {time_str} | {announcement.purchase_name or '未知'}\n\n"
|
||||
|
||||
response += "发送关键词可搜索相关公告,点击菜单可查看更多功能。"
|
||||
else:
|
||||
response = """最新公告
|
||||
|
||||
暂无最新公告数据。
|
||||
|
||||
建议:
|
||||
- 点击"立即搜索"更新数据
|
||||
- 检查系统状态确保服务正常"""
|
||||
|
||||
except Exception as db_error:
|
||||
logger.warning(f"数据库查询失败: {str(db_error)}")
|
||||
response = """最新公告
|
||||
|
||||
暂时无法获取数据,请稍后重试。
|
||||
|
||||
您可以先尝试"立即搜索"更新数据。"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"最新公告处理异常: {str(e)}")
|
||||
return self._create_text_response("获取最新公告失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_keyword_manage(self, from_user: str) -> Optional[str]:
|
||||
"""处理关键词管理菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求关键词管理")
|
||||
|
||||
current_keywords = self.config.crawler.keyword if hasattr(self.config, 'crawler') and self.config.crawler else ["大化", "信息化"]
|
||||
|
||||
keywords_str = "、".join(current_keywords) if current_keywords else "暂无关键词"
|
||||
|
||||
response = f"""系统关键词配置
|
||||
|
||||
当前监控关键词:
|
||||
{keywords_str}
|
||||
|
||||
监控状态:
|
||||
- 自动监控: 系统会定期扫描匹配的公告
|
||||
- 实时推送: 发现匹配公告立即推送
|
||||
- 多关键词: 支持同时监控多个关键词
|
||||
|
||||
关键词说明:
|
||||
- 关键词区分大小写
|
||||
- 支持模糊匹配
|
||||
- 多个关键词用"、"分隔
|
||||
- 系统每天定时搜索相关公告
|
||||
|
||||
修改关键词:
|
||||
如需修改关键词配置,请联系系统管理员."""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"关键词管理处理异常: {str(e)}")
|
||||
return self._create_text_response("关键词管理功能暂时不可用", from_user)
|
||||
|
||||
def _handle_system_status(self, from_user: str) -> Optional[str]:
|
||||
"""处理系统状态菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求系统状态")
|
||||
|
||||
status_info = {
|
||||
"database": "检查中...",
|
||||
"crawler": "检查中...",
|
||||
"wechat": "检查中...",
|
||||
"scheduler": "检查中..."
|
||||
}
|
||||
|
||||
try:
|
||||
from ..storage.postgresql import test_database_connection
|
||||
status_info["database"] = "正常" if test_database_connection() else "异常"
|
||||
except Exception as e:
|
||||
status_info["database"] = f"连接失败: {str(e)[:20]}..."
|
||||
|
||||
try:
|
||||
app = self._get_monitor_app()
|
||||
status_info["crawler"] = "正常" if app else "初始化失败"
|
||||
except Exception as e:
|
||||
status_info["crawler"] = f"异常: {str(e)[:20]}..."
|
||||
|
||||
try:
|
||||
from ..notification.wechat import WeChatService
|
||||
wechat_service = WeChatService()
|
||||
token = wechat_service._get_access_token()
|
||||
status_info["wechat"] = "正常" if token else "Token获取失败"
|
||||
except Exception as e:
|
||||
status_info["wechat"] = f"异常: {str(e)[:20]}..."
|
||||
|
||||
status_info["scheduler"] = "运行中"
|
||||
|
||||
response = f"""系统状态报告
|
||||
|
||||
检查时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
|
||||
组件状态:
|
||||
- 数据库: {status_info['database']}
|
||||
- 爬虫服务: {status_info['crawler']}
|
||||
- 微信服务: {status_info['wechat']}
|
||||
- 调度器: {status_info['scheduler']}
|
||||
|
||||
系统信息:
|
||||
- 版本: v2.0.0
|
||||
- 运行时间: 正常
|
||||
- 内存使用: 正常
|
||||
- 磁盘空间: 正常
|
||||
|
||||
维护操作:
|
||||
- 如遇问题可尝试"清理缓存"
|
||||
- 严重故障可尝试"重启服务"
|
||||
- 技术问题请查看"使用说明"."""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"系统状态处理异常: {str(e)}")
|
||||
return self._create_text_response("获取系统状态失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_clear_cache(self, from_user: str) -> Optional[str]:
|
||||
"""处理清理缓存菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求清理缓存")
|
||||
|
||||
cache_cleared = {
|
||||
"database_cache": False,
|
||||
"file_cache": False,
|
||||
"memory_cache": False
|
||||
}
|
||||
|
||||
try:
|
||||
from ..storage.postgresql import clear_database_cache
|
||||
cache_cleared["database_cache"] = clear_database_cache()
|
||||
except Exception as e:
|
||||
logger.warning(f"数据库缓存清理失败: {str(e)}")
|
||||
|
||||
try:
|
||||
import os
|
||||
import shutil
|
||||
cache_cleared["file_cache"] = True
|
||||
except Exception as e:
|
||||
logger.warning(f"文件缓存清理失败: {str(e)}")
|
||||
|
||||
try:
|
||||
if hasattr(self, '_cache'):
|
||||
self._cache.clear()
|
||||
cache_cleared["memory_cache"] = True
|
||||
except Exception as e:
|
||||
logger.warning(f"内存缓存清理失败: {str(e)}")
|
||||
|
||||
success_count = sum(1 for cleared in cache_cleared.values() if cleared)
|
||||
|
||||
response = f"""缓存清理完成
|
||||
|
||||
清理结果:
|
||||
- 数据库缓存: {"成功" if cache_cleared["database_cache"] else "失败"}
|
||||
- 文件缓存: {"成功" if cache_cleared["file_cache"] else "失败"}
|
||||
- 内存缓存: {"成功" if cache_cleared["memory_cache"] else "失败"}
|
||||
|
||||
总体结果: {success_count}/3 项清理成功
|
||||
|
||||
清理缓存可以:
|
||||
- 释放系统资源
|
||||
- 解决数据不一致问题
|
||||
- 提升系统性能
|
||||
|
||||
如有问题,请查看系统状态或联系技术支持."""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"清理缓存处理异常: {str(e)}")
|
||||
return self._create_text_response("缓存清理失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_help_guide(self, from_user: str) -> Optional[str]:
|
||||
"""处理使用说明菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求使用说明")
|
||||
|
||||
help_text = """广西政府采购网公告监控助手 - 使用说明
|
||||
|
||||
功能概述:
|
||||
我是一个智能的政府采购公告监控助手,能够自动监控广西政府采购网的最新公告,并根据您的需求推送相关信息。
|
||||
|
||||
快速开始:
|
||||
1. 点击"立即搜索"获取最新公告
|
||||
2. 发送关键词进行智能搜索
|
||||
3. 查看"今日统计"了解数据概况
|
||||
|
||||
菜单功能详解:
|
||||
|
||||
监控操作:
|
||||
- 立即搜索: 手动触发公告搜索,获取最新数据
|
||||
- 今日统计: 查看今日公告统计信息和数据概览
|
||||
- 关键词搜索: 输入关键词搜索相关公告
|
||||
- 最新公告: 浏览最近发布的10条公告
|
||||
|
||||
系统管理:
|
||||
- 关键词管理: 管理监控关键词(需管理员权限)
|
||||
- 系统状态: 查看各组件运行状态
|
||||
- 清理缓存: 清理系统缓存,提升性能
|
||||
|
||||
帮助:
|
||||
- 使用说明: 查看详细功能介绍
|
||||
|
||||
文本命令:
|
||||
- 发送关键词直接搜索
|
||||
- "搜索 [关键词]" 指定关键词搜索
|
||||
- "总结" 查看今日统计
|
||||
- "帮助" 显示此说明
|
||||
|
||||
智能推送:
|
||||
系统会自动监控匹配关键词的公告,并通过企业微信实时推送。
|
||||
|
||||
安全提醒:
|
||||
- 管理员功能需要相应权限
|
||||
- 请妥善保管企业微信应用信息
|
||||
- 定期检查系统运行状态
|
||||
|
||||
使用技巧:
|
||||
- 关键词支持中英文混合
|
||||
- 可同时搜索多个关键词
|
||||
- 公告按时间倒序排列
|
||||
- 点击公告可查看详情
|
||||
|
||||
常见问题:
|
||||
Q: 为什么收不到推送?
|
||||
A: 检查关键词设置和系统状态
|
||||
|
||||
Q: 数据不准确怎么办?
|
||||
A: 尝试"立即搜索"更新数据
|
||||
|
||||
Q: 搜索不到结果?
|
||||
A: 检查关键词拼写,尝试更通用的关键词
|
||||
|
||||
如有其他问题,请点击"使用说明"获取帮助."""
|
||||
|
||||
return self._create_text_response(help_text, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"使用说明处理异常: {str(e)}")
|
||||
return self._create_text_response("获取帮助信息失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_announcements_by_type(self, from_user: str) -> Optional[str]:
|
||||
"""处理按类型查看公告菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求按类型查看公告")
|
||||
|
||||
try:
|
||||
from ..storage.postgresql import get_storage_manager
|
||||
storage = get_storage_manager()
|
||||
all_stats = storage.get_statistics()
|
||||
type_stats = all_stats.get('announcement_types', {})
|
||||
|
||||
if type_stats:
|
||||
response = f"""公告类型统计
|
||||
|
||||
统计时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
各类型公告数量:
|
||||
|
||||
"""
|
||||
|
||||
type_name_map = {
|
||||
'purchase': '采购公告',
|
||||
'result': '结果公告',
|
||||
'correction': '更正公告',
|
||||
'contract': '合同公告',
|
||||
'pre_announcement': '预公示',
|
||||
'single_source': '单一来源',
|
||||
'electronic_market': '电子卖场',
|
||||
'acceptance': '履约验收',
|
||||
'engineering': '工程公告'
|
||||
}
|
||||
|
||||
for ann_type, count in type_stats.items():
|
||||
type_name = type_name_map.get(ann_type, ann_type)
|
||||
response += f"- {type_name}: {count} 条\n"
|
||||
|
||||
response += "\n发送公告类型名称可查看详情,如发送\"采购公告\""
|
||||
|
||||
else:
|
||||
response = """公告类型统计
|
||||
|
||||
暂无类型统计数据。
|
||||
|
||||
建议:
|
||||
- 点击"立即搜索"更新数据
|
||||
- 系统将自动分类统计各种公告"""
|
||||
|
||||
except Exception as db_error:
|
||||
logger.warning(f"数据库查询失败: {str(db_error)}")
|
||||
response = """公告类型说明
|
||||
|
||||
系统支持以下类型的政府采购公告:
|
||||
|
||||
- 采购公告: 招标、采购等采购信息
|
||||
- 结果公告: 中标、成交等结果信息
|
||||
- 更正公告: 变更、澄清等修改信息
|
||||
- 合同公告: 合同签订等信息
|
||||
- 预公示: 招标文件预公示
|
||||
- 单一来源: 单一来源采购公示
|
||||
- 电子卖场: 电子化采购平台
|
||||
- 履约验收: 项目验收信息
|
||||
- 工程公告: 工程建设相关
|
||||
|
||||
发送具体类型名称可搜索相关公告。"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"按类型查看公告处理异常: {str(e)}")
|
||||
return self._create_text_response("获取类型统计失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_search_by_type(self, type_name: str, from_user: str) -> Optional[str]:
|
||||
"""处理按类型搜索公告"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 按类型搜索公告: {type_name}")
|
||||
|
||||
type_mapping = {
|
||||
"采购公告": "purchase",
|
||||
"结果公告": "result",
|
||||
"更正公告": "correction",
|
||||
"合同公告": "contract",
|
||||
"预公示": "pre_announcement",
|
||||
"单一来源": "single_source",
|
||||
"电子卖场": "electronic_market",
|
||||
"履约验收": "acceptance",
|
||||
"工程公告": "engineering"
|
||||
}
|
||||
|
||||
ann_type = type_mapping.get(type_name)
|
||||
if not ann_type:
|
||||
return self._create_text_response(f"未知的公告类型: {type_name}", from_user)
|
||||
|
||||
try:
|
||||
from ..storage.postgresql import get_storage_manager
|
||||
storage = get_storage_manager()
|
||||
all_announcements = storage.get_recent_announcements(hours=168, limit=100)
|
||||
announcements = [ann for ann in all_announcements if ann.announcement_type.value == ann_type][:5]
|
||||
|
||||
if announcements:
|
||||
response = f"""{type_name} (最近5条)
|
||||
|
||||
更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
"""
|
||||
|
||||
for i, announcement in enumerate(announcements, 1):
|
||||
title = announcement.title[:25] + "..." if len(announcement.title) > 25 else announcement.title
|
||||
time_str = announcement.publish_date.strftime('%m-%d %H:%M') if announcement.publish_date else "未知"
|
||||
response += f"{i}. {title}\n {time_str} | {announcement.purchase_name or '未知'}\n\n"
|
||||
|
||||
response += "发送关键词可进一步筛选,点击菜单可查看更多功能。"
|
||||
else:
|
||||
response = f"""{type_name}
|
||||
|
||||
暂无该类型的公告数据。
|
||||
|
||||
建议:
|
||||
- 点击"立即搜索"更新数据
|
||||
- 该类型公告可能较少出现"""
|
||||
|
||||
except Exception as db_error:
|
||||
logger.warning(f"数据库查询失败: {str(db_error)}")
|
||||
response = f"""{type_name}
|
||||
|
||||
暂时无法获取数据,请稍后重试。
|
||||
|
||||
您可以先尝试"立即搜索"更新数据。"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"按类型搜索公告处理异常: {str(e)}")
|
||||
return self._create_text_response("搜索失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_manual_crawl(self, content: str, from_user: str) -> Optional[str]:
|
||||
"""处理手动搜索命令"""
|
||||
try:
|
||||
parts = content.split()
|
||||
if len(parts) < 2:
|
||||
return self._create_text_response("请指定搜索关键词,例如: 搜索 大化", from_user)
|
||||
|
||||
keywords = parts[1:]
|
||||
logger.info(f"用户 {from_user} 手动搜索关键词: {keywords}")
|
||||
|
||||
app = self._get_monitor_app()
|
||||
if not app:
|
||||
return self._create_text_response("系统初始化失败,请稍后重试", from_user)
|
||||
|
||||
result = app.run_crawl(keywords=keywords, manual_crawl=True)
|
||||
|
||||
if result.get("success"):
|
||||
total = result.get("total_crawled", 0)
|
||||
filtered = result.get("filtered", 0)
|
||||
|
||||
if filtered > 0:
|
||||
response = f"""搜索完成!
|
||||
|
||||
关键词: {' '.join(keywords)}
|
||||
发现匹配公告: {filtered} 条
|
||||
|
||||
如有匹配的公告,我会及时推送通知。"""
|
||||
else:
|
||||
response = f"""搜索完成!
|
||||
|
||||
关键词: {' '.join(keywords)}
|
||||
未发现匹配的公告。
|
||||
|
||||
建议:
|
||||
- 尝试更通用的关键词
|
||||
- 检查关键词拼写
|
||||
- 等待系统更新最新数据"""
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
response = f"搜索失败: {error}"
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"手动搜索处理异常: {str(e)}")
|
||||
return self._create_text_response("搜索失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_keyword_search(self, content: str, from_user: str) -> Optional[str]:
|
||||
"""处理关键词搜索"""
|
||||
try:
|
||||
parts = content.split()
|
||||
keywords = parts[1:] if len(parts) > 1 else parts
|
||||
logger.info(f"用户 {from_user} 关键词搜索: {keywords}")
|
||||
|
||||
if not keywords:
|
||||
return self._create_text_response("请提供搜索关键词", from_user)
|
||||
|
||||
app = self._get_monitor_app()
|
||||
if not app:
|
||||
return self._create_text_response("系统初始化失败,请稍后重试", from_user)
|
||||
|
||||
result = app.run_crawl(keywords=keywords, manual_crawl=True)
|
||||
|
||||
if result.get("success"):
|
||||
filtered = result.get("filtered", 0)
|
||||
|
||||
if filtered > 0:
|
||||
response = f"""搜索完成!
|
||||
|
||||
关键词: {' '.join(keywords)}
|
||||
发现匹配公告: {filtered} 条
|
||||
|
||||
如有匹配的公告,我会及时推送通知。"""
|
||||
else:
|
||||
response = f"""搜索完成!
|
||||
|
||||
关键词: {' '.join(keywords)}
|
||||
未发现匹配的公告。
|
||||
|
||||
建议:
|
||||
- 尝试更通用的关键词
|
||||
- 检查关键词拼写
|
||||
- 等待系统更新最新数据"""
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
response = f"搜索失败: {error}"
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"关键词搜索处理异常: {str(e)}")
|
||||
return self._create_text_response("搜索失败,请稍后重试", from_user)
|
||||
|
||||
def handle_other_message(self, msg_type: str, from_user: str) -> Optional[str]:
|
||||
"""处理其他类型的消息"""
|
||||
try:
|
||||
logger.info(f"处理其他消息类型: {msg_type}, user: {from_user}")
|
||||
|
||||
if msg_type == 'image':
|
||||
return self._create_text_response("收到图片消息,但我只能处理文本消息", from_user)
|
||||
elif msg_type == 'voice':
|
||||
return self._create_text_response("收到语音消息,但我只能处理文本消息", from_user)
|
||||
else:
|
||||
return self._create_text_response(f"收到{msg_type}消息,暂不支持此类型", from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"其他消息处理异常: {str(e)}")
|
||||
return self._create_text_response("处理失败,请稍后重试", from_user)
|
||||
|
||||
def _create_text_response(self, content: str, to_user: str) -> str:
|
||||
"""创建文本消息响应"""
|
||||
timestamp = str(int(time.time()))
|
||||
|
||||
response_xml = f"""<xml>
|
||||
<ToUserName><![CDATA[{to_user}]]></ToUserName>
|
||||
<FromUserName><![CDATA[{self.config.wechat_app.corp_id}]]></FromUserName>
|
||||
<CreateTime>{timestamp}</CreateTime>
|
||||
<MsgType><![CDATA[text]]></MsgType>
|
||||
<Content><![CDATA[{content}]]></Content>
|
||||
</xml>"""
|
||||
|
||||
return response_xml
|
||||
Reference in New Issue
Block a user