1199 lines
48 KiB
Python
1199 lines
48 KiB
Python
"""
|
|
企业微信消息处理器
|
|
处理用户消息和事件,实现菜单功能
|
|
"""
|
|
|
|
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._request_cache = {} # {f"{user_id}:{content}": timestamp}
|
|
self._cache_timeout = 30 # 30秒内相同请求不处理
|
|
|
|
# 菜单配置
|
|
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 self._is_duplicate_request(from_user, content):
|
|
logger.info(f"检测到重复请求: user={from_user}, content={content[:20]}...")
|
|
return self._create_text_response("请求过于频繁,请稍后再试。", from_user)
|
|
|
|
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)
|
|
elif content in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "全部", "all"]:
|
|
# 处理最新公告来源选择
|
|
return self._handle_latest_news_by_source(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
|
|
from ..core.database import get_db_cursor
|
|
from datetime import date
|
|
|
|
# 获取今日关键词命中公告数(从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')}
|
|
|
|
🎯 今日关键词命中: {today_keyword_hits} 条
|
|
📈 历史累计命中: {total_keyword_hits} 条
|
|
|
|
📋 今日命中分类:
|
|
"""
|
|
|
|
# 添加各类型统计
|
|
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 = f"""📊 今日关键词命中统计
|
|
|
|
统计时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
|
|
|
🎯 今日关键词命中: 0 条
|
|
📈 历史累计命中: {total_keyword_hits} 条
|
|
|
|
暂无今日关键词命中公告。
|
|
|
|
💡 可能原因:
|
|
- 今日暂无匹配关键词的公告发布
|
|
- 系统定时搜索还未执行
|
|
- 点击"立即搜索"可手动触发更新"""
|
|
|
|
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} 请求最新公告")
|
|
|
|
response = """📋 最新公告查询
|
|
|
|
请选择要查看的公告来源:
|
|
|
|
1️⃣ 采购公告 - 招标采购信息
|
|
2️⃣ 结果公告 - 中标成交信息
|
|
3️⃣ 更正公告 - 变更澄清信息
|
|
4️⃣ 合同公告 - 合同签订信息
|
|
5️⃣ 预公示 - 招标文件预公示
|
|
6️⃣ 单一来源 - 单一来源采购
|
|
7️⃣ 电子卖场 - 电子化采购平台
|
|
8️⃣ 履约验收 - 项目验收信息
|
|
9️⃣ 工程公告 - 工程建设信息
|
|
🔟 采购意向 - 采购意向公开
|
|
|
|
💡 使用方法:
|
|
• 发送对应数字选择来源
|
|
• 例如:发送 "1" 查看采购公告
|
|
• 发送 "10" 查看采购意向
|
|
|
|
或发送 "全部" 查看所有来源的最新公告。"""
|
|
|
|
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_by_source(self, source_choice: str, from_user: str) -> Optional[str]:
|
|
"""处理按来源查看最新公告 - 直接从指定来源爬取最新的10条公告"""
|
|
try:
|
|
logger.info(f"用户 {from_user} 选择公告来源: {source_choice}")
|
|
|
|
# 来源映射 - 公告类型到来源代码的映射
|
|
source_mapping = {
|
|
"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":
|
|
# 显示所有来源的最新公告
|
|
return self._handle_latest_news_all(from_user)
|
|
|
|
if source_choice not in source_mapping:
|
|
response = """❌ 无效的选择
|
|
|
|
请发送正确的数字(1-10)或"全部"。
|
|
|
|
返回公告查询菜单,请点击"最新公告"重新选择。"""
|
|
return self._create_text_response(response, from_user)
|
|
|
|
source_code, type_name = source_mapping[source_choice]
|
|
|
|
# 直接从指定来源爬取最新公告
|
|
try:
|
|
from ..crawler.spider import crawl_announcements
|
|
from ..filters.filters import DateFilter
|
|
from datetime import date
|
|
|
|
# 只爬取指定来源的公告
|
|
logger.info(f"开始爬取 {type_name} 来源的公告")
|
|
crawl_results = crawl_announcements(sources=[source_code])
|
|
|
|
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"
|
|
|
|
if len(sorted_announcements) > 5:
|
|
response += f"\n... 还有 {len(sorted_announcements) - 5} 条公告"
|
|
|
|
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 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)}")
|
|
return self._create_text_response("获取最新公告失败,请稍后重试", from_user)
|
|
|
|
def _handle_latest_news_all(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 "未知"
|
|
type_name = {
|
|
'purchase': '采购',
|
|
'result': '结果',
|
|
'correction': '更正',
|
|
'contract': '合同',
|
|
'pre_announcement': '预公示',
|
|
'single_source': '单一来源',
|
|
'electronic_market': '电子卖场',
|
|
'acceptance': '履约验收',
|
|
'engineering': '工程'
|
|
}.get(announcement.announcement_type.value, announcement.announcement_type.value)
|
|
response += f"{i}. [{type_name}] {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 = []
|
|
try:
|
|
if hasattr(self.config, 'crawler') and self.config.crawler and hasattr(self.config.crawler, 'keyword'):
|
|
current_keywords = self.config.crawler.keyword
|
|
else:
|
|
# 从配置文件直接读取
|
|
import yaml
|
|
import os
|
|
config_path = os.path.join(os.path.dirname(__file__), '..', '..', 'gx_gp_monitor', 'config', 'config.yaml')
|
|
if os.path.exists(config_path):
|
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|
config_data = yaml.safe_load(f)
|
|
current_keywords = config_data.get('crawler', {}).get('keyword', [])
|
|
except Exception as e:
|
|
logger.warning(f"读取关键词配置失败: {str(e)}")
|
|
|
|
# 如果获取不到,使用默认显示
|
|
if not current_keywords:
|
|
current_keywords = ["大化", "南宁", "信息化", "政府采购"]
|
|
|
|
keywords_str = "\n• ".join(current_keywords)
|
|
|
|
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 ..core.database import init_database
|
|
try:
|
|
init_database() # 这个函数没有返回值
|
|
except Exception as init_error:
|
|
status_info["database"] = f"初始化失败: {str(init_error)[:20]}..."
|
|
else:
|
|
# 简单的数据库连接测试
|
|
from ..core.database import get_db_connection
|
|
with get_db_connection() as conn:
|
|
# 执行一个简单的查询来测试连接
|
|
with conn.cursor() as cursor:
|
|
cursor.execute("SELECT 1")
|
|
result = cursor.fetchone()
|
|
if result and result[0] == 1:
|
|
status_info["database"] = "正常"
|
|
else:
|
|
status_info["database"] = "异常"
|
|
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']}
|
|
|
|
"""
|
|
|
|
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 cleanup_storage
|
|
cleaned_count = cleanup_storage(days=30) # 清理30天前的数据
|
|
cache_cleared["database_cache"] = cleaned_count >= 0 # 如果清理成功,返回True
|
|
logger.info(f"清理了 {cleaned_count} 条过期数据")
|
|
except Exception as e:
|
|
logger.warning(f"数据库缓存清理失败: {str(e)}")
|
|
cache_cleared["database_cache"] = False
|
|
|
|
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 = """菜单功能详解:
|
|
|
|
监控操作:
|
|
- 立即搜索: 手动触发公告搜索,获取最新数据
|
|
- 今日统计: 查看今日公告统计信息和数据概览
|
|
- 关键词搜索: 输入关键词搜索相关公告
|
|
- 最新公告: 浏览最近发布的10条公告
|
|
|
|
系统管理:
|
|
- 关键词管理: 管理监控关键词(需管理员权限)
|
|
- 系统状态: 查看各组件运行状态
|
|
- 清理缓存: 清理系统缓存,提升性能
|
|
|
|
帮助:
|
|
- 使用说明: 查看详细功能介绍
|
|
|
|
文本命令:
|
|
- 发送关键词直接搜索
|
|
- "搜索 [关键词]" 指定关键词搜索
|
|
- "总结" 查看今日统计
|
|
- "帮助" 显示此说明
|
|
|
|
智能推送:
|
|
系统会自动监控匹配关键词的公告,并通过企业微信实时推送。
|
|
|
|
使用技巧:
|
|
- 关键词支持中英文混合
|
|
- 可同时搜索多个关键词
|
|
- 公告按时间倒序排列
|
|
- 点击公告可查看详情
|
|
"""
|
|
|
|
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]:
|
|
"""处理关键词搜索 - 显示所有匹配公告的markdown格式"""
|
|
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_announcements = result.get("filtered_announcements", [])
|
|
|
|
if filtered_announcements:
|
|
# 生成markdown格式的结果
|
|
markdown_content = self._generate_keyword_search_markdown(keywords, filtered_announcements)
|
|
|
|
# 发送markdown消息
|
|
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="🔍 搜索完成",
|
|
content=markdown_content,
|
|
message_type="markdown"
|
|
)
|
|
else:
|
|
# 如果不支持markdown,发送文本消息
|
|
notify_success = manager.send_system_notification(
|
|
title="🔍 搜索完成",
|
|
content=f"发现 {len(filtered_announcements)} 条匹配公告"
|
|
)
|
|
|
|
if notify_success:
|
|
# 返回简单的文本确认
|
|
return self._create_text_response("搜索完成!已发送详细结果到聊天窗口。", from_user)
|
|
else:
|
|
# 如果markdown发送失败,返回文本格式
|
|
return self._create_text_response(f"搜索完成!发现 {len(filtered_announcements)} 条匹配公告,已推送通知。", from_user)
|
|
|
|
except Exception as notify_error:
|
|
logger.warning(f"发送markdown通知失败: {str(notify_error)}")
|
|
# 返回文本格式的结果
|
|
return self._create_text_response(f"搜索完成!发现 {len(filtered_announcements)} 条匹配公告。", from_user)
|
|
|
|
else:
|
|
response = f"""🔍 搜索完成
|
|
|
|
关键词: {' '.join(keywords)}
|
|
总公告数: 0
|
|
|
|
未发现匹配的公告。
|
|
|
|
💡 建议:
|
|
• 尝试更通用的关键词
|
|
• 检查关键词拼写
|
|
• 等待系统更新最新数据"""
|
|
return self._create_text_response(response, from_user)
|
|
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 _generate_keyword_search_markdown(self, keywords: List[str], announcements: List) -> str:
|
|
"""生成关键词搜索结果的markdown格式"""
|
|
import datetime
|
|
|
|
# 按公告类型分组
|
|
type_groups = {}
|
|
for ann in announcements:
|
|
ann_type = ann.announcement_type.value
|
|
if ann_type not in type_groups:
|
|
type_groups[ann_type] = []
|
|
type_groups[ann_type].append(ann)
|
|
|
|
# 类型名称映射
|
|
type_name_map = {
|
|
'purchase': '采购公告',
|
|
'result': '结果公告',
|
|
'correction': '更正公告',
|
|
'contract': '合同公告',
|
|
'pre_announcement': '预公示',
|
|
'single_source': '单一来源',
|
|
'electronic_market': '电子卖场公示',
|
|
'acceptance': '履约验收',
|
|
'engineering': '工程公告',
|
|
'intention': '采购意向'
|
|
}
|
|
|
|
# 生成markdown内容
|
|
lines = [
|
|
f"关键词: `{' '.join(keywords)}` - 总公告数: `{len(announcements)}`\n\n",
|
|
f"更新时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
]
|
|
|
|
# 按类型输出
|
|
for ann_type, ann_list in type_groups.items():
|
|
type_name = type_name_map.get(ann_type, ann_type)
|
|
lines.append(f"**{type_name} - 共 {len(ann_list)} 条公告**\n\n")
|
|
|
|
for i, ann in enumerate(ann_list, 1):
|
|
title = ann.title
|
|
if len(title) > 40:
|
|
title = title[:40] + "..."
|
|
|
|
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} | {type_name}\n\n")
|
|
|
|
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:
|
|
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 _is_duplicate_request(self, user_id: str, content: str) -> bool:
|
|
"""检查是否为重复请求"""
|
|
import time
|
|
|
|
cache_key = f"{user_id}:{content}"
|
|
current_time = time.time()
|
|
|
|
# 清理过期的缓存
|
|
expired_keys = [k for k, v in self._request_cache.items() if current_time - v > self._cache_timeout]
|
|
for key in expired_keys:
|
|
del self._request_cache[key]
|
|
|
|
# 检查是否重复
|
|
if cache_key in self._request_cache:
|
|
last_time = self._request_cache[cache_key]
|
|
if current_time - last_time < self._cache_timeout:
|
|
return True
|
|
|
|
# 更新缓存
|
|
self._request_cache[cache_key] = current_time
|
|
return False
|
|
|
|
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
|