Compare commits

..

10 Commits

Author SHA1 Message Date
v6ole c548a8b5bd ```
feat(core): 添加大化县政府网采购公告数据表和相关功能

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

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

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

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

- 为不同来源公告添加前缀标识(【大化县政府网】或【广西政府采购网】)
- 根据公告来源动态调整通知标题:
  - 单一来源显示具体来源
  - 双来源显示"双源监控"标识
- 改进通知卡片的来源区分度,便于用户识别公告来源
```
2026-05-09 10:55:00 +08:00
v6ole bd8df98e9a Remove obsolete scripts related to WeChat menu creation, server functionality, and cron job scheduling. Update project structure by deleting unnecessary files to streamline the codebase. 2026-01-28 16:54:40 +08:00
v6ole b37e8ce581 删除无用文件 2026-01-09 15:17:42 +08:00
v6ole f00068f804 修改通知模板 2026-01-09 11:07:10 +08:00
v6ole 72dc40186d Refactor the cron job functionality to transition from a crawling to a searching model. Update related documentation and scripts to reflect the new terminology and functionality, including changes in the README and script comments. Enhance WeChat menu and message handling to support the new search features, ensuring clarity in user interactions and logging. 2026-01-09 08:19:09 +08:00
v6ole 4000438f49 Remove deprecated files and scripts related to cron jobs and WeChat server functionality. Update README to include new server startup methods using uWSGI and Gunicorn. Ensure callback server configuration is loaded properly in production environments. Clean up unnecessary test scripts and example files to streamline the project structure. 2026-01-08 21:59:39 +08:00
v6ole 85cd7e9d3e Enhance manual crawling functionality in GXGPMonitorApp to skip database saving for both announcements and filtered results. Update logging to reflect manual crawl mode actions, ensuring clarity in the process. Adjusted comments for better understanding of the manual crawling logic. 2026-01-08 19:12:37 +08:00
v6ole 04d254cfef Implement manual crawling feature in GXGPMonitorApp, allowing users to filter announcements by date and keywords. Update Markdown generation to include time period and keyword details. Remove outdated onu.md file. Enhance WeChat message handling to support manual crawl results and improve logging for better traceability. 2026-01-08 18:24:00 +08:00
v6ole b52d9c069d Update announcement timestamp in onu.md and enhance logger formatting to preserve original message and levelname. Remove outdated onu.md file and log files, and add type hints in main.py for better code clarity. 2026-01-08 16:40:10 +08:00
v6ole 5f8ebd2da5 Refactor logger to implement a global file handler, preventing duplicate log entries. Updated logger initialization to ensure a single file handler is used across all loggers, enhancing logging efficiency and clarity. 2026-01-08 16:18:41 +08:00
86 changed files with 132375 additions and 6974 deletions
-231
View File
@@ -1,231 +0,0 @@
# 广西政府采购网公告监控系统 - 企业微信集成
## 概述
本系统已集成企业微信功能,支持:
- 企业微信应用消息推送
- 回调服务器处理用户交互
- 应用菜单管理
- 实时爬取和自定义搜索
## 配置要求
### 1. 企业微信应用配置
`config/config.yaml` 中配置企业微信信息:
```yaml
# 企业微信应用配置
wechat_app:
enabled: true # 是否启用企业微信通知
corp_id: "ww69e8e44636f47780" # 企业ID
agent_id: "1000007" # 应用ID
secret: "SmelCwKFoL0E9ATWFzr-w7gsfXBTN72lT1UqnNd0HpI" # 应用Secret
token: "DmvL98cAF6x9CFtQZwqD2emGL8S7HxA" # Token
encoding_aes_key: "yAc4OoSCP92YTefHXYfw27WeG9oF11W9d6nw6QYlU3D" # 消息加密Key
port: 18001 # 服务端口
```
### 2. 企业微信管理后台设置
1. **设置回调URL**
- 在企业微信管理后台的应用设置中
- 设置回调URL为:`http://your-server-ip:18001/api/v1/wechat/callback`
- Token和EncodingAESKey需要与配置文件一致
2. **设置应用可见范围**
- 配置应用对哪些部门或成员可见
- 确保有基础接口权限
## 功能说明
### 1. 消息推送
系统会在以下情况下自动推送消息到企业微信:
- **爬取完成通知**:每次执行爬取后,推送筛选结果
- **关键词匹配通知**:发现匹配关键词的公告时推送
- **系统状态通知**:系统启动、错误等重要事件
消息格式参考 `企业微信消息示例.md`
### 2. 应用菜单
系统提供以下菜单功能:
#### 操作菜单
- **立即爬取**:执行一次完整的公告爬取
- **今日总结**:查看今日公告统计(开发中)
- **自定义爬取**:输入关键词进行搜索
#### 帮助菜单
- **使用说明**:显示详细使用指南
- **联系我们**:联系信息
### 3. 文本交互
支持以下文本命令:
```
# 关键词搜索
直接发送关键词,如:大化、信息化
# 手动爬取
爬取 [关键词] # 如:爬取 大化
# 获取帮助
帮助 # 显示使用说明
help
# 查看总结
总结 # 查看今日统计
```
## 部署和运行
### 1. 安装依赖
```bash
pip install -r gx_gp_monitor/requirements.txt
```
### 2. 测试功能
```bash
# 测试企业微信模块
python test_wechat_server.py
```
### 3. 启动回调服务器
```bash
# 方法1:使用专用启动脚本
python start_wechat_server.py
# 方法2:使用主程序
python gx_gp_monitor/main.py wechat-server
```
### 4. 管理菜单
```bash
# 创建菜单
python gx_gp_monitor/main.py wechat-menu --action create
# 删除菜单
python gx_gp_monitor/main.py wechat-menu --action delete
# 获取菜单信息
python gx_gp_monitor/main.py wechat-menu --action get
# 测试菜单操作
python gx_gp_monitor/main.py wechat-menu --action test
```
### 5. 正常爬取
```bash
# 执行爬取(会自动推送消息)
python gx_gp_monitor/main.py crawl
```
## 技术实现
### 1. 回调服务器
- **框架**Flask
- **端口**18001
- **路径**`/api/v1/wechat/callback`
- **功能**:处理企业微信的回调消息和URL验证
### 2. 消息加解密
- **库来源**:企业微信官方Python示例
- **位置**`gx_gp_monitor/wechat/WXBizMsgCrypt.py`
- **功能**:实现消息加密、解密和签名验证
### 3. 消息处理
- **事件处理**:关注、取消关注、菜单点击
- **文本处理**:关键词搜索、手动命令
- **响应格式**:XML格式的企业微信消息
### 4. 菜单管理
- **创建菜单**:自动创建预定义菜单
- **删除菜单**:清理现有菜单
- **获取菜单**:查看当前菜单配置
## 故障排除
### 1. 回调URL验证失败
- 检查Token和EncodingAESKey是否正确配置
- 确保服务器可从公网访问
- 检查端口18001是否开放
### 2. 消息推送失败
- 检查企业微信应用配置(corp_id, agent_id, secret
- 确认应用有消息发送权限
- 查看应用可见范围设置
### 3. 菜单创建失败
- 检查应用是否有菜单管理权限
- 确认Token有效且有管理员权限
### 4. 导入错误
```bash
# 确保依赖已安装
pip install flask pycryptodome
# 测试导入
python -c "from gx_gp_monitor.wechat.callback_server import WeChatCallbackServer; print('OK')"
```
## 日志查看
系统会记录详细的企业微信操作日志:
```bash
# 查看日志
tail -f logs/gx_gp_monitor.log | grep wechat
```
## 安全注意事项
1. **保护配置文件**:不要将包含密钥的配置文件提交到版本控制
2. **网络安全**:确保回调服务器只接受来自企业微信的请求
3. **权限控制**:合理设置应用可见范围和权限
4. **定期更新**:定期更新企业微信应用Secret
## 开发说明
### 项目结构
```
gx_gp_monitor/wechat/
├── __init__.py # 模块初始化
├── callback_server.py # Flask回调服务器
├── message_handler.py # 消息处理逻辑
├── menu_manager.py # 菜单管理
├── WXBizMsgCrypt.py # 企业微信加密库
└── ierror.py # 错误码定义
```
### 扩展功能
如需添加新功能,可以:
1.`message_handler.py` 中添加新的消息处理逻辑
2.`menu_manager.py` 中修改菜单配置
3.`callback_server.py` 中添加新的路由
## 联系支持
如有问题,请检查:
1. 系统日志
2. 企业微信管理后台错误信息
3. 网络连接和配置
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""
企业微信回调服务器 WSGI 应用入口
用于 uWSGI 等部署环境
"""
import sys
import os
# 添加项目路径
sys.path.insert(0, os.path.dirname(__file__))
try:
from gx_gp_monitor.wechat.callback_server import create_callback_app
# 创建 WSGI 应用对象
app = create_callback_app()
except ImportError as e:
print(f"导入失败: {e}", file=sys.stderr)
print("请确保已安装所有依赖: pip install -r gx_gp_monitor/requirements.txt", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"应用创建失败: {e}", file=sys.stderr)
sys.exit(1)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -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 # 最大页数
+373 -10
View File
@@ -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,
@@ -223,12 +264,32 @@ class DatabaseManager:
FOREIGN KEY (source_code) REFERENCES announcement_sources(code)
);
-- 大化县政府网采购公告表(全部推送,不筛选)
CREATE TABLE IF NOT EXISTS dahuagov_announcements (
id SERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
publish_date TIMESTAMP NOT NULL,
purchase_name VARCHAR(200),
content_url TEXT,
source_code VARCHAR(50) NOT NULL DEFAULT 'dahuagov',
source_name VARCHAR(100) NOT NULL DEFAULT '大化县政府网采购公告',
announcement_type VARCHAR(50) NOT NULL DEFAULT 'purchase',
crawled_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
content_hash VARCHAR(32) UNIQUE,
is_new BOOLEAN DEFAULT TRUE
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_announcements_publish_date ON announcements(publish_date DESC);
CREATE INDEX IF NOT EXISTS idx_announcements_source_code ON announcements(source_code);
CREATE INDEX IF NOT EXISTS idx_announcements_content_hash ON announcements(content_hash);
CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_crawl_results_crawled_at ON crawl_results(crawled_at DESC);
CREATE INDEX IF NOT EXISTS idx_dahuagov_publish_date ON dahuagov_announcements(publish_date DESC);
CREATE INDEX IF NOT EXISTS idx_dahuagov_content_hash ON dahuagov_announcements(content_hash);
CREATE INDEX IF NOT EXISTS idx_dahuagov_created_at ON dahuagov_announcements(created_at DESC);
-- 创建更新时间触发器
CREATE OR REPLACE FUNCTION update_updated_at_column()
@@ -330,13 +391,101 @@ class DatabaseManager:
sql = """
INSERT INTO announcements (
title, publish_date, purchase_name, content_url, source_code, source_name,
announcement_type, crawled_at, crawl_mode, content_hash, keyword_matched,
date_filtered, is_new, is_today
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (content_hash) DO NOTHING
"""
values = []
for announcement in announcements:
values.append((
announcement.title,
announcement.publish_date,
announcement.purchase_name,
announcement.content_url,
announcement.source_code,
announcement.source_name,
announcement.announcement_type.value,
announcement.crawled_at,
announcement.crawl_mode,
announcement.content_hash,
announcement.keyword_matched,
announcement.date_filtered,
announcement.is_new,
announcement.is_today
))
try:
with get_db_cursor() as cursor:
extras.execute_batch(cursor, sql, values)
affected_rows = cursor.rowcount
logger.info(f"批量保存公告完成,成功保存 {affected_rows}")
return affected_rows
except Exception as e:
logger.error(f"批量保存公告失败: {str(e)}")
return 0
def save_announcements_batch_to_table(self, announcements: List[Announcement], table_name: str) -> int:
"""
批量保存公告到指定表
Args:
announcements: 公告列表
table_name: 目标表名 ("auto_announcements""manual_announcements")
Returns:
int: 成功保存的数量
"""
if not self.config.database.enabled:
return 0
if not announcements:
return 0
# 为没有哈希的公告生成哈希
for announcement in announcements:
if not announcement.content_hash:
announcement.generate_content_hash()
# 根据表名决定是否使用ON CONFLICT
if table_name == "manual_announcements":
# 手动搜索表不使用唯一约束(允许重复)
sql = f"""
INSERT INTO {table_name} (
title, publish_date, purchase_name, content_url, source_code, source_name,
announcement_type, crawled_at, content_hash, keyword_matched,
date_filtered, is_new, is_today
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
values = []
for announcement in announcements:
values.append((
announcement.title,
announcement.publish_date,
announcement.purchase_name,
announcement.content_url,
announcement.source_code,
announcement.source_name,
announcement.announcement_type.value,
announcement.crawled_at,
announcement.content_hash,
announcement.keyword_matched,
announcement.date_filtered,
announcement.is_new,
announcement.is_today
))
else:
# 自动搜索表使用唯一约束
sql = f"""
INSERT INTO {table_name} (
title, publish_date, purchase_name, content_url, source_code, source_name,
announcement_type, crawled_at, content_hash, keyword_matched,
date_filtered, is_new, is_today
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (content_hash) DO NOTHING
"""
values = []
for announcement in announcements:
values.append((
@@ -359,10 +508,11 @@ class DatabaseManager:
with get_db_cursor() as cursor:
extras.execute_batch(cursor, sql, values)
affected_rows = cursor.rowcount
logger.info(f"批量保存公告完成,成功保存 {affected_rows}")
logger.info(f"批量保存公告{table_name}完成,影响行数: {affected_rows}")
return affected_rows
except Exception as e:
logger.error(f"批量保存公告失败: {str(e)}")
logger.error(f"批量保存公告{table_name}失败: {str(e)}")
return 0
@retry_on_exception(RetryConfig(max_retries=3))
@@ -504,21 +654,96 @@ 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(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
UNION ALL
SELECT
'dahuagov_announcements' as table_name,
COUNT(*) as count,
COUNT(DISTINCT source_code) as sources,
MAX(crawled_at) as last_crawl
FROM dahuagov_announcements
"""
cursor.execute(detail_sql)
detail_results = cursor.fetchall()
stats['table_details'] = {row['table_name']: {
'count': row['count'],
'sources': row['sources'],
'last_crawl': row['last_crawl']
} for row in detail_results}
return stats
except Exception as e:
logger.error(f"获取统计信息失败: {str(e)}")
return {}
@@ -536,11 +761,22 @@ 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
UNION ALL
SELECT content_hash FROM dahuagov_announcements WHERE content_hash = %s
) as combined_check LIMIT 1
"""
try:
with get_db_cursor() as cursor:
cursor.execute(sql, (content_hash,))
cursor.execute(sql, (content_hash, content_hash, content_hash, content_hash))
return cursor.fetchone() is not None
except Exception as e:
logger.error(f"检查公告存在性失败: {str(e)}")
@@ -583,6 +819,133 @@ class DatabaseManager:
logger.error(f"获取最近公告失败: {str(e)}")
return []
@retry_on_exception(RetryConfig(max_retries=3))
def save_dahuagov_announcements(self, announcements: List[Announcement]) -> int:
"""
保存大化县政府网公告(全部推送,不筛选关键词)
Args:
announcements: 公告列表
Returns:
int: 成功保存的新公告数量
"""
if not self.config.database.enabled:
return 0
if not announcements:
return 0
# 为没有哈希的公告生成哈希
for announcement in announcements:
if not announcement.content_hash:
announcement.generate_content_hash()
sql = """
INSERT INTO dahuagov_announcements (
title, publish_date, purchase_name, content_url, source_code, source_name,
announcement_type, crawled_at, content_hash, is_new
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (content_hash) DO NOTHING
"""
values = []
for announcement in announcements:
values.append((
announcement.title,
announcement.publish_date,
announcement.purchase_name,
announcement.content_url,
announcement.source_code,
announcement.source_name,
announcement.announcement_type.value,
announcement.crawled_at,
announcement.content_hash,
announcement.is_new
))
try:
with get_db_cursor() as cursor:
extras.execute_batch(cursor, sql, values)
affected_rows = cursor.rowcount
logger.info(f"保存大化县公告完成,新增 {affected_rows}")
return affected_rows
except Exception as e:
logger.error(f"保存大化县公告失败: {str(e)}")
return 0
@retry_on_exception(RetryConfig(max_retries=3))
def get_new_dahuagov_announcements(self) -> List[Announcement]:
"""
获取大化县未推送的新公告(is_new = TRUE
Returns:
List[Announcement]: 未推送的公告列表
"""
if not self.config.database.enabled:
return []
sql = """
SELECT * FROM dahuagov_announcements
WHERE is_new = TRUE
ORDER BY publish_date DESC
"""
try:
with get_db_cursor() as cursor:
cursor.execute(sql)
rows = cursor.fetchall()
announcements = []
for row in rows:
row_dict = dict(row)
row_dict['announcement_type'] = AnnouncementType(row_dict['announcement_type'])
announcements.append(Announcement.from_dict(row_dict))
return announcements
except Exception as e:
logger.error(f"获取大化县新公告失败: {str(e)}")
return []
@retry_on_exception(RetryConfig(max_retries=3))
def mark_dahuagov_announcements_sent(self, announcements: List[Announcement]) -> int:
"""
标记大化县公告已发送(is_new = FALSE
Args:
announcements: 已发送的公告列表
Returns:
int: 更新的记录数
"""
if not self.config.database.enabled:
return 0
if not announcements:
return 0
# 获取所有公告的哈希值
hashes = [ann.content_hash for ann in announcements if ann.content_hash]
if not hashes:
return 0
sql = """
UPDATE dahuagov_announcements
SET is_new = FALSE, updated_at = CURRENT_TIMESTAMP
WHERE content_hash = ANY(%s)
"""
try:
with get_db_cursor() as cursor:
cursor.execute(sql, (hashes,))
affected_rows = cursor.rowcount
logger.info(f"标记大化县公告已发送完成,更新 {affected_rows}")
return affected_rows
except Exception as e:
logger.error(f"标记大化县公告已发送失败: {str(e)}")
return 0
# 全局数据库管理器实例
_db_manager = None
+28 -11
View File
@@ -33,15 +33,25 @@ class ColoredFormatter(logging.Formatter):
# 如果已经着色,直接返回原始格式
return super().format(record)
# 保存原始值
original_levelname = record.levelname
original_msg = record.msg
# 添加颜色
if record.levelname in self.COLORS:
# 为levelname添加颜色
colored_levelname = f"{self.COLORS[record.levelname]}{record.levelname}{self.RESET}"
# 使用原始levelname来确定消息的颜色
record.msg = f"{self.COLORS[record.levelname]}{record.msg}{self.RESET}"
record.levelname = colored_levelname
record.levelname = f"{self.COLORS[record.levelname]}{record.levelname}{self.RESET}"
# 为消息添加颜色
record.msg = f"{self.COLORS[original_levelname]}{record.msg}{self.RESET}"
return super().format(record)
# 格式化
result = super().format(record)
# 恢复原始值,避免影响其他处理器
record.levelname = original_levelname
record.msg = original_msg
return result
class Logger:
@@ -59,6 +69,7 @@ class Logger:
if not self._initialized:
self._loggers = {}
self._config = None
self._file_handler = None # 全局文件处理器,避免重复创建
self._initialized = True
def init_logger(self, name: str = "gx_gp_monitor", config=None) -> logging.Logger:
@@ -89,6 +100,9 @@ class Logger:
logger = logging.getLogger(name)
logger.setLevel(getattr(logging, self._config.log_level.value))
# 防止消息传播到父logger,避免重复记录
logger.propagate = False
# 避免重复添加处理器
if logger.handlers:
return logger
@@ -115,20 +129,23 @@ class Logger:
logger.addHandler(console_handler)
# 文件处理器(如果配置了日志文件)
if self._config.log_file:
# 文件处理器(如果配置了日志文件)- 使用全局文件处理器避免重复
if self._config.log_file and self._file_handler is None:
log_dir = Path(self._config.log_file).parent
log_dir.mkdir(parents=True, exist_ok=True)
file_handler = logging.handlers.RotatingFileHandler(
self._file_handler = logging.handlers.RotatingFileHandler(
self._config.log_file,
maxBytes=self._config.log_max_size,
backupCount=self._config.log_backup_count,
encoding='utf-8'
)
file_handler.setLevel(getattr(logging, self._config.log_level.value))
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
self._file_handler.setLevel(getattr(logging, self._config.log_level.value))
self._file_handler.setFormatter(formatter)
# 为所有logger添加全局文件处理器
if self._file_handler is not None:
logger.addHandler(self._file_handler)
self._loggers[name] = logger
return logger
+1
View File
@@ -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 # 内容哈希,用于去重
+355
View File
@@ -0,0 +1,355 @@
"""
大化瑶族自治县政府采购公告爬虫
爬取大化县政府网站的采购公告页面
"""
import time
import random
from typing import List, Optional, Tuple
from datetime import datetime
from urllib.parse import urljoin, urlparse
from bs4 import BeautifulSoup
import requests
from fake_useragent import UserAgent
try:
from ..core.models import Announcement, AnnouncementSource, AnnouncementType, CrawlResult, CrawlStatus
from ..core.config_manager import get_config
from ..core.logger import get_logger, log_crawl_start, log_crawl_success, log_crawl_error
from ..core.reliability import (
retry_on_exception, RetryConfig, session_with_retry,
TimeoutConfig, safe_execute, check_system_health
)
except ImportError:
from core.models import Announcement, AnnouncementSource, AnnouncementType, CrawlResult, CrawlStatus
from core.config_manager import get_config
from core.logger import get_logger, log_crawl_start, log_crawl_success, log_crawl_error
from core.reliability import (
retry_on_exception, RetryConfig, session_with_retry,
TimeoutConfig, safe_execute, check_system_health
)
logger = get_logger(__name__)
class DahuagovSpider:
"""大化县政府网站爬虫"""
# 大化县政府网站配置
BASE_URL = "http://www.gxdh.gov.cn"
ANNOUNCEMENT_PATH = "/xxgk/zdlyxxgk/ggzypzly/zfcgly/cggg/"
def __init__(self):
self.config = get_config()
self.ua = UserAgent()
self.session = None
self.request_count = 0
self.error_count = 0
def _get_random_user_agent(self) -> str:
"""获取随机User-Agent"""
try:
from fake_useragent import UserAgent
return self.ua.random
except:
return random.choice(self.config.crawler.user_agents)
def init_session(self):
"""初始化会话"""
if self.session is None:
timeout_config = TimeoutConfig(
connect_timeout=self.config.crawler.timeout,
read_timeout=self.config.crawler.timeout
)
retry_config = RetryConfig(
max_retries=self.config.crawler.max_retries,
initial_delay=self.config.crawler.retry_delay,
max_delay=self.config.crawler.max_retry_delay,
backoff_factor=self.config.crawler.backoff_factor
)
self.session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=10,
pool_maxsize=20,
max_retries=0
)
self.session.mount('http://', adapter)
self.session.mount('https://', adapter)
self.session.timeout = (timeout_config.connect_timeout, timeout_config.read_timeout)
return self.session
def close_session(self):
"""关闭会话"""
if self.session:
self.session.close()
self.session = None
def _fetch_page(self, url: str) -> Tuple[Optional[str], Optional[str]]:
"""
获取页面内容
Args:
url: 页面URL
Returns:
Tuple[Optional[str], Optional[str]]: (页面内容, 错误信息)
"""
try:
session = self.init_session()
user_agent = self._get_random_user_agent()
headers = {
"User-Agent": user_agent,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Connection": "keep-alive",
"Referer": self.BASE_URL
}
# 添加随机延迟
delay = random.uniform(
self.config.crawler.request_delay,
self.config.crawler.request_delay_max
)
time.sleep(delay)
response = session.get(url, headers=headers, timeout=self.session.timeout)
self.request_count += 1
if response.status_code == 200:
# 确保使用正确的编码
response.encoding = response.apparent_encoding or 'utf-8'
return response.text, None
else:
return None, f"请求失败, 状态码: {response.status_code}"
except Exception as e:
self.error_count += 1
logger.error(f"获取页面异常: {str(e)}")
return None, f"请求异常: {str(e)}"
def _parse_page(self, html: str, crawled_at: datetime) -> List[Announcement]:
"""
解析页面内容
Args:
html: 页面HTML内容
crawled_at: 爬取时间
Returns:
List[Announcement]: 解析后的公告列表
"""
announcements = []
try:
soup = BeautifulSoup(html, 'html.parser')
# 查找公告列表
# 大化县政府网站使用 ul.more-list 结构
lists = soup.find_all('ul', class_='more-list')
if not lists:
logger.info("未找到公告列表")
return announcements
for ul in lists:
lis = ul.find_all('li')
for li in lis:
try:
announcement = self._parse_li_element(li, crawled_at)
if announcement:
announcements.append(announcement)
except Exception as e:
logger.warning(f"解析单个公告失败: {str(e)}")
continue
logger.info(f"成功解析 {len(announcements)} 条公告")
except Exception as e:
logger.error(f"解析页面失败: {str(e)}")
return announcements
def _parse_li_element(self, li, crawled_at: datetime) -> Optional[Announcement]:
"""
解析单个li元素
Args:
li: BeautifulSoup li元素
crawled_at: 爬取时间
Returns:
Optional[Announcement]: 解析后的公告对象
"""
try:
# 查找日期 span
date_span = li.find('span')
if not date_span:
return None
date_text = date_span.get_text(strip=True)
if not date_text:
return None
# 解析日期
try:
publish_date = datetime.strptime(date_text, "%Y-%m-%d")
except ValueError:
logger.warning(f"日期格式无法解析: {date_text}")
return None
# 查找链接和标题
link_tag = li.find('a')
if not link_tag:
return None
title = link_tag.get('title', '') or link_tag.get_text(strip=True)
if not title:
return None
href = link_tag.get('href', '')
if not href:
return None
# 构建完整URL
if href.startswith('./') or href.startswith('../'):
content_url = urljoin(self.BASE_URL + self.ANNOUNCEMENT_PATH, href)
elif href.startswith('/'):
content_url = self.BASE_URL + href
elif href.startswith('http'):
content_url = href
else:
content_url = urljoin(self.BASE_URL + self.ANNOUNCEMENT_PATH, href)
# 创建公告对象
announcement = Announcement(
title=title,
publish_date=publish_date,
purchase_name="大化瑶族自治县", # 默认采购单位
content_url=content_url,
source_code="dahuagov",
source_name="大化县政府网采购公告",
announcement_type=AnnouncementType.PURCHASE,
crawled_at=crawled_at,
is_new=True
)
# 生成内容哈希用于去重
announcement.generate_content_hash()
return announcement
except Exception as e:
logger.warning(f"解析li元素失败: {str(e)}")
return None
@retry_on_exception(RetryConfig(max_retries=2))
def crawl(self) -> CrawlResult:
"""
爬取公告(只爬取第一页)
Returns:
CrawlResult: 爬取结果
"""
log_crawl_start("大化县政府网采购公告")
start_time = datetime.now()
result = CrawlResult(
source=AnnouncementSource(
code="dahuagov",
category_id=0,
name="大化县政府网采购公告",
type=AnnouncementType.PURCHASE
),
status=CrawlStatus.RUNNING,
crawled_at=start_time
)
try:
# 构建完整URL(只爬取第一页)
url = self.BASE_URL + self.ANNOUNCEMENT_PATH
logger.info(f"开始爬取大化县政府网站: {url}")
# 获取页面内容
html, error_msg = self._fetch_page(url)
if error_msg:
logger.warning(f"获取页面失败: {error_msg}")
result.status = CrawlStatus.FAILED
result.error_message = error_msg
return result
if not html:
logger.info("页面内容为空")
result.status = CrawlStatus.SUCCESS
result.total_count = 0
result.new_count = 0
return result
# 解析页面
announcements = self._parse_page(html, start_time)
# 更新结果
result.announcements = announcements
result.total_count = len(announcements)
result.new_count = len(announcements)
result.status = CrawlStatus.SUCCESS
duration = (datetime.now() - start_time).total_seconds()
result.duration = duration
log_crawl_success("大化县政府网采购公告", len(announcements), duration)
except Exception as e:
duration = (datetime.now() - start_time).total_seconds()
result.duration = duration
result.status = CrawlStatus.FAILED
result.error_message = str(e)
log_crawl_error("大化县政府网采购公告", str(e))
return result
def get_stats(self) -> dict:
"""获取爬虫统计信息"""
return {
"request_count": self.request_count,
"error_count": self.error_count,
"error_rate": self.error_count / max(self.request_count, 1),
"session_active": self.session is not None
}
def create_dahuagov_spider() -> DahuagovSpider:
"""
创建大化县爬虫实例
Returns:
DahuagovSpider: 爬虫实例
"""
return DahuagovSpider()
def crawl_dahuagov_announcements() -> List[CrawlResult]:
"""
便捷函数:爬取大化县公告
Returns:
List[CrawlResult]: 爬取结果列表
"""
spider = create_dahuagov_spider()
try:
result = spider.crawl()
return [result]
finally:
spider.close_session()
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""
定时搜索脚本
执行搜索、筛选关键词、保存到数据库并发送企业微信卡片通知
"""
import sys
import os
from pathlib import Path
# 添加项目根目录到路径(gx_gp_monitor的父目录)
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
try:
from gx_gp_monitor.core.config_manager import load_config, get_config
from gx_gp_monitor.core.logger import init_logger, get_logger
from gx_gp_monitor.crawler.spider import crawl_announcements
from gx_gp_monitor.crawler.dahuagov_spider import crawl_dahuagov_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, save_auto_announcements_to_storage
from gx_gp_monitor.notification.wechat import send_announcements_notification, send_system_notification
logger = get_logger(__name__)
def main():
"""主函数:执行定时搜索任务"""
try:
# 加载配置
config = load_config()
if not config:
logger.error("无法加载配置")
return False
# 初始化日志
init_logger(config=config)
logger.info("=== 开始定时搜索任务 ===")
# 初始化存储
init_storage()
# 导入数据库模块
import gx_gp_monitor.core.database as db_module
db_manager = db_module.get_database_manager()
# 执行搜索(爬取所有公告)
logger.info("开始执行定时搜索任务")
# 收集所有爬取结果
all_crawl_results = []
# 1. 爬取广西政府采购网
logger.info("开始爬取广西政府采购网...")
gxgp_results = crawl_announcements()
if gxgp_results:
all_crawl_results.extend(gxgp_results)
logger.info(f"广西政府采购网爬取完成,获取 {sum(len(r.announcements) for r in gxgp_results)} 条公告")
# 2. 爬取大化县政府网采购公告(全部推送,不筛选)
logger.info("开始爬取大化县政府网采购公告(全部推送)...")
dahua_results = crawl_dahuagov_announcements()
if dahua_results:
all_crawl_results.extend(dahua_results)
logger.info(f"大化县政府网爬取完成,获取 {sum(len(r.announcements) for r in dahua_results)} 条公告")
if not all_crawl_results:
logger.info("爬取完成:无数据")
return True
# 收集所有公告
all_announcements = []
for result in all_crawl_results:
if result.announcements:
all_announcements.extend(result.announcements)
total_crawled = len(all_announcements)
logger.info(f"搜索到 {total_crawled} 条原始公告")
if not all_announcements:
logger.info("没有获取到任何公告")
return True
# 分离广西政府采购网和大化县政府网的公告
gxgp_all_announcements = [a for a in all_announcements if a.source_code != 'dahuagov']
dahua_all_announcements = [a for a in all_announcements if a.source_code == 'dahuagov']
logger.info(f"广西政府采购网: {len(gxgp_all_announcements)}")
logger.info(f"大化县政府网: {len(dahua_all_announcements)}")
# ========== 处理广西政府采购网(关键词筛选)==========
gxgp_filtered = []
if gxgp_all_announcements:
# 对广西政府采购网公告进行关键词筛选
from gx_gp_monitor.filters.filters import KeywordFilter, DateFilter
from datetime import date
keyword_filter = KeywordFilter()
gxgp_keyword_filtered = keyword_filter.filter_announcements(
gxgp_all_announcements, keywords=config.crawler.keyword)
# 日期筛选(只处理今天的)
date_filter = DateFilter()
gxgp_today_filtered = date_filter.filter_announcements(
gxgp_keyword_filtered,
start_date=date.today(),
end_date=date.today()
)
logger.info(f"广西政府采购网关键词筛选后: {len(gxgp_keyword_filtered)}")
logger.info(f"广西政府采购网今日匹配: {len(gxgp_today_filtered)}")
# 检查是否已存在
for ann in gxgp_today_filtered:
try:
with db_module.get_db_cursor() as cursor:
cursor.execute(
"SELECT 1 FROM auto_announcements WHERE content_hash = %s LIMIT 1",
(ann.content_hash,)
)
exists = cursor.fetchone() is not None
if not exists:
gxgp_filtered.append(ann)
except Exception as e:
logger.warning(f"检查公告是否存在失败: {str(e)}")
pass
logger.info(f"广西政府采购网新增公告: {len(gxgp_filtered)}")
# ========== 处理大化县政府网(全部推送,不筛选)==========
dahua_new_announcements = []
if dahua_all_announcements:
# 大化县公告不需要关键词筛选,直接检查是否已存在于dahuagov_announcements表
for ann in dahua_all_announcements:
try:
with db_module.get_db_cursor() as cursor:
cursor.execute(
"SELECT 1 FROM dahuagov_announcements WHERE content_hash = %s LIMIT 1",
(ann.content_hash,)
)
exists = cursor.fetchone() is not None
if not exists:
# 标记为新公告
ann.is_new = True
dahua_new_announcements.append(ann)
except Exception as e:
logger.warning(f"检查大化县公告是否存在失败: {str(e)}")
pass
logger.info(f"大化县政府网新增公告: {len(dahua_new_announcements)}")
# 如果没有新增公告,直接结束
if not gxgp_filtered and not dahua_new_announcements:
logger.info("没有新增公告,任务完成")
return True
# ========== 保存到数据库 ==========
# 保存广西政府采购网公告
if gxgp_filtered:
saved_gxgp = save_auto_announcements_to_storage(gxgp_filtered)
logger.info(f"保存广西政府采购网公告: {saved_gxgp}")
# 保存大化县政府网公告到专用表
if dahua_new_announcements:
saved_dahua = db_manager.save_dahuagov_announcements(dahua_new_announcements)
logger.info(f"保存大化县政府网公告: {saved_dahua}")
# ========== 发送企业微信通知 ==========
if config.wechat_app.enabled:
logger.info("开始发送企业微信卡片通知...")
notify_success = True
# 发送广西政府采购网通知
if gxgp_filtered:
logger.info(f"发送广西政府采购网通知,共 {len(gxgp_filtered)} 条...")
gxgp_success = send_announcements_notification(gxgp_filtered)
if gxgp_success:
logger.info("广西政府采购网通知发送成功")
else:
logger.error("广西政府采购网通知发送失败")
notify_success = False
# 发送大化县政府网通知
if dahua_new_announcements:
logger.info(f"发送大化县政府网通知,共 {len(dahua_new_announcements)} 条...")
dahua_success = send_announcements_notification(dahua_new_announcements)
if dahua_success:
logger.info("大化县政府网通知发送成功")
# 标记为已发送
db_manager.mark_dahuagov_announcements_sent(dahua_new_announcements)
else:
logger.error("大化县政府网通知发送失败")
notify_success = False
else:
logger.info("企业微信通知未启用,跳过发送")
notify_success = True
# ========== 输出统计信息 ==========
print("\n=== 定时搜索任务完成 ===")
print(f"总共爬取: {total_crawled} 条公告")
print(f"广西政府采购网:")
print(f" - 爬取: {len(gxgp_all_announcements)}")
print(f" - 关键词匹配: {len(gxgp_filtered)}")
print(f"大化县政府网:")
print(f" - 爬取: {len(dahua_all_announcements)}")
print(f" - 新增推送: {len(dahua_new_announcements)}")
print(f"企业微信通知: {'成功' if notify_success else '失败' if config.wechat_app.enabled else '未启用'}")
logger.info("=== 定时搜索任务完成 ===")
return True
except Exception as e:
logger.error(f"定时搜索任务执行失败: {str(e)}")
print(f"❌ 定时搜索任务失败: {str(e)}", file=sys.stderr)
# 尝试发送错误通知
try:
if config and config.wechat_app.enabled:
send_system_notification(
"定时搜索任务失败",
f"错误信息: {str(e)}"
)
except Exception as notify_error:
logger.error(f"发送错误通知失败: {notify_error}")
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)
except ImportError as e:
print(f"导入失败: {e}", file=sys.stderr)
print("请确保已安装所有依赖: pip install -r gx_gp_monitor/requirements.txt", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"脚本执行失败: {e}", file=sys.stderr)
sys.exit(1)
-858
View File
@@ -1,858 +0,0 @@
2026-01-07 17:16:51 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:16:51 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:16:51 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:16:52 - core.reliability - INFO - 健康检查通过
2026-01-07 17:16:52 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:16:52 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:16:52 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:16:52 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:16:52 - __main__ - INFO - 应用初始化完成
2026-01-07 17:16:52 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:16:52 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:20:31 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:20:31 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:20:31 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:20:31 - core.reliability - INFO - 健康检查通过
2026-01-07 17:20:31 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:20:31 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:20:31 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:20:31 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:20:31 - __main__ - INFO - 应用初始化完成
2026-01-07 17:20:31 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:20:31 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:21:02 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:21:02 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:21:02 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:21:02 - core.reliability - INFO - 健康检查通过
2026-01-07 17:21:02 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:21:02 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:21:02 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:21:02 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:21:02 - __main__ - INFO - 应用初始化完成
2026-01-07 17:21:02 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:21:02 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:22:10 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:22:10 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:22:10 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:22:10 - core.reliability - INFO - 健康检查通过
2026-01-07 17:22:10 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:22:10 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:22:10 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:22:10 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:22:10 - __main__ - INFO - 应用初始化完成
2026-01-07 17:22:10 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:22:10 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:22:44 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:22:44 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:22:44 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:22:44 - core.reliability - INFO - 健康检查通过
2026-01-07 17:22:44 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:22:44 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:22:44 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:22:44 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:22:44 - __main__ - INFO - 应用初始化完成
2026-01-07 17:22:44 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:22:44 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:22:46 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:22:46 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:22:46 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:22:47 - core.reliability - INFO - 健康检查通过
2026-01-07 17:22:47 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:22:47 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:22:47 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:22:47 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:22:47 - __main__ - INFO - 应用初始化完成
2026-01-07 17:22:47 - __main__ - INFO - 开始执行爬取任务
2026-01-07 17:22:47 - crawler.spider - INFO - 开始爬取 13 个公告来源
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 采购公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 结果公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 合同公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 更正公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 招标文件预公示 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 单一来源公示 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 电子卖场公示 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 履约验收公示 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 工程类公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 框架协议征集公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 框架协议入围结果公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 框架协议成交结果汇总公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - ERROR - 爬取来源 采购意向公开 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'
2026-01-07 17:22:47 - crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 0 个,失败 13 个,获取 0 条公告
2026-01-07 17:22:47 - __main__ - INFO - 爬取到 0 条原始公告
2026-01-07 17:22:47 - __main__ - ERROR - 爬取任务执行失败: attempted relative import beyond top-level package
2026-01-07 17:22:47 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:22:47 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:23:10 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:23:10 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:23:10 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:23:10 - core.reliability - INFO - 健康检查通过
2026-01-07 17:23:10 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:23:10 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:23:10 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:23:10 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:23:10 - __main__ - INFO - 应用初始化完成
2026-01-07 17:23:10 - __main__ - INFO - 开始执行爬取任务
2026-01-07 17:23:10 - crawler.spider - INFO - 开始爬取 13 个公告来源
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 采购公告 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 结果公告 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 合同公告 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 更正公告 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 招标文件预公示 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 单一来源公示 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 电子卖场公示 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 履约验收公示 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 工程类公告 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 框架协议征集公告 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 框架协议入围结果公告 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 框架协议成交结果汇总公告 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - ERROR - 爬取来源 采购意向公开 时发生未预期错误: attempted relative import beyond top-level package
2026-01-07 17:23:10 - crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 0 个,失败 13 个,获取 0 条公告
2026-01-07 17:23:10 - __main__ - INFO - 爬取到 0 条原始公告
2026-01-07 17:23:10 - __main__ - ERROR - 爬取任务执行失败: attempted relative import beyond top-level package
2026-01-07 17:23:10 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:23:10 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:23:34 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:23:34 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:23:34 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:23:35 - core.reliability - INFO - 健康检查通过
2026-01-07 17:23:35 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:23:35 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:23:35 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:23:35 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:23:35 - __main__ - INFO - 应用初始化完成
2026-01-07 17:23:35 - __main__ - INFO - 开始执行爬取任务
2026-01-07 17:23:35 - crawler.spider - INFO - 开始爬取 13 个公告来源
2026-01-07 17:23:35 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-01-07 17:24:05 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:05 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:05 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:05 - crawler.parsers - INFO - 成功解析 97/100 条公告记录
2026-01-07 17:24:05 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 97 条公告,耗时 30.46秒
2026-01-07 17:24:05 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-01-07 17:24:05 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:24:05 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.18秒
2026-01-07 17:24:05 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-01-07 17:24:06 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:24:06 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.17秒
2026-01-07 17:24:06 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-01-07 17:24:06 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:24:06 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.23秒
2026-01-07 17:24:06 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - INFO - 成功解析 78/100 条公告记录
2026-01-07 17:24:06 - gx_gp_monitor - INFO - 招标文件预公示 爬取完成,共获取 78 条公告,耗时 0.17秒
2026-01-07 17:24:06 - gx_gp_monitor - INFO - 开始爬取 单一来源公示
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:06 - crawler.parsers - INFO - 成功解析 0/100 条公告记录
2026-01-07 17:24:06 - crawler.spider - INFO - 单一来源公示 第1页解析到0条公告
2026-01-07 17:24:06 - gx_gp_monitor - INFO - 单一来源公示 爬取完成,共获取 0 条公告,耗时 0.18秒
2026-01-07 17:24:06 - gx_gp_monitor - INFO - 开始爬取 电子卖场公示
2026-01-07 17:24:06 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:24:06 - gx_gp_monitor - INFO - 电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.31秒
2026-01-07 17:24:06 - gx_gp_monitor - INFO - 开始爬取 履约验收公示
2026-01-07 17:24:07 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:24:07 - gx_gp_monitor - INFO - 履约验收公示 爬取完成,共获取 100 条公告,耗时 0.16秒
2026-01-07 17:24:07 - gx_gp_monitor - INFO - 开始爬取 工程类公告
2026-01-07 17:24:07 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:24:07 - gx_gp_monitor - INFO - 工程类公告 爬取完成,共获取 100 条公告,耗时 0.27秒
2026-01-07 17:24:07 - gx_gp_monitor - INFO - 开始爬取 框架协议征集公告
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - INFO - 成功解析 62/100 条公告记录
2026-01-07 17:24:07 - gx_gp_monitor - INFO - 框架协议征集公告 爬取完成,共获取 62 条公告,耗时 0.20秒
2026-01-07 17:24:07 - gx_gp_monitor - INFO - 开始爬取 框架协议入围结果公告
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:07 - crawler.parsers - INFO - 成功解析 13/100 条公告记录
2026-01-07 17:24:07 - gx_gp_monitor - INFO - 框架协议入围结果公告 爬取完成,共获取 13 条公告,耗时 0.17秒
2026-01-07 17:24:07 - gx_gp_monitor - INFO - 开始爬取 框架协议成交结果汇总公告
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - ERROR - 解析单个公告记录失败: 'NoneType' object has no attribute 'strip'
2026-01-07 17:24:08 - crawler.parsers - INFO - 成功解析 0/100 条公告记录
2026-01-07 17:24:08 - crawler.spider - INFO - 框架协议成交结果汇总公告 第1页解析到0条公告
2026-01-07 17:24:08 - gx_gp_monitor - INFO - 框架协议成交结果汇总公告 爬取完成,共获取 0 条公告,耗时 0.29秒
2026-01-07 17:24:08 - gx_gp_monitor - INFO - 开始爬取 采购意向公开
2026-01-07 17:24:08 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:24:08 - gx_gp_monitor - INFO - 采购意向公开 爬取完成,共获取 100 条公告,耗时 0.17秒
2026-01-07 17:24:08 - crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 950 条公告
2026-01-07 17:24:08 - __main__ - INFO - 爬取到 950 条原始公告
2026-01-07 17:24:08 - __main__ - ERROR - 爬取任务执行失败: attempted relative import beyond top-level package
2026-01-07 17:24:08 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:24:08 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:24:17 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:24:17 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:24:17 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:24:17 - core.reliability - INFO - 健康检查通过
2026-01-07 17:24:17 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:24:17 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:24:17 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:24:17 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:24:17 - __main__ - INFO - 应用初始化完成
2026-01-07 17:24:17 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:24:17 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:26:34 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:26:34 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:26:34 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:26:34 - core.reliability - INFO - 健康检查通过
2026-01-07 17:26:34 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:26:34 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:26:34 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:26:34 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:26:34 - __main__ - INFO - 应用初始化完成
2026-01-07 17:26:34 - __main__ - INFO - 开始执行爬取任务
2026-01-07 17:26:34 - crawler.spider - INFO - 开始爬取 13 个公告来源
2026-01-07 17:26:34 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-01-07 17:26:34 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:34 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 100 条公告,耗时 0.48秒
2026-01-07 17:26:34 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-01-07 17:26:35 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:35 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.21秒
2026-01-07 17:26:35 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-01-07 17:26:35 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:35 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-07 17:26:35 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-01-07 17:26:35 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:35 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.23秒
2026-01-07 17:26:35 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
2026-01-07 17:26:35 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:35 - gx_gp_monitor - INFO - 招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.20秒
2026-01-07 17:26:35 - gx_gp_monitor - INFO - 开始爬取 单一来源公示
2026-01-07 17:26:35 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:35 - gx_gp_monitor - INFO - 单一来源公示 爬取完成,共获取 100 条公告,耗时 0.17秒
2026-01-07 17:26:35 - gx_gp_monitor - INFO - 开始爬取 电子卖场公示
2026-01-07 17:26:36 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:36 - gx_gp_monitor - INFO - 电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.40秒
2026-01-07 17:26:36 - gx_gp_monitor - INFO - 开始爬取 履约验收公示
2026-01-07 17:26:36 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:36 - gx_gp_monitor - INFO - 履约验收公示 爬取完成,共获取 100 条公告,耗时 0.22秒
2026-01-07 17:26:36 - gx_gp_monitor - INFO - 开始爬取 工程类公告
2026-01-07 17:26:36 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:36 - gx_gp_monitor - INFO - 工程类公告 爬取完成,共获取 100 条公告,耗时 0.27秒
2026-01-07 17:26:36 - gx_gp_monitor - INFO - 开始爬取 框架协议征集公告
2026-01-07 17:26:36 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:36 - gx_gp_monitor - INFO - 框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-07 17:26:36 - gx_gp_monitor - INFO - 开始爬取 框架协议入围结果公告
2026-01-07 17:26:37 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:37 - gx_gp_monitor - INFO - 框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.25秒
2026-01-07 17:26:37 - gx_gp_monitor - INFO - 开始爬取 框架协议成交结果汇总公告
2026-01-07 17:26:37 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:37 - gx_gp_monitor - INFO - 框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-07 17:26:37 - gx_gp_monitor - INFO - 开始爬取 采购意向公开
2026-01-07 17:26:37 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:37 - gx_gp_monitor - INFO - 采购意向公开 爬取完成,共获取 100 条公告,耗时 0.17秒
2026-01-07 17:26:37 - crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告
2026-01-07 17:26:37 - __main__ - INFO - 爬取到 1300 条原始公告
2026-01-07 17:26:37 - __main__ - ERROR - 爬取任务执行失败: attempted relative import beyond top-level package
2026-01-07 17:26:37 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:26:37 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:26:57 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:26:57 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:26:57 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:26:58 - core.reliability - INFO - 健康检查通过
2026-01-07 17:26:58 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:26:58 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:26:58 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:26:58 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:26:58 - __main__ - INFO - 应用初始化完成
2026-01-07 17:26:58 - __main__ - INFO - 开始执行爬取任务
2026-01-07 17:26:58 - crawler.spider - INFO - 开始爬取 13 个公告来源
2026-01-07 17:26:58 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-01-07 17:26:58 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:58 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 100 条公告,耗时 0.37秒
2026-01-07 17:26:58 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-01-07 17:26:58 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:58 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.23秒
2026-01-07 17:26:58 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-01-07 17:26:58 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:58 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-07 17:26:58 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-01-07 17:26:59 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:59 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.20秒
2026-01-07 17:26:59 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
2026-01-07 17:26:59 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:59 - gx_gp_monitor - INFO - 招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.21秒
2026-01-07 17:26:59 - gx_gp_monitor - INFO - 开始爬取 单一来源公示
2026-01-07 17:26:59 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:59 - gx_gp_monitor - INFO - 单一来源公示 爬取完成,共获取 100 条公告,耗时 0.16秒
2026-01-07 17:26:59 - gx_gp_monitor - INFO - 开始爬取 电子卖场公示
2026-01-07 17:26:59 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:59 - gx_gp_monitor - INFO - 电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.38秒
2026-01-07 17:26:59 - gx_gp_monitor - INFO - 开始爬取 履约验收公示
2026-01-07 17:26:59 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:26:59 - gx_gp_monitor - INFO - 履约验收公示 爬取完成,共获取 100 条公告,耗时 0.18秒
2026-01-07 17:26:59 - gx_gp_monitor - INFO - 开始爬取 工程类公告
2026-01-07 17:27:00 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:27:00 - gx_gp_monitor - INFO - 工程类公告 爬取完成,共获取 100 条公告,耗时 0.25秒
2026-01-07 17:27:00 - gx_gp_monitor - INFO - 开始爬取 框架协议征集公告
2026-01-07 17:27:00 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:27:00 - gx_gp_monitor - INFO - 框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.21秒
2026-01-07 17:27:00 - gx_gp_monitor - INFO - 开始爬取 框架协议入围结果公告
2026-01-07 17:27:00 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:27:00 - gx_gp_monitor - INFO - 框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.17秒
2026-01-07 17:27:00 - gx_gp_monitor - INFO - 开始爬取 框架协议成交结果汇总公告
2026-01-07 17:27:00 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:27:00 - gx_gp_monitor - INFO - 框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-07 17:27:00 - gx_gp_monitor - INFO - 开始爬取 采购意向公开
2026-01-07 17:27:01 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:27:01 - gx_gp_monitor - INFO - 采购意向公开 爬取完成,共获取 100 条公告,耗时 0.25秒
2026-01-07 17:27:01 - crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告
2026-01-07 17:27:01 - __main__ - INFO - 爬取到 1300 条原始公告
2026-01-07 17:27:01 - filters.filters - INFO - 来源筛选: 1300 -> 1300 条公告
2026-01-07 17:27:01 - filters.filters - INFO - 关键词筛选: 1300 -> 18 条公告
2026-01-07 17:27:01 - filters.filters - INFO - 筛选完成: 总数 1300 -> 筛选后 18 (关键词: 1282, 日期: 0, 去重: 0, 来源: 0)
2026-01-07 17:27:01 - __main__ - INFO - 筛选后剩余 18 条公告
2026-01-07 17:27:01 - storage.postgresql - INFO - 开始保存 18 条公告到数据库
2026-01-07 17:27:01 - core.database - INFO - 批量保存公告完成,成功保存 1 条
2026-01-07 17:27:01 - storage.postgresql - INFO - 成功保存 1 条公告到数据库
2026-01-07 17:27:01 - storage.md_generator - INFO - Markdown文件已保存到: onu.md (共 18 条公告)
2026-01-07 17:27:01 - notification.wechat - INFO - 企业微信服务初始化完成
2026-01-07 17:27:01 - notification.wechat - INFO - 成功获取企业微信访问令牌
2026-01-07 17:27:01 - notification.wechat - INFO - Markdown消息发送成功
2026-01-07 17:27:01 - __main__ - INFO - 爬取任务完成: {'success': True, 'total_crawled': 1300, 'filtered': 18, 'saved': 1, 'markdown_generated': True, 'notification_sent': True, 'filter_stats': {'keyword_filtered': 1282, 'date_filtered': 0, 'duplicate_filtered': 0, 'source_filtered': 0}}
2026-01-07 17:27:01 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:27:01 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:29:05 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:29:05 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:29:05 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:29:06 - core.reliability - INFO - 健康检查通过
2026-01-07 17:29:06 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:29:06 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:29:06 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:29:06 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:29:06 - __main__ - INFO - 应用初始化完成
2026-01-07 17:29:06 - scheduler.scheduler - INFO - 添加定时任务: daily_crawl (0 8,14,18 * * *)
2026-01-07 17:29:06 - scheduler.scheduler - INFO - 添加定时任务: data_cleanup (0 2 * * *)
2026-01-07 17:31:58 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:31:58 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:31:58 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:31:58 - core.reliability - INFO - 健康检查通过
2026-01-07 17:31:58 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:31:58 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:31:58 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:31:58 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:31:58 - __main__ - INFO - 应用初始化完成
2026-01-07 17:34:32 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:34:32 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:34:32 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:34:32 - core.reliability - INFO - 健康检查通过
2026-01-07 17:34:32 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:34:32 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:34:32 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:34:32 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:34:32 - __main__ - INFO - 应用初始化完成
2026-01-07 17:34:35 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:34:35 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:34:35 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:34:35 - core.reliability - INFO - 健康检查通过
2026-01-07 17:34:35 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:34:35 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:34:35 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:34:35 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:34:35 - __main__ - INFO - 应用初始化完成
2026-01-07 17:34:35 - __main__ - INFO - 开始执行爬取任务
2026-01-07 17:34:35 - crawler.spider - INFO - 开始爬取 13 个公告来源
2026-01-07 17:34:35 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-01-07 17:35:05 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:05 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 100 条公告,耗时 30.44秒
2026-01-07 17:35:05 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-01-07 17:35:06 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:06 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.26秒
2026-01-07 17:35:06 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-01-07 17:35:06 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:06 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.20秒
2026-01-07 17:35:06 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-01-07 17:35:06 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:06 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.23秒
2026-01-07 17:35:06 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
2026-01-07 17:35:06 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:06 - gx_gp_monitor - INFO - 招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.18秒
2026-01-07 17:35:06 - gx_gp_monitor - INFO - 开始爬取 单一来源公示
2026-01-07 17:35:07 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:07 - gx_gp_monitor - INFO - 单一来源公示 爬取完成,共获取 100 条公告,耗时 0.18秒
2026-01-07 17:35:07 - gx_gp_monitor - INFO - 开始爬取 电子卖场公示
2026-01-07 17:35:07 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:07 - gx_gp_monitor - INFO - 电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.34秒
2026-01-07 17:35:07 - gx_gp_monitor - INFO - 开始爬取 履约验收公示
2026-01-07 17:35:07 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:07 - gx_gp_monitor - INFO - 履约验收公示 爬取完成,共获取 100 条公告,耗时 0.21秒
2026-01-07 17:35:07 - gx_gp_monitor - INFO - 开始爬取 工程类公告
2026-01-07 17:35:07 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:07 - gx_gp_monitor - INFO - 工程类公告 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-07 17:35:07 - gx_gp_monitor - INFO - 开始爬取 框架协议征集公告
2026-01-07 17:35:07 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:07 - gx_gp_monitor - INFO - 框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-07 17:35:07 - gx_gp_monitor - INFO - 开始爬取 框架协议入围结果公告
2026-01-07 17:35:08 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:08 - gx_gp_monitor - INFO - 框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.18秒
2026-01-07 17:35:08 - gx_gp_monitor - INFO - 开始爬取 框架协议成交结果汇总公告
2026-01-07 17:35:08 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:08 - gx_gp_monitor - INFO - 框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.25秒
2026-01-07 17:35:08 - gx_gp_monitor - INFO - 开始爬取 采购意向公开
2026-01-07 17:35:08 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:35:08 - gx_gp_monitor - INFO - 采购意向公开 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-07 17:35:08 - crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告
2026-01-07 17:35:08 - __main__ - INFO - 爬取到 1300 条原始公告
2026-01-07 17:35:08 - filters.filters - INFO - 来源筛选: 1300 -> 1300 条公告
2026-01-07 17:35:08 - filters.filters - INFO - 去重筛选: 移除了 18 条重复公告
2026-01-07 17:35:08 - filters.filters - INFO - 关键词筛选: 1282 -> 0 条公告
2026-01-07 17:35:08 - filters.filters - INFO - 筛选完成: 总数 1300 -> 筛选后 0 (关键词: 1282, 日期: 0, 去重: 18, 来源: 0)
2026-01-07 17:35:08 - __main__ - INFO - 筛选后剩余 0 条公告
2026-01-07 17:35:08 - storage.md_generator - INFO - Markdown文件已保存到: onu.md (共 0 条公告)
2026-01-07 17:35:08 - __main__ - INFO - 爬取任务完成: {'success': True, 'total_crawled': 1300, 'filtered': 0, 'saved': 0, 'markdown_generated': True, 'notification_sent': False, 'filter_stats': {'keyword_filtered': 1282, 'date_filtered': 0, 'duplicate_filtered': 18, 'source_filtered': 0}}
2026-01-07 17:35:38 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-07 17:35:38 - __main__ - INFO - 版本: 1.0.0
2026-01-07 17:35:38 - __main__ - INFO - 配置文件: 默认配置
2026-01-07 17:35:38 - core.reliability - INFO - 健康检查通过
2026-01-07 17:35:38 - core.database - INFO - 数据库连接池初始化成功
2026-01-07 17:35:38 - core.database - INFO - 开始初始化数据库表结构
2026-01-07 17:35:38 - core.database - INFO - 数据库表结构初始化完成
2026-01-07 17:35:38 - storage.postgresql - INFO - 存储初始化完成
2026-01-07 17:35:38 - __main__ - INFO - 应用初始化完成
2026-01-07 17:35:38 - __main__ - INFO - 开始执行爬取任务
2026-01-07 17:35:38 - crawler.spider - INFO - 开始爬取 13 个公告来源
2026-01-07 17:35:38 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-01-07 17:36:09 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:09 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 100 条公告,耗时 30.49秒
2026-01-07 17:36:09 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-01-07 17:36:09 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:09 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.20秒
2026-01-07 17:36:09 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-01-07 17:36:09 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:09 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-07 17:36:09 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-01-07 17:36:09 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:09 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.27秒
2026-01-07 17:36:09 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
2026-01-07 17:36:10 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:10 - gx_gp_monitor - INFO - 招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.28秒
2026-01-07 17:36:10 - gx_gp_monitor - INFO - 开始爬取 单一来源公示
2026-01-07 17:36:10 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:10 - gx_gp_monitor - INFO - 单一来源公示 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-07 17:36:10 - gx_gp_monitor - INFO - 开始爬取 电子卖场公示
2026-01-07 17:36:10 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:10 - gx_gp_monitor - INFO - 电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.36秒
2026-01-07 17:36:10 - gx_gp_monitor - INFO - 开始爬取 履约验收公示
2026-01-07 17:36:11 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:11 - gx_gp_monitor - INFO - 履约验收公示 爬取完成,共获取 100 条公告,耗时 0.27秒
2026-01-07 17:36:11 - gx_gp_monitor - INFO - 开始爬取 工程类公告
2026-01-07 17:36:11 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:11 - gx_gp_monitor - INFO - 工程类公告 爬取完成,共获取 100 条公告,耗时 0.21秒
2026-01-07 17:36:11 - gx_gp_monitor - INFO - 开始爬取 框架协议征集公告
2026-01-07 17:36:11 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:11 - gx_gp_monitor - INFO - 框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.15秒
2026-01-07 17:36:11 - gx_gp_monitor - INFO - 开始爬取 框架协议入围结果公告
2026-01-07 17:36:11 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:11 - gx_gp_monitor - INFO - 框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.24秒
2026-01-07 17:36:11 - gx_gp_monitor - INFO - 开始爬取 框架协议成交结果汇总公告
2026-01-07 17:36:11 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:11 - gx_gp_monitor - INFO - 框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.16秒
2026-01-07 17:36:11 - gx_gp_monitor - INFO - 开始爬取 采购意向公开
2026-01-07 17:36:11 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-07 17:36:11 - gx_gp_monitor - INFO - 采购意向公开 爬取完成,共获取 100 条公告,耗时 0.18秒
2026-01-07 17:36:11 - crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告
2026-01-07 17:36:11 - __main__ - INFO - 爬取到 1300 条原始公告
2026-01-07 17:36:11 - filters.filters - INFO - 来源筛选: 1300 -> 1300 条公告
2026-01-07 17:36:12 - filters.filters - INFO - 去重筛选: 移除了 18 条重复公告
2026-01-07 17:36:12 - filters.filters - INFO - 关键词筛选: 1282 -> 0 条公告
2026-01-07 17:36:12 - filters.filters - INFO - 筛选完成: 总数 1300 -> 筛选后 0 (关键词: 1282, 日期: 0, 去重: 18, 来源: 0)
2026-01-07 17:36:12 - __main__ - INFO - 筛选后剩余 0 条公告
2026-01-07 17:36:12 - storage.md_generator - INFO - Markdown文件已保存到: onu.md (共 0 条公告)
2026-01-07 17:36:12 - __main__ - INFO - 爬取任务完成: {'success': True, 'total_crawled': 1300, 'filtered': 0, 'saved': 0, 'markdown_generated': True, 'notification_sent': False, 'filter_stats': {'keyword_filtered': 1282, 'date_filtered': 0, 'duplicate_filtered': 18, 'source_filtered': 0}}
2026-01-08 08:17:30 - __main__ - INFO - === 广西政府采购网公告监控系统启动 ===
2026-01-08 08:17:30 - __main__ - INFO - 版本: 1.0.0
2026-01-08 08:17:30 - __main__ - INFO - 配置文件: 默认配置
2026-01-08 08:17:30 - core.reliability - INFO - 健康检查通过
2026-01-08 08:17:30 - core.database - INFO - 数据库连接池初始化成功
2026-01-08 08:17:30 - core.database - INFO - 开始初始化数据库表结构
2026-01-08 08:17:30 - core.database - INFO - 数据库表结构初始化完成
2026-01-08 08:17:30 - storage.postgresql - INFO - 存储初始化完成
2026-01-08 08:17:30 - __main__ - INFO - 应用初始化完成
2026-01-08 08:17:30 - __main__ - INFO - 开始执行爬取任务
2026-01-08 08:17:30 - crawler.spider - INFO - 开始爬取 13 个公告来源
2026-01-08 08:17:30 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-01-08 08:17:31 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:31 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 100 条公告,耗时 0.53秒
2026-01-08 08:17:31 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-01-08 08:17:31 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:31 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-08 08:17:31 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-01-08 08:17:31 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:31 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.20秒
2026-01-08 08:17:31 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-01-08 08:17:31 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:31 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.28秒
2026-01-08 08:17:31 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
2026-01-08 08:17:31 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:31 - gx_gp_monitor - INFO - 招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.18秒
2026-01-08 08:17:31 - gx_gp_monitor - INFO - 开始爬取 单一来源公示
2026-01-08 08:17:32 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:32 - gx_gp_monitor - INFO - 单一来源公示 爬取完成,共获取 100 条公告,耗时 0.16秒
2026-01-08 08:17:32 - gx_gp_monitor - INFO - 开始爬取 电子卖场公示
2026-01-08 08:17:32 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:32 - gx_gp_monitor - INFO - 电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.34秒
2026-01-08 08:17:32 - gx_gp_monitor - INFO - 开始爬取 履约验收公示
2026-01-08 08:17:32 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:32 - gx_gp_monitor - INFO - 履约验收公示 爬取完成,共获取 100 条公告,耗时 0.48秒
2026-01-08 08:17:32 - gx_gp_monitor - INFO - 开始爬取 工程类公告
2026-01-08 08:17:33 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:33 - gx_gp_monitor - INFO - 工程类公告 爬取完成,共获取 100 条公告,耗时 0.23秒
2026-01-08 08:17:33 - gx_gp_monitor - INFO - 开始爬取 框架协议征集公告
2026-01-08 08:17:33 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:33 - gx_gp_monitor - INFO - 框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.18秒
2026-01-08 08:17:33 - gx_gp_monitor - INFO - 开始爬取 框架协议入围结果公告
2026-01-08 08:17:33 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:33 - gx_gp_monitor - INFO - 框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.23秒
2026-01-08 08:17:33 - gx_gp_monitor - INFO - 开始爬取 框架协议成交结果汇总公告
2026-01-08 08:17:33 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:33 - gx_gp_monitor - INFO - 框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.16秒
2026-01-08 08:17:33 - gx_gp_monitor - INFO - 开始爬取 采购意向公开
2026-01-08 08:17:33 - crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-01-08 08:17:33 - gx_gp_monitor - INFO - 采购意向公开 爬取完成,共获取 100 条公告,耗时 0.19秒
2026-01-08 08:17:33 - crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告
2026-01-08 08:17:33 - __main__ - INFO - 爬取到 1300 条原始公告
2026-01-08 08:17:33 - filters.filters - INFO - 来源筛选: 1300 -> 1300 条公告
2026-01-08 08:17:34 - filters.filters - INFO - 去重筛选: 移除了 17 条重复公告
2026-01-08 08:17:34 - filters.filters - INFO - 关键词筛选: 1283 -> 0 条公告
2026-01-08 08:17:34 - filters.filters - INFO - 筛选完成: 总数 1300 -> 筛选后 0 (关键词: 1283, 日期: 0, 去重: 17, 来源: 0)
2026-01-08 08:17:34 - __main__ - INFO - 筛选后剩余 0 条公告
2026-01-08 08:17:34 - storage.md_generator - INFO - Markdown文件已保存到: onu.md (共 0 条公告)
2026-01-08 08:17:34 - __main__ - INFO - 爬取任务完成: {'success': True, 'total_crawled': 1300, 'filtered': 0, 'saved': 0, 'markdown_generated': True, 'notification_sent': False, 'filter_stats': {'keyword_filtered': 1283, 'date_filtered': 0, 'duplicate_filtered': 17, 'source_filtered': 0}}
+83 -11
View File
@@ -8,6 +8,7 @@ import sys
import argparse
import signal
from pathlib import Path
from typing import Dict, Any
# 添加项目根目录到Python路径
project_root = Path(__file__).parent
@@ -20,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
@@ -88,16 +89,23 @@ class GXGPMonitorApp:
print(f"应用初始化失败: {str(e)}", file=sys.stderr)
return False
def run_crawl(self, keywords: list = None, sources: list = None, max_pages: int = None):
"""执行爬取任务"""
def run_crawl(self, keywords: list = None, sources: list = None, max_pages: int = None, manual_crawl: bool = False):
"""执行搜索任务
Args:
keywords: 关键词列表
sources: 来源列表
max_pages: 最大页数
manual_crawl: 是否为手动搜索(只筛选今天的数据)
"""
try:
logger.info("开始执行爬取任务")
logger.info(f"开始执行搜索任务 (手动搜索: {manual_crawl})")
# 执行爬取
crawl_results = crawl_announcements(keywords, sources)
if not crawl_results:
logger.info("爬取完成:无数据")
logger.info("搜索完成:无数据")
return {"success": True, "results": []}
# 收集所有公告
@@ -106,29 +114,89 @@ class GXGPMonitorApp:
if result.announcements:
all_announcements.extend(result.announcements)
logger.info(f"爬取{len(all_announcements)} 条原始公告")
logger.info(f"搜索{len(all_announcements)} 条原始公告")
# 保存所有公告(按来源分组,每源保留最新100条)
# 保存公告到对应的专用表
if not manual_crawl:
# 自动爬取:保存到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:
# 手动搜索:保存到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:
# 手动搜索:只筛选今天的公告和用户指定的关键词,不进行去重
from .filters.filters import KeywordFilter, DateFilter, SourceFilter
from datetime import date
# 1. 日期筛选:只保留今天的公告
date_filter = DateFilter()
date_filtered = date_filter.filter_announcements(all_announcements, start_date=date.today(), end_date=date.today())
# 2. 关键词筛选
keyword_filter = KeywordFilter()
keyword_filtered = keyword_filter.filter_announcements(date_filtered, keywords=keywords or [])
# 3. 来源筛选
source_filter = SourceFilter()
filtered_announcements = source_filter.filter_announcements(keyword_filtered, sources or list(self.config.sources.keys()))
# 计算统计信息
filter_stats = type('FilterResult', (), {
"keyword_filtered": len(date_filtered) - len(keyword_filtered),
"date_filtered": len(all_announcements) - len(date_filtered),
"duplicate_filtered": 0, # 手动搜索不进行去重
"source_filtered": len(keyword_filtered) - len(filtered_announcements)
})()
else:
# 自动爬取:筛选出新公告并应用关键词筛选
# 1. 筛选出数据库中没有的新公告
new_announcements = [ann for ann in all_announcements if ann.is_new]
logger.info(f"筛选出 {len(new_announcements)} 条新公告")
# 2. 对新公告应用关键词筛选等
if new_announcements:
filter_obj = filter_from_config()
filtered_announcements, filter_stats = filter_obj.filter(all_announcements)
filtered_announcements, filter_stats = filter_obj.filter(new_announcements)
# 更新统计信息,加上未筛选的新公告数量
filter_stats.keyword_filtered += len(new_announcements) - len(filtered_announcements)
else:
filtered_announcements = []
filter_stats = type('FilterResult', (), {
"keyword_filtered": 0,
"date_filtered": 0,
"duplicate_filtered": len(all_announcements),
"source_filtered": 0
})()
logger.info(f"筛选后剩余 {len(filtered_announcements)} 条公告")
# 对于手动爬取,不保存筛选后的公告到数据库
if not manual_crawl:
# 保存筛选后的公告(用于标记关键词匹配等)
saved_count = save_announcements_to_storage(filtered_announcements)
else:
saved_count = 0
logger.info("手动爬取模式:跳过筛选后公告的数据库保存")
# 生成Markdown文件
# 生成Markdown文件(只在自动爬取时生成)
md_success = False
if not manual_crawl:
md_success = generate_onu_md(filtered_announcements)
# 发送通知
notify_success = False
if filtered_announcements and self.config.wechat_app.enabled:
if not manual_crawl and filtered_announcements and self.config.wechat_app.enabled:
# 自动爬取时发送卡片消息
notify_success = send_announcements_notification(filtered_announcements)
elif manual_crawl and filtered_announcements and self.config.wechat_app.enabled:
# 手动爬取时不在这里发送消息,由消息处理器负责发送markdown消息
notify_success = True # 标记为成功,因为消息会通过其他方式发送
result = {
"success": True,
@@ -145,6 +213,10 @@ class GXGPMonitorApp:
}
}
# 对于手动爬取,额外返回筛选后的公告列表
if manual_crawl:
result["filtered_announcements"] = filtered_announcements
logger.info(f"爬取任务完成: {result}")
return result
+30 -2
View File
@@ -383,14 +383,21 @@ class WeChatService:
if len(source_name) > 25: # 限制来源名称长度
source_name = source_name[:22] + "..."
# 根据来源代码添加前缀标识
source_prefix = ""
if announcement.source_code == 'dahuagov':
source_prefix = "【大化县政府网】"
else:
source_prefix = "【广西政府采购网】"
# 时间格式化
if announcement.publish_date:
time_str = announcement.publish_date.strftime("%Y-%m-%d %H:%M")
else:
time_str = "时间未知"
# 生成描述:类型 | 来源 | 时间(使用默认颜色)
description = f'<div style="font-size: 14px; margin-top: 8px;">{announcement_type_display} | {source_name} | {time_str}</div>'
# 生成描述:来源标识 | 类型 | 来源单位 | 时间
description = f'<div style="font-size: 14px; margin-top: 8px;">{source_prefix}{announcement_type_display} | {source_name} | {time_str}</div>'
# URL:公告详情链接
url = announcement.content_url
@@ -434,7 +441,18 @@ class WeChatService:
# 标题和概要
total_count = len(announcements)
# 检查是否包含多个来源
has_gxgp = any(a.source_code != 'dahuagov' for a in announcements)
has_dahua = any(a.source_code == 'dahuagov' for a in announcements)
if has_gxgp and has_dahua:
lines.append("# 🔔 政府采购公告更新(双源监控)")
elif has_dahua:
lines.append("# 🔔 大化县政府网采购公告更新")
else:
lines.append("# 🔔 广西政府采购网公告更新")
lines.append("")
lines.append(f"📊 **共发现 {total_count} 条新公告**")
lines.append("")
@@ -541,6 +559,16 @@ class WeChatService:
# 生成标题
total_count = len(announcements)
# 检查是否包含多个来源
has_gxgp = any(a.source_code != 'dahuagov' for a in announcements)
has_dahua = any(a.source_code == 'dahuagov' for a in announcements)
if has_gxgp and has_dahua:
title = f"🔔 政府采购公告更新 ({total_count}条) - 双源监控"
elif has_dahua:
title = f"🔔 大化县政府网采购公告更新 ({total_count}条)"
else:
title = f"🔔 广西政府采购网公告更新 ({total_count}条)"
# 生成描述HTML
-555
View File
@@ -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)
-11
View File
@@ -1,11 +0,0 @@
# 广西政府采购网公告监控
**更新时间**: 2026-01-08 08:17:34
## 无新公告
当前时间范围内没有找到符合条件的公告。
---
*由广西政府采购网公告监控系统生成*
-1
View File
@@ -1 +0,0 @@
"""调度模块"""
-372
View File
@@ -1,372 +0,0 @@
"""
调度器模块
提供定时任务调度功能,支持cron表达式和间隔执行
"""
import time
import threading
from datetime import datetime, timedelta
from typing import Callable, Dict, Any, Optional, List
import schedule
from croniter import croniter
try:
from ..core.config_manager import get_config
from ..core.logger import get_logger
except ImportError:
from core.config_manager import get_config
from core.logger import get_logger
logger = get_logger(__name__)
class TaskScheduler:
"""任务调度器"""
def __init__(self):
self.config = get_config().scheduler
self._running = False
self._thread = None
self._jobs = {}
self._job_stats = {}
# 初始化调度
if self.config.enabled:
self._load_jobs_from_config()
def _load_jobs_from_config(self):
"""从配置加载定时任务"""
for job_config in self.config.jobs:
if job_config.get("enabled", False):
job_name = job_config["name"]
cron_expr = job_config["cron"]
# 根据任务名称创建对应的任务函数
if job_name == "daily_crawl":
func = self._create_crawl_job()
elif job_name == "data_cleanup":
func = self._create_cleanup_job()
else:
logger.warning(f"未知的任务类型: {job_name}")
continue
self.add_cron_job(job_name, cron_expr, func)
def _create_crawl_job(self) -> Callable:
"""创建爬取任务"""
def crawl_job():
try:
logger.info("开始执行定时爬取任务")
# 导入这里避免循环导入
try:
from ..crawler.spider import crawl_announcements
from ..filters.filters import filter_from_config
from ..storage.postgresql import save_announcements_to_storage
from ..storage.md_generator import generate_onu_md
from ..notification.wechat import send_announcements_notification
except ImportError:
from crawler.spider import crawl_announcements
from filters.filters import filter_from_config
from storage.postgresql import save_announcements_to_storage
from storage.md_generator import generate_onu_md
from notification.wechat import send_announcements_notification
# 执行爬取
crawl_results = crawl_announcements()
if not crawl_results:
logger.info("定时爬取任务完成:无数据")
return
# 收集所有公告
all_announcements = []
for result in crawl_results:
if result.announcements:
all_announcements.extend(result.announcements)
if not all_announcements:
logger.info("定时爬取任务完成:无新公告")
return
# 筛选公告
filter_obj = filter_from_config()
filtered_announcements, filter_stats = filter_obj.filter(all_announcements)
logger.info(f"筛选结果: {len(all_announcements)} -> {len(filtered_announcements)}")
# 保存到数据库
saved_count = save_announcements_to_storage(filtered_announcements)
# 生成Markdown文件
generate_onu_md(filtered_announcements)
# 发送通知
if filtered_announcements:
send_announcements_notification(filtered_announcements)
logger.info(f"定时爬取任务完成:处理 {len(filtered_announcements)} 条公告,保存 {saved_count}")
except Exception as e:
logger.error(f"定时爬取任务执行失败: {str(e)}")
# 发送错误通知
from ..notification.wechat import send_error_alert
send_error_alert("定时爬取任务失败", str(e))
return crawl_job
def _create_cleanup_job(self) -> Callable:
"""创建数据清理任务"""
def cleanup_job():
try:
logger.info("开始执行数据清理任务")
try:
from ..storage.postgresql import cleanup_storage
except ImportError:
from storage.postgresql import cleanup_storage
# 执行清理
deleted_count = cleanup_storage()
logger.info(f"数据清理任务完成:删除 {deleted_count} 条过期数据")
# 发送通知(如果删除的数据较多)
if deleted_count > 0:
try:
from ..notification.wechat import send_system_notification
except ImportError:
from notification.wechat import send_system_notification
send_system_notification(
"数据清理完成",
f"已清理 {deleted_count} 条过期数据"
)
except Exception as e:
logger.error(f"数据清理任务执行失败: {str(e)}")
return cleanup_job
def add_cron_job(self, name: str, cron_expr: str, func: Callable) -> bool:
"""
添加cron定时任务
Args:
name: 任务名称
cron_expr: cron表达式
func: 任务函数
Returns:
bool: 添加是否成功
"""
try:
# 验证cron表达式
croniter(cron_expr)
# 添加到schedule
schedule.every().day.at("00:00").do(func) # 临时设置,会被替换
# 存储任务信息
self._jobs[name] = {
"func": func,
"cron": cron_expr,
"next_run": None,
"last_run": None,
"run_count": 0,
"error_count": 0
}
logger.info(f"添加定时任务: {name} ({cron_expr})")
return True
except Exception as e:
logger.error(f"添加定时任务失败 {name}: {str(e)}")
return False
def add_interval_job(self, name: str, interval_seconds: int, func: Callable) -> bool:
"""
添加间隔执行任务
Args:
name: 任务名称
interval_seconds: 执行间隔(秒)
func: 任务函数
Returns:
bool: 添加是否成功
"""
try:
schedule.every(interval_seconds).seconds.do(func)
self._jobs[name] = {
"func": func,
"interval": interval_seconds,
"next_run": None,
"last_run": None,
"run_count": 0,
"error_count": 0
}
logger.info(f"添加间隔任务: {name} ({interval_seconds}秒)")
return True
except Exception as e:
logger.error(f"添加间隔任务失败 {name}: {str(e)}")
return False
def remove_job(self, name: str) -> bool:
"""
移除任务
Args:
name: 任务名称
Returns:
bool: 移除是否成功
"""
if name in self._jobs:
# 注意:schedule库没有直接的移除方法
# 这里只是从我们的记录中移除
del self._jobs[name]
logger.info(f"移除任务: {name}")
return True
return False
def start(self):
"""启动调度器"""
if self._running:
logger.warning("调度器已经在运行中")
return
if not self.config.enabled:
logger.info("调度器已禁用")
return
self._running = True
self._thread = threading.Thread(target=self._run_scheduler, daemon=True)
self._thread.start()
logger.info("调度器已启动")
def stop(self):
"""停止调度器"""
if not self._running:
return
self._running = False
if self._thread and self._thread.is_alive():
self._thread.join(timeout=5)
logger.info("调度器已停止")
def _run_scheduler(self):
"""运行调度器主循环"""
logger.info("调度器主循环开始")
while self._running:
try:
schedule.run_pending()
time.sleep(1)
except Exception as e:
logger.error(f"调度器运行异常: {str(e)}")
time.sleep(5) # 出错后等待5秒再继续
logger.info("调度器主循环结束")
def run_once(self, job_name: Optional[str] = None):
"""
手动执行任务一次
Args:
job_name: 任务名称,如果为None则执行所有任务
"""
if job_name:
if job_name in self._jobs:
job_info = self._jobs[job_name]
logger.info(f"手动执行任务: {job_name}")
try:
job_info["func"]()
job_info["run_count"] += 1
job_info["last_run"] = datetime.now()
logger.info(f"任务 {job_name} 执行完成")
except Exception as e:
job_info["error_count"] += 1
logger.error(f"任务 {job_name} 执行失败: {str(e)}")
else:
logger.error(f"任务不存在: {job_name}")
else:
# 执行所有任务
for name in list(self._jobs.keys()):
self.run_once(name)
def get_status(self) -> Dict[str, Any]:
"""
获取调度器状态
Returns:
Dict[str, Any]: 状态信息
"""
jobs_status = {}
for name, job_info in self._jobs.items():
jobs_status[name] = {
"enabled": True,
"run_count": job_info.get("run_count", 0),
"error_count": job_info.get("error_count", 0),
"last_run": job_info.get("last_run").isoformat() if job_info.get("last_run") else None,
"next_run": job_info.get("next_run").isoformat() if job_info.get("next_run") else None
}
return {
"enabled": self.config.enabled,
"running": self._running,
"timezone": self.config.timezone,
"jobs": jobs_status
}
def is_running(self) -> bool:
"""检查调度器是否正在运行"""
return self._running
# 全局调度器实例
_scheduler = None
def get_scheduler() -> TaskScheduler:
"""
获取调度器实例
Returns:
TaskScheduler: 调度器实例
"""
global _scheduler
if _scheduler is None:
_scheduler = TaskScheduler()
return _scheduler
def start_scheduler():
"""启动调度器"""
scheduler = get_scheduler()
scheduler.start()
def stop_scheduler():
"""停止调度器"""
scheduler = get_scheduler()
scheduler.stop()
def run_scheduled_jobs(job_name: Optional[str] = None):
"""
手动执行定时任务
Args:
job_name: 任务名称
"""
scheduler = get_scheduler()
scheduler.run_once(job_name)
+42 -45
View File
@@ -42,7 +42,8 @@ class MarkdownGenerator:
output_path.parent.mkdir(parents=True, exist_ok=True)
def generate_markdown(self, announcements: List[Announcement],
title: str = "广西政府采购网公告监控") -> str:
title: str = "广西政府采购网公告监控",
time_period: str = None) -> str:
"""
生成Markdown内容
@@ -54,22 +55,31 @@ class MarkdownGenerator:
str: Markdown格式的文本
"""
if not announcements:
return self._generate_empty_markdown(title)
return self._generate_empty_markdown(title, time_period)
# 按来源分组
grouped_announcements = self._group_announcements_by_source(announcements)
# 生成Markdown
lines = []
lines.append(f"# {title}")
lines.append("")
lines.append(f"**更新时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
lines.append(f"**总公告数**: {len(announcements)}")
lines.append("# 搜索完成")
lines.append("")
# 生成目录
lines.extend(self._generate_toc(grouped_announcements))
lines.append("---")
# 解析标题中的关键词
keyword = "未知"
if "关键词:" in title:
keyword_part = title.split("关键词:")[-1].strip()
keyword = keyword_part.split()[0] if keyword_part else "未知"
lines.append(f"📋 关键词搜索: `{keyword}` - 总公告数: `{len(announcements)}`")
lines.append("")
# 使用传入的时间段或默认的更新时间
if time_period:
lines.append(f"**时间段: {time_period}**")
else:
lines.append(f"**更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}**")
lines.append("")
lines.append("")
# 生成各来源的公告
@@ -107,11 +117,8 @@ class MarkdownGenerator:
def _generate_source_section(self, source_name: str, announcements: List[Announcement]) -> List[str]:
"""生成来源章节"""
lines = []
anchor = self._create_anchor(source_name)
lines.append(f"## {source_name}")
lines.append("")
lines.append(f"**共 {len(announcements)} 条公告**")
lines.append(f"## {source_name} - **共 {len(announcements)} 条**")
lines.append("")
# 生成公告列表
@@ -124,59 +131,49 @@ class MarkdownGenerator:
"""生成单个公告项"""
lines = []
# 公告标题
title_prefix = ""
if self.include_today_highlight and announcement.is_today:
title_prefix = "🆕 **【今日】**"
title_line = f"### {index}. {title_prefix}[{announcement.title}]({announcement.content_url})"
# 公告标题(包含超链接)
title_line = f"### {index}. [{announcement.title}]({announcement.content_url})"
lines.append(title_line)
lines.append("")
# 公告信息
info_items = []
# 公告信息 - 简洁格式
info_parts = []
if announcement.publish_date:
publish_date = announcement.publish_date.strftime("%Y-%m-%d")
info_items.append(f"📅 发布时间: {publish_date}")
info_parts.append(publish_date)
if announcement.purchase_name:
info_items.append(f"🏢 发布单位: {announcement.purchase_name}")
info_parts.append(announcement.purchase_name)
info_items.append(f"📄 来源: {announcement.source_name}")
info_parts.append(announcement.source_name)
if announcement.crawled_at:
crawled_time = announcement.crawled_at.strftime("%m-%d %H:%M")
info_items.append(f"🤖 爬取时间: {crawled_time}")
if info_items:
lines.append(" | ".join(info_items))
if info_parts:
info_line = " | ".join(info_parts)
lines.append(info_line)
lines.append("")
# 如果是新公告,添加标记
if announcement.is_new:
lines.append("*🚀 新公告*")
lines.append("")
lines.append("---")
lines.append("")
return lines
def _generate_empty_markdown(self, title: str) -> str:
def _generate_empty_markdown(self, title: str, time_period: str = None) -> str:
"""生成空内容的Markdown"""
# 解析标题中的关键词
keyword = "未知"
if "关键词:" in title:
keyword_part = title.split("关键词:")[-1].strip()
keyword = keyword_part.split()[0] if keyword_part else "未知"
lines = [
f"# {title}",
f"📋 关键词搜索: `{keyword}` - 总公告数: `0`",
"",
f"**更新时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"**时间段: {time_period}**" if time_period else f"**更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}**",
"",
"## 无新公告",
"",
"当前时间范围内没有找到符合条件的公告",
"## 无匹配公告",
"",
"---",
"",
f"*由广西政府采购网公告监控系统生成*"
"在指定时间范围内没有找到符合条件的公告。",
""
]
return "\n".join(lines)
+161
View File
@@ -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:
"""
清理存储中的过期数据
+10
View File
@@ -261,5 +261,15 @@ def get_callback_server() -> WeChatCallbackServer:
def create_callback_app() -> Flask:
"""创建回调应用(用于外部集成)"""
# 确保配置已加载(用于uWSGI等部署环境)
from ..core.config_manager import load_config, get_config
# 检查配置是否已加载
try:
config = get_config()
except RuntimeError:
# 配置未加载,尝试加载默认配置
load_config()
server = get_callback_server()
return server.app
+31 -11
View File
@@ -34,22 +34,47 @@ class WeChatMenuManager:
self.menu_data = {
"button": [
{
"name": "操作",
"name": "监控操作",
"sub_button": [
{
"type": "click",
"name": "立即爬取",
"name": "立即搜索",
"key": "crawl_now"
},
{
"type": "click",
"name": "今日总结",
"key": "today_summary"
"name": "今日统计",
"key": "today_stats"
},
{
"type": "click",
"name": "自定义爬取",
"key": "custom_crawl"
"name": "关键词搜索",
"key": "keyword_search"
},
{
"type": "click",
"name": "最新公告",
"key": "latest_news"
}
]
},
{
"name": "系统管理",
"sub_button": [
{
"type": "click",
"name": "关键词管理",
"key": "keyword_manage"
},
{
"type": "click",
"name": "系统状态",
"key": "system_status"
},
{
"type": "click",
"name": "清理缓存",
"key": "clear_cache"
}
]
},
@@ -60,11 +85,6 @@ class WeChatMenuManager:
"type": "click",
"name": "使用说明",
"key": "help_guide"
},
{
"type": "click",
"name": "联系我们",
"key": "contact_us"
}
]
}
File diff suppressed because it is too large Load Diff
+6366 -530
View File
File diff suppressed because it is too large Load Diff
+493
View File
@@ -0,0 +1,493 @@
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 512
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: GwLOHlMWE3qBsHJ3DegXZk5RGOb0hEg2g9SbYIvxkhynxr/43t...
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: vlN4cpR79+nVcy2Cvf22D4nsbOxqtyYFlVO3nvPdIz2NLVZTCr...
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 052516f0e87dd3793a1035dccb0f3b3d9fd806de
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 060aff2e6666d1df9c957ebb61d214664c64e72f
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 052516f0e87dd3793a1035dccb0f3b3d9fd806de
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 060aff2e6666d1df9c957ebb61d214664c64e72f
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=text
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - INFO - 处理文本消息: content=河池分行大化瑶族自治县中医医院结算系
统项目..., user=NongGuangGuo
2026-05-06 15:08:19 - gx_gp_monitor.wechat.message_handler - INFO - 处理文本消息: 河池分行大化瑶族自治县中医医院结算系
统项目, user: NongGuangGuo
2026-05-06 15:08:19 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 关键词搜索: ['河池分行大化瑶族自治县中医医院结算系', '统项目']
2026-05-06 15:08:19 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=crawl_now, user=NongGuangGuo
2026-05-06 15:08:19 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: crawl_now, user: NongGuangGuo
2026-05-06 15:08:19 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 触发立即搜索
2026-05-06 15:08:19 - gx_gp_monitor.main - INFO - === 广西政府采购网公告监控系统启动 ===
2026-05-06 15:08:19 - gx_gp_monitor.main - INFO - 版本: 1.0.0
2026-05-06 15:08:19 - gx_gp_monitor.main - INFO - 配置文件: 默认配置
2026-05-06 15:08:19 - gx_gp_monitor.main - INFO - === 广西政府采购网公告监控系统启动 ===
2026-05-06 15:08:19 - gx_gp_monitor.main - INFO - 版本: 1.0.0
2026-05-06 15:08:19 - gx_gp_monitor.main - INFO - 配置文件: 默认配置
2026-05-06 15:08:19 - gx_gp_monitor.core.database - INFO - 数据库连接池初始化成功
2026-05-06 15:08:19 - gx_gp_monitor.core.database - INFO - 开始初始化数据库表结构
2026-05-06 15:08:19 - gx_gp_monitor.core.database - INFO - 数据库表结构初始化完成
2026-05-06 15:08:19 - gx_gp_monitor.storage.postgresql - INFO - 存储初始化完成
2026-05-06 15:08:19 - gx_gp_monitor.main - INFO - 应用初始化完成
2026-05-06 15:08:19 - gx_gp_monitor.main - INFO - 开始执行搜索任务 (手动搜索: False)
2026-05-06 15:08:19 - gx_gp_monitor.crawler.spider - INFO - 开始爬取 13 个公告来源
2026-05-06 15:08:19 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-05-06 15:08:19 - gx_gp_monitor.core.reliability - INFO - 健康检查通过
2026-05-06 15:08:19 - gx_gp_monitor.core.database - INFO - 开始初始化数据库表结构
2026-05-06 15:08:19 - gx_gp_monitor.core.database - INFO - 数据库表结构初始化完成
2026-05-06 15:08:19 - gx_gp_monitor.storage.postgresql - INFO - 存储初始化完成
2026-05-06 15:08:19 - gx_gp_monitor.main - INFO - 应用初始化完成
2026-05-06 15:08:19 - gx_gp_monitor.main - INFO - 开始执行搜索任务 (手动搜索: True)
2026-05-06 15:08:20 - gx_gp_monitor.crawler.spider - INFO - 开始爬取 13 个公告来源
2026-05-06 15:08:20 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-05-06 15:08:20 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:20 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 100 条公告,耗时 0.44秒
2026-05-06 15:08:20 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-05-06 15:08:20 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:20 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.40秒
2026-05-06 15:08:20 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-05-06 15:08:21 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:21 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.46秒
2026-05-06 15:08:21 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-05-06 15:08:21 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:21 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.32秒
2026-05-06 15:08:21 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
2026-05-06 15:08:21 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:21 - gx_gp_monitor - INFO - 招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.32秒
2026-05-06 15:08:21 - gx_gp_monitor - INFO - 开始爬取 单一来源公示
2026-05-06 15:08:22 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:22 - gx_gp_monitor - INFO - 单一来源公示 爬取完成,共获取 100 条公告,耗时 0.32秒
2026-05-06 15:08:22 - gx_gp_monitor - INFO - 开始爬取 电子卖场公示
2026-05-06 15:08:22 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:22 - gx_gp_monitor - INFO - 电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.50秒
2026-05-06 15:08:22 - gx_gp_monitor - INFO - 开始爬取 履约验收公示
2026-05-06 15:08:22 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:22 - gx_gp_monitor - INFO - 履约验收公示 爬取完成,共获取 100 条公告,耗时 0.27秒
2026-05-06 15:08:22 - gx_gp_monitor - INFO - 开始爬取 工程类公告
2026-05-06 15:08:23 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:23 - gx_gp_monitor - INFO - 工程类公告 爬取完成,共获取 100 条公告,耗时 0.30秒
2026-05-06 15:08:23 - gx_gp_monitor - INFO - 开始爬取 框架协议征集公告
2026-05-06 15:08:23 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:23 - gx_gp_monitor - INFO - 框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.27秒
2026-05-06 15:08:23 - gx_gp_monitor - INFO - 开始爬取 框架协议入围结果公告
2026-05-06 15:08:23 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:23 - gx_gp_monitor - INFO - 框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.26秒
2026-05-06 15:08:23 - gx_gp_monitor - INFO - 开始爬取 框架协议成交结果汇总公告
2026-05-06 15:08:24 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:24 - gx_gp_monitor - INFO - 框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.27秒
2026-05-06 15:08:24 - gx_gp_monitor - INFO - 开始爬取 采购意向公开
2026-05-06 15:08:24 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:24 - gx_gp_monitor - INFO - 采购意向公开 爬取完成,共获取 100 条公告,耗时 0.29秒
2026-05-06 15:08:24 - gx_gp_monitor.crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告
2026-05-06 15:08:24 - gx_gp_monitor.main - INFO - 搜索到 1300 条原始公告
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 开始按来源保存 1300 条公告,每个来源最多保留 100 条
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 1 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement1 保存了 1 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 1 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement2 保存了 1 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 1 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement3 保存了 1 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 1 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement4 保存了 1 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 1 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement5 保存了 1 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement6 保存了 0 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement7 保存了 0 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement10 保存了 0 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement11 保存了 0 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement20 保存了 0 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement21 保存了 0 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement23 保存了 0 条公告
2026-05-06 15:08:24 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 来源 61-266648 保存了 0 条公告
2026-05-06 15:08:24 - gx_gp_monitor.storage.postgresql - INFO - 按来源保存完成,总计保存 5 条公告
2026-05-06 15:08:24 - gx_gp_monitor.main - INFO - 保存自动爬取公告完成:共保存 5 条,按来源统计: {'ZcyAnnouncement1': 1, 'ZcyAnnouncement2': 1, 'ZcyAnnouncement3': 1, 'ZcyAnnouncement4': 1, 'ZcyAnnouncement5': 1, 'ZcyAnnouncement6': 0, 'ZcyAnnouncement7': 0, 'ZcyAnnouncement10': 0, 'ZcyAnnouncement11': 0, 'ZcyAnnouncement20': 0, 'ZcyAnnouncement21': 0, 'ZcyAnnouncement23': 0, '61-266648': 0}
2026-05-06 15:08:24 - gx_gp_monitor.main - INFO - 筛选出 1300 条新公告
2026-05-06 15:08:24 - gx_gp_monitor.filters.filters - INFO - 来源筛选: 1300 -> 1300 条公告
2026-05-06 15:08:24 - gx_gp_monitor.filters.filters - INFO - 去重筛选: 移除了 1300 条重复公告
2026-05-06 15:08:24 - gx_gp_monitor.filters.filters - INFO - 关键词筛选: 0 -> 0 条公告
2026-05-06 15:08:24 - gx_gp_monitor.filters.filters - INFO - 筛选完成: 总数 1300 -> 筛选后 0 (关键词: 0, 日期: 0, 去重: 1300, 来源: 0)
2026-05-06 15:08:24 - gx_gp_monitor.main - INFO - 筛选后剩余 0 条公告
2026-05-06 15:08:24 - gx_gp_monitor.storage.md_generator - INFO - Markdown文件已保存到: onu.md (共 0 条公告)
2026-05-06 15:08:24 - gx_gp_monitor.main - INFO - 爬取任务完成: {'success': True, 'total_crawled': 1300, 'filtered': 0, 'saved': 0, 'markdown_generated': True, 'notification_sent': False, 'filter_stats': {'keyword_filtered': 1300, 'date_filtered': 0, 'duplicate_filtered': 1300, 'source_filtered': 0}}
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: /GWEdhQRyz77Ot0gML2ySdkbya+0yS/l2Zun0R3RRbsUsiZ/5/...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: a3f07c703a094e51e6f65f10e1207347ba52e516
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: a3f07c703a094e51e6f65f10e1207347ba52e516
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_search, user=WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_search, user: WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 WeiJueSen 触发关键词搜索菜单
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: Zh2CaGZnzxNyfY2UAmovYgB312yS33AE9njwfsmdnR4Zdz6dpM...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 2b12a7b22e5b1ce6051b25e1239eb3f89e168f89
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 2b12a7b22e5b1ce6051b25e1239eb3f89e168f89
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求关键词管理
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: /GWEdhQRyz77Ot0gML2ySdkbya+0yS/l2Zun0R3RRbsUsiZ/5/...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: a3f07c703a094e51e6f65f10e1207347ba52e516
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: a3f07c703a094e51e6f65f10e1207347ba52e516
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_search, user=WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_search, user: WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 WeiJueSen 触发关键词搜索菜单
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: BShc5Ko31VKzOh2A5AV7Id4WWX6+b+i013Kb+j4gOeFD4QpMz/...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: bcacf78d1048a9a06df24a596f173cd483feff96
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: bcacf78d1048a9a06df24a596f173cd483feff96
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求关键词管理
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: Zh2CaGZnzxNyfY2UAmovYgB312yS33AE9njwfsmdnR4Zdz6dpM...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 2b12a7b22e5b1ce6051b25e1239eb3f89e168f89
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 2b12a7b22e5b1ce6051b25e1239eb3f89e168f89
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求关键词管理
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: Hj5eJcf9InnhKnkJxjUdwjrfJLtEGw9pSsTXEZUemhGwdaseGY...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: efc35b9b2fb9c388203fd0d92725e4c52e7971bd
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: efc35b9b2fb9c388203fd0d92725e4c52e7971bd
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 WeiJueSen 请求关键词管理
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: /GWEdhQRyz77Ot0gML2ySdkbya+0yS/l2Zun0R3RRbsUsiZ/5/...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: a3f07c703a094e51e6f65f10e1207347ba52e516
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: a3f07c703a094e51e6f65f10e1207347ba52e516
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_search, user=WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_search, user: WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 WeiJueSen 触发关键词搜索菜单
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: BShc5Ko31VKzOh2A5AV7Id4WWX6+b+i013Kb+j4gOeFD4QpMz/...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: bcacf78d1048a9a06df24a596f173cd483feff96
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: bcacf78d1048a9a06df24a596f173cd483feff96
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求关键词管理
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: Zh2CaGZnzxNyfY2UAmovYgB312yS33AE9njwfsmdnR4Zdz6dpM...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 2b12a7b22e5b1ce6051b25e1239eb3f89e168f89
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 2b12a7b22e5b1ce6051b25e1239eb3f89e168f89
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求关键词管理
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: Hj5eJcf9InnhKnkJxjUdwjrfJLtEGw9pSsTXEZUemhGwdaseGY...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: efc35b9b2fb9c388203fd0d92725e4c52e7971bd
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: efc35b9b2fb9c388203fd0d92725e4c52e7971bd
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 WeiJueSen 请求关键词管理
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: s2AORSQopkHXxA09QhYM2xaxUQiwXq6SbJmEVtAdVgBsKdaTyk...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 09b95c92d81b3fdf2e2ce2d39f26f3fabb702ca2
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 09b95c92d81b3fdf2e2ce2d39f26f3fabb702ca2
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=help_guide, user=WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: help_guide, user: WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 WeiJueSen 请求使用说明
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: BShc5Ko31VKzOh2A5AV7Id4WWX6+b+i013Kb+j4gOeFD4QpMz/...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: bcacf78d1048a9a06df24a596f173cd483feff96
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: bcacf78d1048a9a06df24a596f173cd483feff96
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求关键词管理
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: koSh3mf1jROPiRIjbk0r2Wxz6G45YqfC7e6FEMFpko9WHl2TaA...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: b1009660d3a9d1248e4f4ae0380f311e15a31b35
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: b1009660d3a9d1248e4f4ae0380f311e15a31b35
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求关键词管理
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: Hj5eJcf9InnhKnkJxjUdwjrfJLtEGw9pSsTXEZUemhGwdaseGY...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: efc35b9b2fb9c388203fd0d92725e4c52e7971bd
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: efc35b9b2fb9c388203fd0d92725e4c52e7971bd
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 WeiJueSen 请求关键词管理
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: s2AORSQopkHXxA09QhYM2xaxUQiwXq6SbJmEVtAdVgBsKdaTyk...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 09b95c92d81b3fdf2e2ce2d39f26f3fabb702ca2
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 09b95c92d81b3fdf2e2ce2d39f26f3fabb702ca2
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=help_guide, user=WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: help_guide, user: WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 WeiJueSen 请求使用说明
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: Wf8nBEZm2h74BAUuJO4v23ZodRVoaehzIkrZmu5znNTFx6ZviG...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 1374c68c6060fedb594170f77ac661f74115216a
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 1374c68c6060fedb594170f77ac661f74115216a
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: NongGuangGuo
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求关键词管理
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: gpwcqJRahQ4lkz8z+9dRmAKMTp6cZTj7TFaoc7dXjJ+gpiTKee...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 2f83d678f4cf2d420d13889341727e94da5f7e20
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 2f83d678f4cf2d420d13889341727e94da5f7e20
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=text
2026-05-06 15:08:24 - gx_gp_monitor.wechat.callback_server - INFO - 处理文本消息: content=你好..., user=WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 处理文本消息: 你好, user: WeiJueSen
2026-05-06 15:08:24 - gx_gp_monitor.wechat.message_handler - INFO - 用户 WeiJueSen 关键词搜索: ['你好']
2026-05-06 15:08:24 - gx_gp_monitor.main - INFO - 开始执行搜索任务 (手动搜索: True)
2026-05-06 15:08:24 - gx_gp_monitor.crawler.spider - INFO - 开始爬取 13 个公告来源
2026-05-06 15:08:24 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-05-06 15:08:25 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:25 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 100 条公告,耗时 0.45秒
2026-05-06 15:08:25 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-05-06 15:08:25 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:25 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.36秒
2026-05-06 15:08:25 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-05-06 15:08:26 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:26 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.30秒
2026-05-06 15:08:26 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-05-06 15:08:26 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:26 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.31秒
2026-05-06 15:08:26 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
2026-05-06 15:08:26 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:26 - gx_gp_monitor - INFO - 招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.31秒
2026-05-06 15:08:26 - gx_gp_monitor - INFO - 开始爬取 单一来源公示
2026-05-06 15:08:26 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:26 - gx_gp_monitor - INFO - 单一来源公示 爬取完成,共获取 100 条公告,耗时 0.30秒
2026-05-06 15:08:26 - gx_gp_monitor - INFO - 开始爬取 电子卖场公示
2026-05-06 15:08:27 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:27 - gx_gp_monitor - INFO - 电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.48秒
2026-05-06 15:08:27 - gx_gp_monitor - INFO - 开始爬取 履约验收公示
2026-05-06 15:08:27 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:27 - gx_gp_monitor - INFO - 履约验收公示 爬取完成,共获取 100 条公告,耗时 0.29秒
2026-05-06 15:08:27 - gx_gp_monitor - INFO - 开始爬取 工程类公告
2026-05-06 15:08:28 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:28 - gx_gp_monitor - INFO - 工程类公告 爬取完成,共获取 100 条公告,耗时 0.40秒
2026-05-06 15:08:28 - gx_gp_monitor - INFO - 开始爬取 框架协议征集公告
2026-05-06 15:08:28 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:28 - gx_gp_monitor - INFO - 框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.32秒
2026-05-06 15:08:28 - gx_gp_monitor - INFO - 开始爬取 框架协议入围结果公告
2026-05-06 15:08:28 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:28 - gx_gp_monitor - INFO - 框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.27秒
2026-05-06 15:08:28 - gx_gp_monitor - INFO - 开始爬取 框架协议成交结果汇总公告
2026-05-06 15:08:29 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:29 - gx_gp_monitor - INFO - 框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.29秒
2026-05-06 15:08:29 - gx_gp_monitor - INFO - 开始爬取 采购意向公开
2026-05-06 15:08:29 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:29 - gx_gp_monitor - INFO - 采购意向公开 爬取完成,共获取 100 条公告,耗时 0.31秒
2026-05-06 15:08:29 - gx_gp_monitor.crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告
2026-05-06 15:08:29 - gx_gp_monitor.main - INFO - 搜索到 1300 条原始公告
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 开始按来源保存 1300 条手动搜索公告到专用表,每个来源最多保留 100 条
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement1 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement2 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement3 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement4 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement5 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement6 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement7 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement10 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement11 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement20 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement21 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement23 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 来源 61-266648 保存了 1 条手动搜索公告
2026-05-06 15:08:29 - gx_gp_monitor.storage.postgresql - INFO - 按来源保存手动搜索公告完成,总计保存 13 条公告
2026-05-06 15:08:29 - gx_gp_monitor.main - INFO - 保存手动搜索公告完成:共保存 13 条,按来源统计: {'ZcyAnnouncement1': 1, 'ZcyAnnouncement2': 1, 'ZcyAnnouncement3': 1, 'ZcyAnnouncement4': 1, 'ZcyAnnouncement5': 1, 'ZcyAnnouncement6': 1, 'ZcyAnnouncement7': 1, 'ZcyAnnouncement10': 1, 'ZcyAnnouncement11': 1, 'ZcyAnnouncement20': 1, 'ZcyAnnouncement21': 1, 'ZcyAnnouncement23': 1, '61-266648': 1}
2026-05-06 15:08:29 - gx_gp_monitor.filters.filters - INFO - 日期筛选: 1300 -> 352 条公告
2026-05-06 15:08:29 - gx_gp_monitor.filters.filters - INFO - 关键词筛选: 352 -> 0 条公告
2026-05-06 15:08:29 - gx_gp_monitor.filters.filters - INFO - 来源筛选: 0 -> 0 条公告
2026-05-06 15:08:29 - gx_gp_monitor.main - INFO - 筛选后剩余 0 条公告
2026-05-06 15:08:29 - gx_gp_monitor.main - INFO - 手动爬取模式:跳过筛选后公告的数据库保存
2026-05-06 15:08:29 - gx_gp_monitor.main - INFO - 爬取任务完成: {'success': True, 'total_crawled': 1300, 'filtered': 0, 'saved': 0, 'markdown_generated': False, 'notification_sent': False, 'filter_stats': {'keyword_filtered': 352, 'date_filtered': 948, 'duplicate_filtered': 0, 'source_filtered': 0}, 'filtered_announcements': []}
2026-05-06 15:08:39 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:39 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: rMwd7/7jivhj3K171JP9cTR0n/vSdK/XxP5FWC4NirBXOkApNN...
2026-05-06 15:08:39 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: aff09303a74cb9b91b58bda7f544420d89e99342
2026-05-06 15:08:39 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: aff09303a74cb9b91b58bda7f544420d89e99342
2026-05-06 15:08:39 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:39 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:39 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:39 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=NongGuangGuo
2026-05-06 15:08:39 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: NongGuangGuo
2026-05-06 15:08:39 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求关键词管理
2026-05-06 15:08:46 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:08:46 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: 9BiCuWCpV4Por0LpC3Q2d+iJL/AY9a8gv724JzeANrK2O/S/9L...
2026-05-06 15:08:46 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 3b373232e89cde03fef86f3efae1d3848558428c
2026-05-06 15:08:46 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 3b373232e89cde03fef86f3efae1d3848558428c
2026-05-06 15:08:46 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:08:46 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:08:46 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:08:46 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=NongGuangGuo
2026-05-06 15:08:46 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: NongGuangGuo
2026-05-06 15:08:46 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求关键词管理
2026-05-06 15:08:50 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:50 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 100 条公告,耗时 30.48秒
2026-05-06 15:08:50 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-05-06 15:08:50 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:50 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.32秒
2026-05-06 15:08:50 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-05-06 15:08:51 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:51 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.29秒
2026-05-06 15:08:51 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-05-06 15:08:51 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:51 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.34秒
2026-05-06 15:08:51 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
2026-05-06 15:08:51 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:51 - gx_gp_monitor - INFO - 招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.32秒
2026-05-06 15:08:51 - gx_gp_monitor - INFO - 开始爬取 单一来源公示
2026-05-06 15:08:52 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:52 - gx_gp_monitor - INFO - 单一来源公示 爬取完成,共获取 100 条公告,耗时 0.28秒
2026-05-06 15:08:52 - gx_gp_monitor - INFO - 开始爬取 电子卖场公示
2026-05-06 15:08:52 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:52 - gx_gp_monitor - INFO - 电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.46秒
2026-05-06 15:08:52 - gx_gp_monitor - INFO - 开始爬取 履约验收公示
2026-05-06 15:08:52 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:52 - gx_gp_monitor - INFO - 履约验收公示 爬取完成,共获取 100 条公告,耗时 0.40秒
2026-05-06 15:08:52 - gx_gp_monitor - INFO - 开始爬取 工程类公告
2026-05-06 15:08:53 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:53 - gx_gp_monitor - INFO - 工程类公告 爬取完成,共获取 100 条公告,耗时 0.30秒
2026-05-06 15:08:53 - gx_gp_monitor - INFO - 开始爬取 框架协议征集公告
2026-05-06 15:08:53 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:53 - gx_gp_monitor - INFO - 框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.26秒
2026-05-06 15:08:53 - gx_gp_monitor - INFO - 开始爬取 框架协议入围结果公告
2026-05-06 15:08:53 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:53 - gx_gp_monitor - INFO - 框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.28秒
2026-05-06 15:08:53 - gx_gp_monitor - INFO - 开始爬取 框架协议成交结果汇总公告
2026-05-06 15:08:54 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:54 - gx_gp_monitor - INFO - 框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.32秒
2026-05-06 15:08:54 - gx_gp_monitor - INFO - 开始爬取 采购意向公开
2026-05-06 15:08:54 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:54 - gx_gp_monitor - INFO - 采购意向公开 爬取完成,共获取 100 条公告,耗时 0.30秒
2026-05-06 15:08:54 - gx_gp_monitor.crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告
2026-05-06 15:08:54 - gx_gp_monitor.main - INFO - 搜索到 1300 条原始公告
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 开始按来源保存 1300 条手动搜索公告到专用表,每个来源最多保留 100 条
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement1 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement2 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement3 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement4 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement5 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement6 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement7 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement10 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement11 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement20 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement21 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement23 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.core.database - INFO - 批量保存公告到manual_announcements完成,影响行数: 1
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 来源 61-266648 保存了 1 条手动搜索公告
2026-05-06 15:08:54 - gx_gp_monitor.storage.postgresql - INFO - 按来源保存手动搜索公告完成,总计保存 13 条公告
2026-05-06 15:08:54 - gx_gp_monitor.main - INFO - 保存手动搜索公告完成:共保存 13 条,按来源统计: {'ZcyAnnouncement1': 1, 'ZcyAnnouncement2': 1, 'ZcyAnnouncement3': 1, 'ZcyAnnouncement4': 1, 'ZcyAnnouncement5': 1, 'ZcyAnnouncement6': 1, 'ZcyAnnouncement7': 1, 'ZcyAnnouncement10': 1, 'ZcyAnnouncement11': 1, 'ZcyAnnouncement20': 1, 'ZcyAnnouncement21': 1, 'ZcyAnnouncement23': 1, '61-266648': 1}
2026-05-06 15:08:54 - gx_gp_monitor.filters.filters - INFO - 日期筛选: 1300 -> 352 条公告
2026-05-06 15:08:54 - gx_gp_monitor.filters.filters - INFO - 关键词筛选: 352 -> 0 条公告
2026-05-06 15:08:54 - gx_gp_monitor.filters.filters - INFO - 来源筛选: 0 -> 0 条公告
2026-05-06 15:08:54 - gx_gp_monitor.main - INFO - 筛选后剩余 0 条公告
2026-05-06 15:08:54 - gx_gp_monitor.main - INFO - 手动爬取模式:跳过筛选后公告的数据库保存
2026-05-06 15:08:54 - gx_gp_monitor.main - INFO - 爬取任务完成: {'success': True, 'total_crawled': 1300, 'filtered': 0, 'saved': 0, 'markdown_generated': False, 'notification_sent': False, 'filter_stats': {'keyword_filtered': 352, 'date_filtered': 948, 'duplicate_filtered': 0, 'source_filtered': 0}, 'filtered_announcements': []}
2026-05-06 15:09:28 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:09:28 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: thc6lzWswZ3ZylEM+A5xlLi2CKg5D6FMa1JFGzAuW6/IrfJMqq...
2026-05-06 15:09:28 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 16e97f27eb0f7307ebb9b87c5a072b613b2c8a18
2026-05-06 15:09:28 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 16e97f27eb0f7307ebb9b87c5a072b613b2c8a18
2026-05-06 15:09:28 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:09:28 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:09:28 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:09:28 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=help_guide, user=NongGuangGuo
2026-05-06 15:09:28 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: help_guide, user: NongGuangGuo
2026-05-06 15:09:28 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求使用说明
2026-05-06 15:09:33 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:09:33 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: 5ju3K87vTeR4WaPsI8IqATUO1g30XklsBfeGNDSzcH09aZgr3x...
2026-05-06 15:09:33 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 5c49ef855bd9a5763992d62e734aef75666a4a2c
2026-05-06 15:09:33 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 5c49ef855bd9a5763992d62e734aef75666a4a2c
2026-05-06 15:09:33 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:09:33 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:09:33 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:09:33 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=today_stats, user=WeiJueSen
2026-05-06 15:09:33 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: today_stats, user: WeiJueSen
2026-05-06 15:09:33 - gx_gp_monitor.wechat.message_handler - INFO - 用户 WeiJueSen 请求今日统计
2026-05-09 10:31:46 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 384
2026-05-09 10:31:46 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: Cu+8S6elykWeq3afFdD/lbMtOni21mETVdpRjEn+AHtiYRlBYV...
2026-05-09 10:31:46 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 21012d5bc0f6d7e71328b0ec2ccb62b938898f89
2026-05-09 10:31:46 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 21012d5bc0f6d7e71328b0ec2ccb62b938898f89
2026-05-09 10:31:46 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-09 10:31:46 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-09 10:31:46 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-09 10:31:46 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=subscribe, event_key=None, user=LuLiuCui
2026-05-09 10:31:46 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: subscribe, key: None, user: LuLiuCui
+139
View File
@@ -0,0 +1,139 @@
2026-05-06 15:07:43 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:07:43 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: vlN4cpR79+nVcy2Cvf22D4nsbOxqtyYFlVO3nvPdIz2NLVZTCr...
2026-05-06 15:07:43 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 060aff2e6666d1df9c957ebb61d214664c64e72f
2026-05-06 15:07:43 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 060aff2e6666d1df9c957ebb61d214664c64e72f
2026-05-06 15:07:43 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:07:43 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:07:43 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:07:43 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=crawl_now, user=NongGuangGuo
2026-05-06 15:07:43 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: crawl_now, user: NongGuangGuo
2026-05-06 15:07:43 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 触发立即搜索
2026-05-06 15:07:43 - gx_gp_monitor.main - INFO - 开始执行搜索任务 (手动搜索: False)
2026-05-06 15:07:43 - gx_gp_monitor.core.reliability - INFO - 健康检查通过
2026-05-06 15:07:43 - gx_gp_monitor.crawler.spider - INFO - 开始爬取 13 个公告来源
2026-05-06 15:07:43 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-05-06 15:07:44 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:44 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 100 条公告,耗时 0.53秒
2026-05-06 15:07:44 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-05-06 15:07:44 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:44 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.43秒
2026-05-06 15:07:44 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-05-06 15:07:45 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:45 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.29秒
2026-05-06 15:07:45 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-05-06 15:07:45 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:45 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.38秒
2026-05-06 15:07:45 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
2026-05-06 15:07:45 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:45 - gx_gp_monitor - INFO - 招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.31秒
2026-05-06 15:07:45 - gx_gp_monitor - INFO - 开始爬取 单一来源公示
2026-05-06 15:07:46 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:46 - gx_gp_monitor - INFO - 单一来源公示 爬取完成,共获取 100 条公告,耗时 0.34秒
2026-05-06 15:07:46 - gx_gp_monitor - INFO - 开始爬取 电子卖场公示
2026-05-06 15:07:46 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:46 - gx_gp_monitor - INFO - 电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.51秒
2026-05-06 15:07:46 - gx_gp_monitor - INFO - 开始爬取 履约验收公示
2026-05-06 15:07:47 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:47 - gx_gp_monitor - INFO - 履约验收公示 爬取完成,共获取 100 条公告,耗时 0.30秒
2026-05-06 15:07:47 - gx_gp_monitor - INFO - 开始爬取 工程类公告
2026-05-06 15:07:47 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:47 - gx_gp_monitor - INFO - 工程类公告 爬取完成,共获取 100 条公告,耗时 0.27秒
2026-05-06 15:07:47 - gx_gp_monitor - INFO - 开始爬取 框架协议征集公告
2026-05-06 15:07:47 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:47 - gx_gp_monitor - INFO - 框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.35秒
2026-05-06 15:07:47 - gx_gp_monitor - INFO - 开始爬取 框架协议入围结果公告
2026-05-06 15:07:47 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:47 - gx_gp_monitor - INFO - 框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.32秒
2026-05-06 15:07:47 - gx_gp_monitor - INFO - 开始爬取 框架协议成交结果汇总公告
2026-05-06 15:07:48 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:48 - gx_gp_monitor - INFO - 框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.35秒
2026-05-06 15:07:48 - gx_gp_monitor - INFO - 开始爬取 采购意向公开
2026-05-06 15:07:48 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:07:48 - gx_gp_monitor - INFO - 采购意向公开 爬取完成,共获取 100 条公告,耗时 0.33秒
2026-05-06 15:07:48 - gx_gp_monitor.crawler.spider - INFO - 爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告
2026-05-06 15:07:48 - gx_gp_monitor.main - INFO - 搜索到 1300 条原始公告
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 开始按来源保存 1300 条公告,每个来源最多保留 100 条
2026-05-06 15:07:48 - gx_gp_monitor.core.database - ERROR - 数据库连接错误: connection already closed
2026-05-06 15:07:48 - gx_gp_monitor.core.database - ERROR - 批量保存公告失败: connection already closed
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement1 保存了 0 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - ERROR - 数据库连接错误: connection already closed
2026-05-06 15:07:48 - gx_gp_monitor.core.database - ERROR - 批量保存公告失败: connection already closed
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement2 保存了 0 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - ERROR - 数据库连接错误: connection already closed
2026-05-06 15:07:48 - gx_gp_monitor.core.database - ERROR - 批量保存公告失败: connection already closed
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement3 保存了 0 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - ERROR - 数据库连接错误: connection already closed
2026-05-06 15:07:48 - gx_gp_monitor.core.database - ERROR - 批量保存公告失败: connection already closed
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement4 保存了 0 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - ERROR - 数据库连接错误: connection already closed
2026-05-06 15:07:48 - gx_gp_monitor.core.database - ERROR - 批量保存公告失败: connection already closed
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement5 保存了 0 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement6 保存了 0 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 1 条
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement7 保存了 1 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 1 条
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement10 保存了 1 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 1 条
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement11 保存了 1 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement20 保存了 0 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement21 保存了 0 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 0 条
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 ZcyAnnouncement23 保存了 0 条公告
2026-05-06 15:07:48 - gx_gp_monitor.core.database - INFO - 批量保存公告完成,成功保存 1 条
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 来源 61-266648 保存了 1 条公告
2026-05-06 15:07:48 - gx_gp_monitor.storage.postgresql - INFO - 按来源保存完成,总计保存 4 条公告
2026-05-06 15:07:48 - gx_gp_monitor.main - INFO - 保存自动爬取公告完成:共保存 4 条,按来源统计: {'ZcyAnnouncement1': 0, 'ZcyAnnouncement2': 0, 'ZcyAnnouncement3': 0, 'ZcyAnnouncement4': 0, 'ZcyAnnouncement5': 0, 'ZcyAnnouncement6': 0, 'ZcyAnnouncement7': 1, 'ZcyAnnouncement10': 1, 'ZcyAnnouncement11': 1, 'ZcyAnnouncement20': 0, 'ZcyAnnouncement21': 0, 'ZcyAnnouncement23': 0, '61-266648': 1}
2026-05-06 15:07:48 - gx_gp_monitor.main - INFO - 筛选出 1300 条新公告
2026-05-06 15:07:48 - gx_gp_monitor.filters.filters - INFO - 来源筛选: 1300 -> 1300 条公告
2026-05-06 15:07:49 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 512
2026-05-06 15:07:49 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: GwLOHlMWE3qBsHJ3DegXZk5RGOb0hEg2g9SbYIvxkhynxr/43t...
2026-05-06 15:07:49 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 052516f0e87dd3793a1035dccb0f3b3d9fd806de
2026-05-06 15:07:49 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 052516f0e87dd3793a1035dccb0f3b3d9fd806de
2026-05-06 15:07:49 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:07:49 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:07:49 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=text
2026-05-06 15:07:49 - gx_gp_monitor.wechat.callback_server - INFO - 处理文本消息: content=河池分行大化瑶族自治县中医医院结算系
统项目..., user=NongGuangGuo
2026-05-06 15:07:49 - gx_gp_monitor.wechat.message_handler - INFO - 处理文本消息: 河池分行大化瑶族自治县中医医院结算系
统项目, user: NongGuangGuo
2026-05-06 15:07:49 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 关键词搜索: ['河池分行大化瑶族自治县中医医院结算系', '统项目']
2026-05-06 15:07:49 - gx_gp_monitor.main - INFO - 开始执行搜索任务 (手动搜索: True)
2026-05-06 15:07:49 - gx_gp_monitor.crawler.spider - INFO - 开始爬取 13 个公告来源
2026-05-06 15:07:49 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-05-06 15:07:49 - gx_gp_monitor.filters.filters - INFO - 去重筛选: 移除了 803 条重复公告
2026-05-06 15:07:49 - gx_gp_monitor.filters.filters - INFO - 关键词筛选: 497 -> 0 条公告
2026-05-06 15:07:49 - gx_gp_monitor.filters.filters - INFO - 筛选完成: 总数 1300 -> 筛选后 0 (关键词: 497, 日期: 0, 去重: 803, 来源: 0)
2026-05-06 15:07:49 - gx_gp_monitor.main - INFO - 筛选后剩余 0 条公告
2026-05-06 15:07:49 - gx_gp_monitor.storage.md_generator - INFO - Markdown文件已保存到: onu.md (共 0 条公告)
2026-05-06 15:07:49 - gx_gp_monitor.main - INFO - 爬取任务完成: {'success': True, 'total_crawled': 1300, 'filtered': 0, 'saved': 0, 'markdown_generated': True, 'notification_sent': False, 'filter_stats': {'keyword_filtered': 1797, 'date_filtered': 0, 'duplicate_filtered': 803, 'source_filtered': 0}}
2026-05-06 15:07:51 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:07:51 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: p4+cMpoxGV5kJJ2lLlM6u6/HHN4OJMSHH/wxWwSxKkZGGxajZn...
2026-05-06 15:07:51 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: fc48810bdc9c0216f44d1adde162007b4cca0790
2026-05-06 15:07:51 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: fc48810bdc9c0216f44d1adde162007b4cca0790
2026-05-06 15:07:51 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:07:51 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:07:51 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:07:51 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=crawl_now, user=NongGuangGuo
2026-05-06 15:07:51 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: crawl_now, user: NongGuangGuo
2026-05-06 15:07:51 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 触发立即搜索
2026-05-06 15:07:51 - gx_gp_monitor.main - INFO - 开始执行搜索任务 (手动搜索: False)
2026-05-06 15:07:51 - gx_gp_monitor.crawler.spider - INFO - 开始爬取 13 个公告来源
2026-05-06 15:07:51 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-05-06 15:08:20 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:20 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 100 条公告,耗时 30.54秒
2026-05-06 15:08:20 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-05-06 15:08:20 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:20 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.34秒
2026-05-06 15:08:20 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-05-06 15:08:20 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:20 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.31秒
2026-05-06 15:08:20 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-05-06 15:08:21 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:21 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.33秒
2026-05-06 15:08:21 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
2026-05-06 15:08:21 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:21 - gx_gp_monitor - INFO - 招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.31秒
2026-05-06 15:08:21 - gx_gp_monitor - INFO - 开始爬取 单一来源公示
+48
View File
@@ -0,0 +1,48 @@
2026-05-06 15:07:40 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: QMt5ylkt6tq8Vl8TdJuv5/WIdXPum6+KEhz/iY06ZJZ6cc6oAi...
2026-05-06 15:07:40 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: d9502bdfb3f5fe20997c0668e5f7af42d6444726
2026-05-06 15:07:40 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: d9502bdfb3f5fe20997c0668e5f7af42d6444726
2026-05-06 15:07:40 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:07:40 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:07:40 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:07:40 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=keyword_manage, user=NongGuangGuo
2026-05-06 15:07:40 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: keyword_manage, user: NongGuangGuo
2026-05-06 15:07:40 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 请求关键词管理
2026-05-06 15:07:46 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:07:46 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: p4+cMpoxGV5kJJ2lLlM6u6/HHN4OJMSHH/wxWwSxKkZGGxajZn...
2026-05-06 15:07:46 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: fc48810bdc9c0216f44d1adde162007b4cca0790
2026-05-06 15:07:46 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: fc48810bdc9c0216f44d1adde162007b4cca0790
2026-05-06 15:07:46 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:07:46 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:07:46 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:07:46 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=crawl_now, user=NongGuangGuo
2026-05-06 15:07:46 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: crawl_now, user: NongGuangGuo
2026-05-06 15:07:46 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 触发立即搜索
2026-05-06 15:07:46 - gx_gp_monitor.main - INFO - 开始执行搜索任务 (手动搜索: False)
2026-05-06 15:07:46 - gx_gp_monitor.core.reliability - INFO - 健康检查通过
2026-05-06 15:07:46 - gx_gp_monitor.crawler.spider - INFO - 开始爬取 13 个公告来源
2026-05-06 15:07:46 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-05-06 15:07:48 - gx_gp_monitor.wechat.callback_server - ERROR - ✅ XML解析成功,提取的encrypt长度: 472
2026-05-06 15:07:48 - gx_gp_monitor.wechat.callback_server - ERROR - 提取的encrypt前50字符: vlN4cpR79+nVcy2Cvf22D4nsbOxqtyYFlVO3nvPdIz2NLVZTCr...
2026-05-06 15:07:48 - gx_gp_monitor.wechat.callback_server - ERROR - 计算的签名: 060aff2e6666d1df9c957ebb61d214664c64e72f
2026-05-06 15:07:48 - gx_gp_monitor.wechat.callback_server - ERROR - 接收的签名: 060aff2e6666d1df9c957ebb61d214664c64e72f
2026-05-06 15:07:48 - gx_gp_monitor.wechat.callback_server - ERROR - 签名匹配: True
2026-05-06 15:07:48 - gx_gp_monitor.wechat.callback_server - ERROR - 尝试使用默认token计算签名...
2026-05-06 15:07:48 - gx_gp_monitor.wechat.callback_server - INFO - 收到企业微信消息: 类型=event
2026-05-06 15:07:48 - gx_gp_monitor.wechat.callback_server - INFO - 处理事件消息: event=click, event_key=crawl_now, user=NongGuangGuo
2026-05-06 15:07:48 - gx_gp_monitor.wechat.message_handler - INFO - 处理事件: click, key: crawl_now, user: NongGuangGuo
2026-05-06 15:07:48 - gx_gp_monitor.wechat.message_handler - INFO - 用户 NongGuangGuo 触发立即搜索
2026-05-06 15:07:48 - gx_gp_monitor.main - INFO - 开始执行搜索任务 (手动搜索: False)
2026-05-06 15:07:48 - gx_gp_monitor.crawler.spider - INFO - 开始爬取 13 个公告来源
2026-05-06 15:07:48 - gx_gp_monitor - INFO - 开始爬取 采购公告
2026-05-06 15:08:17 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:17 - gx_gp_monitor - INFO - 采购公告 爬取完成,共获取 100 条公告,耗时 30.47秒
2026-05-06 15:08:17 - gx_gp_monitor - INFO - 开始爬取 结果公告
2026-05-06 15:08:17 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:17 - gx_gp_monitor - INFO - 结果公告 爬取完成,共获取 100 条公告,耗时 0.34秒
2026-05-06 15:08:17 - gx_gp_monitor - INFO - 开始爬取 合同公告
2026-05-06 15:08:17 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:17 - gx_gp_monitor - INFO - 合同公告 爬取完成,共获取 100 条公告,耗时 0.31秒
2026-05-06 15:08:17 - gx_gp_monitor - INFO - 开始爬取 更正公告
2026-05-06 15:08:18 - gx_gp_monitor.crawler.parsers - INFO - 成功解析 100/100 条公告记录
2026-05-06 15:08:18 - gx_gp_monitor - INFO - 更正公告 爬取完成,共获取 100 条公告,耗时 0.33秒
2026-05-06 15:08:18 - gx_gp_monitor - INFO - 开始爬取 招标文件预公示
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-625
View File
@@ -1,625 +0,0 @@
import requests
import json
import xml.etree.ElementTree as ET
import hashlib
import time
import random
import string
from config import Config
import logging
from Crypto.Cipher import AES
import base64
import socket
import struct
import urllib.parse
import redis
logger = logging.getLogger(__name__)
class WeChatService:
def __init__(self):
self.corpid = Config.WECHAT_CORPID
self.corpsecret = Config.WECHAT_CORPSECRET
self.agentid = Config.WECHAT_AGENTID
self.token = Config.WECHAT_TOKEN
self.encoding_aes_key = Config.WECHAT_ENCODING_AES_KEY
self.access_token = None
self.token_expires_at = 0
# 添加代理API配置
self.use_proxy = getattr(Config, 'USE_WECHAT_PROXY', False)
self.proxy_api_url = getattr(Config, 'WECHAT_PROXY_API_URL', 'https://api.v6ole.top')
logger.info("WeChatService初始化完成")
def _get_redis_connection(self):
"""获取Redis连接"""
try:
redis_conn = redis.Redis(
host=Config.REDIS_HOST,
port=Config.REDIS_PORT,
db=Config.REDIS_DB,
password=Config.REDIS_PASSWORD,
decode_responses=True, # 自动将响应解码为字符串
socket_timeout=5, # 设置超时时间
socket_connect_timeout=5
)
# 测试连接
redis_conn.ping()
return redis_conn
except Exception as e:
logger.error(f"Redis连接失败: {str(e)}")
raise
def get_access_token(self):
"""获取access_token"""
current_time = time.time()
# 如果access_token未过期,直接返回
if self.access_token and current_time < self.token_expires_at:
return self.access_token
try:
# 使用代理API获取access_token
if self.use_proxy:
url = f"{self.proxy_api_url}/cgi-bin/gettoken?corpid={self.corpid}&corpsecret={self.corpsecret}"
logger.info(f"使用代理API获取access_token: {url}")
else:
url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corpid}&corpsecret={self.corpsecret}"
response = requests.get(url)
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
# 转换过期时间为可读格式
expiry_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.token_expires_at))
logger.info(f"成功获取access_token,过期时间: {expiry_time}")
return self.access_token
elif result.get("errcode") == 60020 and not self.use_proxy:
# 如果是IP限制错误并且未使用代理,尝试使用代理
logger.info("IP受限,尝试使用代理API获取access_token")
self.use_proxy = True
return self.get_access_token()
else:
logger.error(f"获取access_token失败: {result}")
return None
except Exception as e:
logger.error(f"获取access_token异常: {str(e)}")
return None
def verify_url(self, signature, timestamp, nonce, echostr):
"""验证URL有效性"""
try:
# URL解码echostr
echostr = urllib.parse.unquote(echostr)
# 1. 将token、timestamp、nonce、echostr四个参数进行字典序排序
temp_list = [self.token, timestamp, nonce, echostr]
temp_list.sort()
# 2. 将四个参数字符串拼接成一个字符串进行sha1加密
temp_str = ''.join(temp_list)
hash_obj = hashlib.sha1(temp_str.encode('utf-8'))
hash_str = hash_obj.hexdigest()
# 3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
if hash_str == signature:
# 如果验证成功,需要解密echostr
if self.encoding_aes_key:
return self.decrypt_echostr(echostr)
return echostr
else:
logger.error(f"URL验证失败: signature={signature}, hash_str={hash_str}")
return "URL验证失败"
except Exception as e:
logger.error(f"URL验证异常: {str(e)}")
return "URL验证异常"
def decrypt_echostr(self, echostr):
"""解密echostr"""
try:
# 1. 对密文进行base64解码
aes_key = base64.b64decode(self.encoding_aes_key + '=')
encrypted = base64.b64decode(echostr)
# 2. 使用AES解密
cipher = AES.new(aes_key, AES.MODE_CBC, aes_key[:16])
decrypted = cipher.decrypt(encrypted)
# 3. 去除补位字符
unpad = lambda s: s[:-ord(s[len(s)-1:])]
decrypted = unpad(decrypted)
# 4. 去除16位随机字符串
content = decrypted[16:]
xml_len = socket.ntohl(struct.unpack("I", content[:4])[0])
xml_content = content[4:xml_len+4]
# 5. 验证企业ID
received_id = content[xml_len+4:].decode('utf-8')
if received_id != self.corpid:
logger.error(f"企业ID验证失败: received={received_id}, expected={self.corpid}")
return "企业ID验证失败"
return xml_content.decode('utf-8')
except Exception as e:
logger.error(f"解密echostr失败: {str(e)}")
return "解密失败"
def parse_message(self, xml_data):
"""解析接收到的XML消息"""
try:
root = ET.fromstring(xml_data)
msg = {}
for child in root:
msg[child.tag] = child.text
# 如果消息是加密的,需要解密
if 'Encrypt' in msg:
logger.info("消息已加密,开始解密")
decrypted = self.decrypt_message(msg['Encrypt'])
logger.info(f"解密后的消息: {decrypted}")
# 解析解密后的XML
decrypted_root = ET.fromstring(decrypted)
msg = {}
for child in decrypted_root:
msg[child.tag] = child.text
logger.info(f"最终解析的消息: {msg}")
return msg
except Exception as e:
logger.error(f"解析消息失败: {str(e)}")
return None
def decrypt_message(self, encrypted_msg):
"""解密消息"""
try:
# 1. 对密文进行base64解码
aes_key = base64.b64decode(self.encoding_aes_key + '=')
encrypted = base64.b64decode(encrypted_msg)
# 2. 使用AES解密
cipher = AES.new(aes_key, AES.MODE_CBC, aes_key[:16])
decrypted = cipher.decrypt(encrypted)
# 3. 去除补位字符
unpad = lambda s: s[:-ord(s[len(s)-1:])]
decrypted = unpad(decrypted)
# 4. 去除16位随机字符串
content = decrypted[16:]
xml_len = socket.ntohl(struct.unpack("I", content[:4])[0])
xml_content = content[4:xml_len+4]
# 5. 验证企业ID
received_id = content[xml_len+4:].decode('utf-8')
if received_id != self.corpid:
logger.error(f"企业ID验证失败: received={received_id}, expected={self.corpid}")
raise Exception("企业ID验证失败")
return xml_content.decode('utf-8')
except Exception as e:
logger.error(f"解密消息失败: {str(e)}")
raise
def send_text_message(self, content, to_user='@all', to_party='', to_tag=''):
"""发送文本消息"""
max_retries = 3
retry_count = 0
while retry_count < max_retries:
try:
access_token = self.get_access_token()
# 构建消息数据
data = {
"touser": to_user,
"toparty": to_party,
"totag": to_tag,
"msgtype": "text",
"agentid": self.agentid,
"text": {
"content": content
}
}
logger.info(f"发送文本消息: {data}")
# 使用代理API发送消息
if self.use_proxy:
url = f"{self.proxy_api_url}/cgi-bin/message/send?access_token={access_token}"
logger.info(f"使用代理API发送消息: {url}")
else:
url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}"
response = requests.post(url, json=data)
result = response.json()
logger.info(f"发送文本消息响应: {result}")
# 如果token过期,重新获取并重试
if result.get('errcode') == 40014:
logger.info("access_token过期,重新获取")
self.access_token = None
self.token_expires_at = 0
retry_count += 1
continue
# 如果是IP限制错误并且未使用代理,尝试使用代理
if result.get('errcode') == 60020 and not self.use_proxy:
logger.info("IP受限,尝试使用代理API发送")
self.use_proxy = True
retry_count += 1
continue
# 如果是其他错误,记录并返回
if result.get('errcode') != 0:
logger.error(f"发送消息失败: {result.get('errmsg')}")
if result.get('errcode') == 301002: # 应用ID不匹配
logger.error("应用ID不匹配,请检查配置")
break
retry_count += 1
continue
return result
except Exception as e:
logger.error(f"发送文本消息失败: {str(e)}")
retry_count += 1
if retry_count >= max_retries:
raise
time.sleep(1) # 等待1秒后重试
return {"errcode": -1, "errmsg": "发送消息失败,已达到最大重试次数"}
def send_markdown_message(self, content, to_user='@all', to_party='', to_tag=''):
"""发送markdown消息"""
try:
access_token = self.get_access_token()
data = {
"touser": to_user,
"toparty": to_party,
"totag": to_tag,
"msgtype": "markdown",
"agentid": self.agentid,
"markdown": {
"content": content
}
}
logger.info(f"发送markdown消息: {data}")
# 使用代理API发送消息
if self.use_proxy:
url = f"{self.proxy_api_url}/cgi-bin/message/send?access_token={access_token}"
logger.info(f"使用代理API发送消息: {url}")
else:
url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}"
response = requests.post(url, json=data)
result = response.json()
logger.info(f"发送markdown消息响应: {result}")
# 如果token过期,重新获取并重试
if result.get('errcode') == 40014:
logger.info("access_token过期,重新获取")
self.access_token = None
self.token_expires_at = 0
return self.send_markdown_message(content, to_user, to_party, to_tag)
# 如果是IP限制错误并且未使用代理,尝试使用代理
if result.get('errcode') == 60020 and not self.use_proxy:
logger.info("IP受限,尝试使用代理API发送")
self.use_proxy = True
return self.send_markdown_message(content, to_user, to_party, to_tag)
return result
except Exception as e:
logger.error(f"发送markdown消息失败: {str(e)}")
raise
def get_user_info(self, userid):
"""获取用户信息"""
try:
access_token = self.get_access_token()
# 使用代理API获取用户信息
if self.use_proxy:
url = f"{self.proxy_api_url}/cgi-bin/user/get?access_token={access_token}&userid={userid}"
logger.info(f"使用代理API获取用户信息: {url}")
else:
url = f"https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token={access_token}&userid={userid}"
logger.info(f"获取用户信息: {url}")
response = requests.get(url)
result = response.json()
# 如果是IP限制错误并且未使用代理,尝试使用代理
if result.get('errcode') == 60020 and not self.use_proxy:
logger.info("IP受限,尝试使用代理API获取用户信息")
self.use_proxy = True
return self.get_user_info(userid)
logger.info(f"获取用户信息响应: {result}")
return result
except Exception as e:
logger.error(f"获取用户信息失败: {str(e)}")
return {"errcode": -1, "errmsg": str(e)}
def generate_temp_token(self, user_id, user_name=None):
"""生成临时访问令牌"""
try:
# 生成随机令牌
token = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 存储令牌信息到Redis
token_data = {
'user_id': user_id,
'name': user_name or '微信用户',
'created_at': int(time.time())
}
# 使用Redis存储令牌,设置过期时间
key = f"temp_token:{token}"
redis_conn = self._get_redis_connection()
redis_conn.setex(
key,
Config.REDIS_TEMP_TOKEN_EXPIRE,
json.dumps(token_data)
)
# 验证令牌是否成功存储
stored_data = redis_conn.get(key)
if not stored_data:
logger.error("令牌存储失败")
return None
logger.info(f"成功生成临时令牌: {token}, 存储数据: {stored_data}")
return token
except Exception as e:
logger.error(f"生成临时令牌失败: {str(e)}")
return None
def verify_temp_token(self, token):
"""验证临时访问令牌"""
try:
# 从Redis中获取令牌信息
key = f"temp_token:{token}"
redis_conn = self._get_redis_connection()
token_info = redis_conn.get(key)
if not token_info:
logger.error(f"临时令牌不存在或已过期: {token}")
return None
# 解析令牌信息
token_data = json.loads(token_info)
user_id = token_data.get('user_id')
if not user_id:
logger.error(f"临时令牌中未找到用户ID: {token_info}")
return None
logger.info(f"临时令牌验证成功: {token}, 用户信息: {token_data}")
return token_data
except Exception as e:
logger.error(f"验证临时令牌失败: {str(e)}")
return None
def send_card_message(self, title, description, url, to_user):
"""发送卡片消息"""
try:
# 获取access_token
access_token = self.get_access_token()
if not access_token:
logger.error("获取access_token失败")
return None
# 构建消息内容
data = {
"touser": to_user,
"toparty": "",
"totag": "",
"msgtype": "textcard",
"agentid": self.agentid,
"textcard": {
"title": title,
"description": description,
"url": url,
"btntxt": "查看详情"
}
}
logger.info(f"发送卡片消息: {data}")
# 使用代理API发送消息
if self.use_proxy:
api_url = f"{self.proxy_api_url}/cgi-bin/message/send?access_token={access_token}"
logger.info(f"使用代理API发送消息: {api_url}")
else:
api_url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}"
# 发送消息
response = requests.post(api_url, json=data)
result = response.json()
# 如果是IP限制错误并且未使用代理,尝试使用代理
if result.get('errcode') == 60020 and not self.use_proxy:
logger.info("IP受限,尝试使用代理API发送")
self.use_proxy = True
return self.send_card_message(title, description, url, to_user)
if result.get('errcode') == 0:
logger.info(f"发送卡片消息成功: {result}")
return result
else:
logger.error(f"发送卡片消息失败: {result}")
return None
except Exception as e:
logger.error(f"发送卡片消息异常: {str(e)}")
return None
def create_menu(self):
"""创建应用菜单"""
try:
# 获取access_token
access_token = self.get_access_token()
if not access_token:
logger.error("获取access_token失败")
return None
# 菜单配置
menu_data = {
"button": [
{
"name": "设备查询",
"sub_button": [
{
"type": "click",
"name": "在线统计",
"key": "online"
},
{
"type": "click",
"name": "设备状态",
"key": "status"
}
]
},
{
"name": "设备管理",
"sub_button": [
{
"type": "click",
"name": "业务下发",
"key": "deploy"
},
{
"type": "view",
"name": "设备管理",
"url": f"{Config.BASE_URL}/devices"
},
{
"type": "click",
"name": "序列修复",
"key": "sequence_fix"
}
]
},
{
"type": "click",
"name": "帮助",
"key": "help"
}
]
}
logger.info(f"创建菜单: {menu_data}")
# 使用代理API创建菜单
if self.use_proxy:
api_url = f"{self.proxy_api_url}/cgi-bin/menu/create?access_token={access_token}&agentid={self.agentid}"
logger.info(f"使用代理API创建菜单: {api_url}")
else:
api_url = f"https://qyapi.weixin.qq.com/cgi-bin/menu/create?access_token={access_token}&agentid={self.agentid}"
# 发送请求
response = requests.post(api_url, json=menu_data)
result = response.json()
# 如果是IP限制错误并且未使用代理,尝试使用代理
if result.get('errcode') == 60020 and not self.use_proxy:
logger.info("IP受限,尝试使用代理API创建菜单")
self.use_proxy = True
return self.create_menu()
if result.get('errcode') == 0:
logger.info(f"创建菜单成功: {result}")
return result
else:
logger.error(f"创建菜单失败: {result}")
return None
except Exception as e:
logger.error(f"创建菜单异常: {str(e)}")
return None
def delete_menu(self):
"""删除应用菜单"""
try:
# 获取access_token
access_token = self.get_access_token()
if not access_token:
logger.error("获取access_token失败")
return None
# 使用代理API删除菜单
if self.use_proxy:
api_url = f"{self.proxy_api_url}/cgi-bin/menu/delete?access_token={access_token}&agentid={self.agentid}"
logger.info(f"使用代理API删除菜单: {api_url}")
else:
api_url = f"https://qyapi.weixin.qq.com/cgi-bin/menu/delete?access_token={access_token}&agentid={self.agentid}"
# 发送请求
response = requests.get(api_url)
result = response.json()
# 如果是IP限制错误并且未使用代理,尝试使用代理
if result.get('errcode') == 60020 and not self.use_proxy:
logger.info("IP受限,尝试使用代理API删除菜单")
self.use_proxy = True
return self.delete_menu()
if result.get('errcode') == 0:
logger.info(f"删除菜单成功: {result}")
return result
else:
logger.error(f"删除菜单失败: {result}")
return None
except Exception as e:
logger.error(f"删除菜单异常: {str(e)}")
return None
def get_menu(self):
"""获取应用菜单"""
try:
# 获取access_token
access_token = self.get_access_token()
if not access_token:
logger.error("获取access_token失败")
return None
# 使用代理API获取菜单
if self.use_proxy:
api_url = f"{self.proxy_api_url}/cgi-bin/menu/get?access_token={access_token}&agentid={self.agentid}"
logger.info(f"使用代理API获取菜单: {api_url}")
else:
api_url = f"https://qyapi.weixin.qq.com/cgi-bin/menu/get?access_token={access_token}&agentid={self.agentid}"
# 发送请求
response = requests.get(api_url)
result = response.json()
# 如果是IP限制错误并且未使用代理,尝试使用代理
if result.get('errcode') == 60020 and not self.use_proxy:
logger.info("IP受限,尝试使用代理API获取菜单")
self.use_proxy = True
return self.get_menu()
if result.get('errcode') == 0:
logger.info(f"获取菜单成功: {result}")
return result
else:
logger.error(f"获取菜单失败: {result}")
return None
except Exception as e:
logger.error(f"获取菜单异常: {str(e)}")
return None
-104
View File
@@ -1,104 +0,0 @@
# 广西政府采购网公告监控系统配置文件
# 复制此文件为 config.yaml 并修改相应配置
# 调试模式
debug: false
# 日志配置
log_level: INFO
log_file: logs/gx_gp_monitor.log
# 爬虫配置
crawler:
base_url: "https://zfcg.gxzf.gov.cn"
timeout: 30 # 请求超时时间(秒)
max_retries: 3 # 最大重试次数
retry_delay: 1.0 # 重试初始延迟
max_retry_delay: 60.0 # 重试最大延迟
backoff_factor: 2.0 # 退避因子
user_agents: # User-Agent列表
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15"
proxies: [] # 代理列表
request_delay: 1.0 # 请求间延迟
request_delay_max: 3.0 # 请求间最大延迟
keyword: ["大化", "信息化"] # 关键词筛选(支持多个关键词)
start_date: "" # 开始日期 (YYYY-MM-DD)
end_date: "" # 结束日期 (YYYY-MM-DD)
max_pages: 10 # 最大页数
page_size: 100 # 每页大小
# 企业微信通知配置
wechat_app:
enabled: true # 是否启用企业微信通知
corp_id: "ww69e8e44636f47780" # 企业ID
agent_id: "1000007" # 应用ID
secret: "SmelCwKFoL0E9ATWFzr-w7gsfXBTN72lT1UqnNd0HpI" # 应用Secret
token: "DmvL98cAF6x9CFtQZwqD2emGL8S7HxA" # Token
encoding_aes_key: "yAc4OoSCP92YTefHXYfw27WeG9oF11W9d6nw6QYlU3D" # 消息加密Key
port: 18001 # 服务端口
host: "0.0.0.0" # 服务主机
debug: false # 调试模式
# 数据库配置
DB_NAME=gx-gp-notify
DB_USER=gx-gp-notify
DB_PASSWORD=MA6RBX4F6Bd5DGmw
DB_HOST=10.10.10.14
DB_PORT=5432
# 公告来源配置
sources:
ZcyAnnouncement1:
category_id: 66485
name: "采购公告"
type: "purchase"
ZcyAnnouncement2:
category_id: 66485
name: "结果公告"
type: "result"
ZcyAnnouncement3:
category_id: 66485
name: "合同公告"
type: "contract"
ZcyAnnouncement4:
category_id: 66485
name: "更正公告"
type: "correction"
ZcyAnnouncement5:
category_id: 66485
name: "招标文件预公示"
type: "pre_announcement"
ZcyAnnouncement6:
category_id: 66485
name: "单一来源公示"
type: "single_source"
ZcyAnnouncement7:
category_id: 66485
name: "电子卖场公示"
type: "electronic_market"
ZcyAnnouncement10:
category_id: 66485
name: "履约验收公示"
type: "acceptance"
ZcyAnnouncement11:
category_id: 66485
name: "工程类公告"
type: "engineering"
ZcyAnnouncement20:
category_id: 66485
name: "框架协议征集公告"
type: "framework_agreement"
ZcyAnnouncement21:
category_id: 66485
name: "框架协议入围结果公告"
type: "framework_result"
ZcyAnnouncement23:
category_id: 66485
name: "框架协议成交结果汇总公告"
type: "framework_summary"
"61-266648":
category_id: 66485
name: "采购意向公开"
type: "intention"
-44
View File
@@ -1,44 +0,0 @@
import web_crawler as webc
"""脚本运行指南"""
# 整个程序的启动需点击右上角的绿色三角按钮(或使用快捷组合键Shift+F10快捷启动),注意!在启动前,请先在按钮的左侧选择“当前文件”再执行。
# 注意!!!请不要在打开公告数据导入的目标excel文件时启动该爬虫程序,程序无法对正在运行的进程文件进行修改。务必在爬虫程序启动并将数据成功导入目标excel文件后再打开并查看目标excel文件
"""脚本结果读取指南"""
# 这个脚本旨在爬取广西政府采购网的多个公告栏目的公告信息。
#该脚本在运行结束之后会返回以下结果:<某专栏> 新增 xxx 条公告,已保存至excel文件中,建议确认是否有新增公告后再查看excel文件
#每次爬取之后会把数据导出至指定excel表格,表格将会把数据按时间倒序的方式排列公告数据,同时会把属于今天的公告数据标红。excel文件默认为桌面的政府采购公告.xlsx('D:/Document/政府采购公告.xlsx')
"""脚本参数修改指南"""
# 若没有创建相应的excel表格文件,无需担心,程序会先检测是否有对应的excel文件,若不存在该文件,程序便会自动生成。
# excel表格的存取路径可以在utils.py中修改。请点击左侧的"项目"(或使用快捷组合键Alt+1打开项目栏),选择utils.py并打开。可以在这个文件中修改某些参数。
# 若需要修改公告信息的筛选条件以及启动爬虫程序的代理列表,请点击左侧的"项目"(或使用快捷组合键Alt+1打开项目栏),选择utils.py并打开。可以在这个文件中修改这些参数。
# 我已经设置了一个定时器,在每天的8:40、14:40、18:00这三个时间段,定时器会启动爬虫程序开始爬取网站。如果需要修改时间段,可以在代码的下半部分修改
# 修改参数以及代码片段后请重新启动程序。
# """脚本定时启动"""
# def job():
# print(f"任务执行于 {datetime.now()}")
# #调用爬取函数
# webc.web_crawler()
# print(f"任务完成于 {datetime.now()}")
#
# # 安排任务在每天的8:40、14:40、18:00执行
# # 此处可以修改时间段,只需修改括号内的时间即可,格式参照原先括号里的数即可,如需额外增加时间段,请复制代码:schedule.every().day.at("时间段").do(job)并粘贴到下方,可增加任意数量的时间段
# schedule.every().day.at("08:37").do(job)
# schedule.every().day.at("11:55").do(job)
# schedule.every().day.at("14:45").do(job)
# schedule.every().day.at("18:00").do(job)
#
# print("脚本定时任务已启动...")
# while True:
# schedule.run_pending()
# time.sleep(1)
"""脚本单次启动"""
def main():
print("脚本单次任务已启动...")
webc.web_crawler()
if __name__ == "__main__":
main()
-50
View File
@@ -1,50 +0,0 @@
# 定义公告导出的目标excel文件路径
FILE_PATH = "D:/Document/政府采购公告.xlsx"
# 初始化excel工作表头
HEADER = ["标题", "发布时间", "发布单位", "内容链接", "来源栏目"]
# 初始化工作表数量以及名称
sheet_names = [
"采购公告", "招标文件预公示", "采购意向公开", "结果公告", "合同公告",
"更正公告", "单一来源公示", "电子卖场公示", "履约验收公示",
"工程类公告", "框架协议征集公告", "框架协议入围结果公告",
"框架协议成交结果汇总公告", "其他"
]
# === 初始化筛选条件 ===
KEYWORD ="大化" #筛选关键词
START_DATE ="2025-12-01" #筛选最早发布日期
END_DATE ="2026-01-31" #筛选最晚发布日期
# 代理列表
PROXIES = [
#若要添加代理,请先测试代理是否能正常连接,否则请将代理置空
#"http://10.10.1.10:3218", #此为无效代理,仅作示例
#"http://user:pass@10.10.1.10:8080", #此为无效代理,仅作示例
# 带认证的代理
# 添加更多代理...
]
USER_AGENTS = [
#此为用户代理,根据实际情况修改
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
# 添加更多 User-Agent...
]
# 公告来源链接
SOURCE_URL = {
"ZcyAnnouncement2":[66485,"结果公告"],# 结果公告
"ZcyAnnouncement3":[66485,"合同公告"],# 合同公告
"ZcyAnnouncement4":[66485,"更正公告"],# 更正公告
"ZcyAnnouncement6":[66485,"单一来源公示"],# 单一来源公示
"ZcyAnnouncement7":[66485,"电子卖场公示"],# 电子卖场公示
"ZcyAnnouncement10":[66485,"履约验收公示"],# 履约验收公示
"ZcyAnnouncement11":[66485,"工程类公告"],# 工程类公告
"ZcyAnnouncement20":[66485,"框架协议征集公告"],# 框架协议征集公告
"ZcyAnnouncement21":[66485,"框架协议入围结果公告"],# 框架协议入围结果公告
"ZcyAnnouncement23":[66485,"框架协议成交结果汇总公告"],# 框架协议成交结果汇总公告
"ZcyAnnouncement1":[66485,"采购公告"],# 采购公告
"ZcyAnnouncement5":[66485,"招标文件预公示"],# 招标文件预公示
"61-266648":[66485,"采购意向公开"]# 采购意向公开
}
-191
View File
@@ -1,191 +0,0 @@
import requests
import time
from datetime import datetime
import random
from fake_useragent import UserAgent
import logging
import write_to_excel as wte
import utils as Utils
# === 获取筛选条件 ===
KEYWORD =Utils.KEYWORD #关键词筛选
START_DATE =Utils.START_DATE
END_DATE =Utils.END_DATE
#获取公告来源链接
SOURCE_URL = Utils.SOURCE_URL
# === 反扒配置 ===
#获取代理列表
PROXIES = Utils.PROXIES
USER_AGENTS = Utils.USER_AGENTS
MAX_RETRIES = 3 # 最大重连次数
DELAY_MIN, DELAY_MAX = 1, 3 # 随机延迟范围(秒)
# === 日志配置 ===
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
def get_random_user_agent():
"""随机选择 User-Agent"""
return random.choice(USER_AGENTS)
def get_random_proxy():
"""随机选择代理"""
if not PROXIES:
return None
proxy = random.choice(PROXIES)
return {"http": proxy, "https": proxy}
def check_sensitive_words(user_agent,payload,category_code,childrencode):
"""敏感词检查请求"""
# === 接口地址 ===
url = "https://zfcg.gxzf.gov.cn/portal/sensitiveWords/check"
headers = {
"User-Agent": user_agent,
"Content-Type": "application/json;charset=UTF-8",
"Origin": "https://zfcg.gxzf.gov.cn",
"Referer": f"https://zfcg.gxzf.gov.cn/site/category?parentId={category_code}&childrenCode={childrencode}",
"Cookie": "_zcy_log_client_uuid=71e283e0-23d2-11f0-844a-eb67dfa7ab64"
}
response = requests.post(url, json=payload, headers=headers)
# print("这是敏感词检查请求返回的响应预览",response.json())
return response.json()
def get_announcements(category_code, childrencode,page_no=1, session=None):
"""请求接口获取公告列表函数(带反扒机制)"""
if session is None:
session = requests.Session()
payload = {
# 接口的请求参数
"keyword": KEYWORD, #关键词筛选,此参数即为搜索框中的关键词输入值
"publishDateBegin": START_DATE, #最早日期筛选
"publishDateEnd": END_DATE, #最晚日期筛选
"pageNo": page_no, #页码数
"pageSize": 15, #页容量
"categoryCode": category_code, # 该参数指定当前查找的公告栏目
"_t": int(time.time() * 1000) # 动态时间戳
}
# 先执行敏感词检查
user_agent=get_random_user_agent()
check_response = check_sensitive_words(user_agent,payload, category_code, childrencode)
if not check_response.get("success", False):
logging.warning("敏感词检查失败")
return None
# 再执行公告数据请求
# === 接口地址 ===
api_url = "https://zfcg.gxzf.gov.cn/portal/category"
headers = {
"User-Agent": user_agent,
"Content-Type": "application/json;charset=UTF-8",
"Origin": "https://zfcg.gxzf.gov.cn",
"Referer": f"https://zfcg.gxzf.gov.cn/site/category?parentId={category_code}&childrenCode={childrencode}",
"Cookie": "_zcy_log_client_uuid=71e283e0-23d2-11f0-844a-eb67dfa7ab64" # 如果需要登录
}
#若添加了至少1条代理,则尝试使用代理
if PROXIES:
proxy = 'Default_value'
else:
proxy=None
for retry in range(MAX_RETRIES):
try:
# 尝试使用代理,失败则切换为无代理
if proxy :
proxy = get_random_proxy()
if proxy:
logging.info(f"使用代理: {proxy['http']}")
else:
logging.info("未使用代理")
response = session.post(
api_url,
json=payload,
headers=headers,
proxies=proxy,
timeout=10
)
if response.status_code == 200:
data = response.json()
if data.get("success",True):
return data
else:
logging.warning(f"接口返回失败: {data.get('error', '未知错误')}")
else:
logging.warning(f"请求失败,状态码: {response.status_code}")
except requests.exceptions.ProxyError as pe:
# 代理错误时记录并跳过,继续无代理请求
logging.error(f"代理错误: {pe}. 切换为无代理模式")
proxy = None # 下次请求不使用代理
except Exception as e:
logging.error(f"请求异常: {e}")
# 重试前等待
delay = random.uniform(DELAY_MIN, DELAY_MAX)
logging.info(f"{retry + 1}/{MAX_RETRIES} 次重试,等待 {delay:.1f} 秒...")
time.sleep(delay)
return None # 所有重试失败
def parse_data(data,category_code,source_name):
"""解析公告数据函数"""
results = []
for item in data["result"]["data"]["data"]:
results.append({
"标题": item["title"],
"发布时间": datetime.fromtimestamp(int(item["publishDate"]) / 1000).strftime("%Y-%m-%d"),
"发布单位": item["purchaseName"],
"内容链接": f"https://zfcg.gxzf.gov.cn/site/detail?parentId={category_code}&articleId={item['articleId']}",
"来源栏目":source_name
})
return results
#爬虫主函数
def web_crawler():
session = requests.Session()
# print("这是本地会话存储:",session)
# 初始化引用值为信息公告,先爬取信息公告栏目的公告数据
childrencode = "ZcyAnnouncement"
# 爬取代码主体,外循环为来源栏目的循环,即遍历需要爬取的所有来源栏目
for key in SOURCE_URL:
page = 1
all_results = []
# print("这是正在爬取的栏目", SOURCE_URL[key][1])
# print("这是正在爬取的栏目的目录码", str(key))
# 判断正确的引用值
# 内循环主体即为在符合筛选条件的公告列表中遍历所有公告,爬取每个公告需要的指定数据。
while True:
# 这是分割线
# logging.info("爬虫日志分割线————————————————————————————————————————————————爬虫日志分割线")
# logging.info("爬虫日志分割线————————————————————————————————————————————————爬虫日志分割线")
# logging.info("爬虫日志分割线————————————————————————————————————————————————爬虫日志分割线")
# logging.info(f"正在爬取第 {page} 页...")
response = get_announcements(str(key), childrencode, page,session)
if response["result"]["data"]["empty"] or not response["result"]["data"]["data"]:
break
# logging.info(f"请求后共返回 {response["result"]["data"]["total"]} 条公告")
current_data = parse_data(response, SOURCE_URL[key][0], SOURCE_URL[key][1])
if not current_data:
break
# logging.info(f"解析后共爬取 {len(current_data)} 条公告")
all_results.extend(current_data)
page += 1
# 随机延迟
delay = random.uniform(DELAY_MIN, DELAY_MAX)
time.sleep(delay)
# print(f"从 {SOURCE_URL[key][1]} 共爬取 {len(all_results)} 条公告")
# 调用excel读写函数,将公告数据筛选后保存到Excel中
wte.write_to_excel(all_results,SOURCE_URL[key][1])
# 这是分割线
print("公告栏目分割线————————————————————————————————————————————————公告栏目分割线")
print("公告栏目分割线————————————————————————————————————————————————公告栏目分割线")
print("公告栏目分割线————————————————————————————————————————————————公告栏目分割线")
-84
View File
@@ -1,84 +0,0 @@
import pandas as pd
from datetime import datetime
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
import schedule
import time
import utils as Utils
# 获取excel文件路径和excel的工作表头
FILE_PATH = Utils.FILE_PATH
HEADER = Utils.HEADER
# 获取工作表数量以及名称
sheet_names = Utils.sheet_names
def write_to_excel(results,source=""):
# 检查文件是否存在,如果不存在则创建一个新的Excel文件
try:
df_existing = pd.read_excel(FILE_PATH, sheet_name=None)
except FileNotFoundError:
df_existing = {name: pd.DataFrame(columns=HEADER) for name in sheet_names}
today_str = datetime.now().strftime('%Y-%m-%d')
aditional_item=0 # 记录本次爬取新增的公告数量
# 将爬取的公告对号入座填充至excel文件的工作表中
for result in results:
sheet_name = result["来源栏目"] if result["来源栏目"] in sheet_names else "其他"
df_sheet = df_existing[sheet_name]
# 检查是否已存在相同内容链接的记录
if not df_sheet[df_sheet["内容链接"] == result["内容链接"]].empty:
continue
# 添加新记录
new_row = pd.DataFrame([result])
df_sheet = pd.concat([df_sheet, new_row], ignore_index=True)
aditional_item+=1 # 若有新增公告,则加1
# 按时间降序排列
df_sheet.sort_values(by='发布时间', ascending=False, inplace=True)
# 更新现有数据
df_existing[sheet_name] = df_sheet
# 写入Excel文件
with pd.ExcelWriter(FILE_PATH, engine='openpyxl') as writer:
for sheet_name, df in df_existing.items():
df.to_excel(writer, sheet_name=sheet_name, index=False)
# 获取工作表对象
worksheet = writer.sheets[sheet_name]
# 设置列宽
for col_idx, column_width in enumerate([40, 15, 20, 30, 20]):
worksheet.column_dimensions[chr(65 + col_idx)].width = column_width
# 创建红色填充
red_fill = PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid")
# 创建边框样式
thin_border = Border(left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin'))
# 应用样式
for row in worksheet.iter_rows(min_row=1, max_col=len(HEADER), max_row=worksheet.max_row):
publish_date = row[1].value if len(row) > 1 else None
# print("这是publish_date",publish_date)
# 若公告发布日期为今天,则将其单元格背景填充至红色
if publish_date == today_str:
for cell in row:
cell.fill = red_fill
for idx, cell in enumerate(row):
alignment = Alignment(horizontal='left' if idx < 4 else 'general', vertical='top', wrap_text=True)
cell.alignment = alignment
cell.border = thin_border
print(f"<{source}> 新增 {aditional_item} 条公告,已保存至excel文件中")
+4 -7
View File
@@ -1,11 +1,8 @@
# 广西政府采购网公告监控
📋 关键词搜索: `未知` - 总公告数: `0`
**更新时间**: 2026-01-08 09:29:42
**更新时间: 2026-05-06 15:08:57**
## 无新公告
当前时间范围内没有找到符合条件的公告。
## 无匹配公告
---
*由广西政府采购网公告监控系统生成*
在指定时间范围内没有找到符合条件的公告。
+113
View File
@@ -0,0 +1,113 @@
#!/bin/bash
# 定时搜索脚本启动器
# 用于启动广西政府采购网和大化县政府网公告定时搜索任务
# 自动激活项目虚拟环境并运行Python脚本
# 设置脚本遇到错误时退出
set -e
# 获取脚本所在目录的绝对路径
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$SCRIPT_DIR"
echo "========================================"
echo "🕒 政府采购公告定时搜索任务"
echo "📂 项目目录: $PROJECT_ROOT"
echo "🌐 监控网站:"
echo " - 广西政府采购网 (zfcg.gxzf.gov.cn)"
echo " - 大化县政府网 (gxdh.gov.cn)"
echo "⏰ 开始时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "========================================"
# 检查虚拟环境
VENV_PATH="$PROJECT_ROOT/venv"
if [ ! -d "$VENV_PATH" ]; then
echo "❌ 错误: 找不到虚拟环境目录"
echo " 预期路径: $VENV_PATH"
echo " 请确保虚拟环境已正确创建"
exit 1
fi
# 检查虚拟环境激活脚本
VENV_ACTIVATE="$VENV_PATH/bin/activate"
if [ ! -f "$VENV_ACTIVATE" ]; then
echo "❌ 错误: 找不到虚拟环境激活脚本"
echo " 预期路径: $VENV_ACTIVATE"
exit 1
fi
echo "✅ 找到虚拟环境: $VENV_PATH"
# 激活虚拟环境
echo "🔧 激活虚拟环境..."
echo " 激活脚本: $VENV_ACTIVATE"
source "$VENV_ACTIVATE"
# 检查Python环境(在虚拟环境中)
if ! command -v python &> /dev/null; then
echo "❌ 错误: 虚拟环境中未找到Python命令"
exit 1
fi
# 检查Python版本(在虚拟环境中)
PYTHON_VERSION=$(python --version 2>&1 | cut -d' ' -f2 | cut -d. -f1-2)
echo "🐍 Python版本 (虚拟环境): $PYTHON_VERSION"
# 检查项目目录
if [ ! -d "$PROJECT_ROOT/gx_gp_monitor" ]; then
echo "❌ 错误: 找不到gx_gp_monitor目录"
echo " 预期路径: $PROJECT_ROOT/gx_gp_monitor"
exit 1
fi
# 检查cron_crawl.py文件
CRON_SCRIPT="$PROJECT_ROOT/gx_gp_monitor/cron_crawl.py"
if [ ! -f "$CRON_SCRIPT" ]; then
echo "❌ 错误: 找不到cron_crawl.py文件"
echo " 预期路径: $CRON_SCRIPT"
exit 1
fi
echo "✅ 环境检查通过"
# 检查当前时间是否在夜间休息时间段 (23:00 - 07:00)
CURRENT_HOUR=$(date +%H)
CURRENT_MINUTE=$(date +%M)
CURRENT_TIME=$((CURRENT_HOUR * 60 + CURRENT_MINUTE))
START_TIME=$((23 * 60 + 0)) # 23:00
END_TIME=$((7 * 60 + 0)) # 07:00
if [ $CURRENT_TIME -ge $START_TIME ] || [ $CURRENT_TIME -lt $END_TIME ]; then
echo "🌙 当前时间 $(date '+%H:%M') 在夜间休息时间段 (23:00-07:00)"
echo "💤 跳过爬取任务,直接结束"
echo "========================================"
echo "🏁 任务跳过"
echo "📅 结束时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "🔚 退出代码: 0 (跳过)"
echo "✅ 任务跳过成功"
echo "========================================"
exit 0
fi
echo "🚀 启动定时搜索脚本..."
# 切换到项目根目录
cd "$PROJECT_ROOT"
# 执行定时爬取脚本
python "$CRON_SCRIPT"
EXIT_CODE=$?
echo "========================================"
echo "🏁 任务完成"
echo "📅 结束时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "🔚 退出代码: $EXIT_CODE"
if [ $EXIT_CODE -eq 0 ]; then
echo "✅ 任务执行成功"
else
echo "❌ 任务执行失败"
fi
echo "========================================"
exit $EXIT_CODE
-63
View File
@@ -1,63 +0,0 @@
#!/bin/bash
# 广西政府采购网公告监控系统启动脚本
echo "=== 广西政府采购网公告监控系统启动脚本 ==="
# 检查Python环境
if ! command -v python3 &> /dev/null; then
echo "错误: 未找到python3,请确保Python 3.8+已安装"
exit 1
fi
# 检查虚拟环境
if [ ! -d "venv" ]; then
echo "错误: 未找到虚拟环境,请先运行: python3 -m venv venv"
exit 1
fi
# 激活虚拟环境
echo "激活虚拟环境..."
source venv/bin/activate
# 检查依赖
echo "检查依赖..."
python3 -c "import sys; sys.path.insert(0, 'gx_gp_monitor'); from core.config_manager import load_config; print('依赖检查通过')" 2>/dev/null
if [ $? -ne 0 ]; then
echo "错误: 依赖不完整,请运行: pip install -r gx_gp_monitor/requirements.txt"
exit 1
fi
# 进入项目目录
cd gx_gp_monitor
# 解析命令行参数
COMMAND=${1:-"status"}
shift
echo "执行命令: $COMMAND"
# 执行命令
case $COMMAND in
"crawl")
python3 main.py crawl "$@"
;;
"cleanup")
python3 main.py cleanup "$@"
;;
"status")
python3 main.py status "$@"
;;
*)
echo "使用方法: $0 {crawl|cleanup|status} [参数]"
echo ""
echo "示例:"
echo " $0 crawl # 执行一次爬取"
echo " $0 crawl --keywords 大化 # 带关键词爬取"
echo " $0 cleanup --days 30 # 清理30天前数据"
echo " $0 status # 查看系统状态"
exit 1
;;
esac
echo "=== 脚本执行完成 ==="
-46
View File
@@ -1,46 +0,0 @@
#!/usr/bin/env python3
"""
企业微信回调服务器启动脚本
"""
import sys
import os
# 添加项目路径
sys.path.insert(0, os.path.dirname(__file__))
try:
from gx_gp_monitor.wechat.callback_server import get_callback_server
from gx_gp_monitor.core.config_manager import load_config
from gx_gp_monitor.core.logger import get_logger
logger = get_logger(__name__)
def main():
"""启动企业微信回调服务器"""
# 加载配置
config = load_config()
# 获取回调服务器
callback_server = get_callback_server()
# 启动服务器
host = '0.0.0.0'
port = 18001
debug = config.debug if config else False
logger.info(f"启动企业微信回调服务器: {host}:{port}")
print(f"启动企业微信回调服务器: {host}:{port}")
print("回调地址: /api/v1/wechat/callback")
callback_server.run(host=host, port=port, debug=debug)
if __name__ == "__main__":
main()
except ImportError as e:
print(f"导入失败: {e}", file=sys.stderr)
print("请确保已安装所有依赖: pip install -r gx_gp_monitor/requirements.txt", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"启动失败: {e}", file=sys.stderr)
sys.exit(1)
+1
View File
@@ -0,0 +1 @@
984557
+28
View File
@@ -0,0 +1,28 @@
[uwsgi]
# 企业微信回调服务器 uWSGI 配置
# 指定 WSGI 应用的入口模块和对象
module = app:app
# 项目基本配置
chdir = /home/v6ole/pyproject/GX-gp-notify
virtualenv = /home/v6ole/pyproject/GX-gp-notify/venv
http = 0.0.0.0:18001 # 指定 uWSGI 监听的 HTTP 地址和端口
master = true # 启用主进程模式
processes = 2 # 设置进程数
threads = 2 # 设置每个进程的线程数
enable-threads = true # 启用线程
harakiri = 30 # 设置超时时间(秒)
http-timeout = 30 # 设置HTTP请求超时
max-requests = 5000 # 每个进程处理请求数达到此值后自动重启
reload-on-rss = 200 # 当进程占用内存超过此值(MB)时自动重启
# 指定日志文件的路径和PID文件的路径
logto = ./logs/uwsgi-wechat.log
pidfile = ./uwsgi-wechat.pid
# 当 uWSGI 退出时,自动清理其创建的套接字和 PID 文件
vacuum = true
# 添加日期到日志
log-date = true
# 自动重载和内存报告配置
; py-autoreload = 1
; memory-report = true
-53
View File
@@ -1,53 +0,0 @@
# About
weworkapi_python 是为了简化开发者对企业微信API接口的使用而设计的,API调用库系列之python版本    
本库仅做示范用,并不保证完全无bug;
作者会不定期更新本库,但不保证与官方API接口文档同步,因此一切以[官方文档](https://work.weixin.qq.com/api/doc)为准。
更多来自个人开发者的其它语言的库推荐:
python : https://github.com/sbzhu/weworkapi_python abelzhu@tencent.com(企业微信团队)
ruby https://github.com/mycolorway/wework MyColorway(个人开发者)
php : https://github.com/sbzhu/weworkapi_php abelzhu@tencent.com(企业微信团队)
golang : https://github.com/sbzhu/weworkapi_golang ryanjelin@tencent.com(企业微信团队)
golang : https://github.com/doubliekill/EnterpriseWechatSDK 1006401052yh@gmail.com(个人开发者)
# Director
├── api // API 接口
│   ├── examples // API接口的测试用例
│   ├── README.md
│   └── src // API接口的关键逻辑
├── callback // 加解密库,python2, xml格式)
├── callback_json // 加解密库,Python2, json格式, 仅适用于企业机器人/智能机器人
├── callback_json_python3 // 加解密库,Python3, json格式, 仅适用于企业机器人/智能机器人
├── callback_python3 // 加解密库,python2, xml格式
├── conf.py
└── README.md
# Usage
将本项目下载到你的目录,既可直接引用相关文件  
详细使用方法参考examples路径下的测试用例
# 关于token的缓存
token是需要缓存的,不能每次调用都去获取token,[否则会中频率限制](https://work.weixin.qq.com/api/doc#10013/%E7%AC%AC%E5%9B%9B%E6%AD%A5%EF%BC%9A%E7%BC%93%E5%AD%98%E5%92%8C%E5%88%B7%E6%96%B0access_token)
在本库的设计里,token是以类里的一个变量缓存的
比如api/src/CorpApi.py 里的access_token变量
在类的生命周期里,这个accessToken都是存在的, 当且仅当发现token过期,CorpAPI类会自动刷新token
刷新机制在 api/src/AbstractApi.py
所以,使用时,只需要全局实例化一个CorpAPI类,不要析构它,就可一直用它调函数,不用关心 token
```
api = CorpAPI(corpid, corpsecret);
api.dosomething()
api.dosomething()
api.dosomething()
....
```
当然,如果要更严格的做的话,建议自行修改,全局缓存token,比如存redis、存文件等,失效周期设置为2小时。
# Contact us
abelzhu@tencent.com
#
@@ -1,223 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File UserTest.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-24
#
#
import sys
sys.path.append("../src/")
import random
from CorpApi import *
from TestConf import *
## test
api = CorpApi(TestConf['CORP_ID'], TestConf['APP_SECRET'])
chatid = "test210";
try :
##
response = api.httpCall(
CORP_API_TYPE['APP_CHAT_CREATE'],
{
'name' : 'appchat_test',
'owner' : 'ZhuBiaoYi',
'userlist' : ['LiShuang', 'ZhuShengBen', 'LinJianEn', 'ZhuBiaoYi', 'XuBin', 'yangpeiyi', 'HaLuoTeQu', 'lucky', 'raindong', 'simon', 'Wang', 'ZhaoDong', 'DengLinSheng', 'Li'],
'chatid' : chatid,
})
print response
chatid = response['chatid']
except ApiException as e :
print e.errCode, e.errMsg
try :
##
response = api.httpCall(
CORP_API_TYPE['APP_CHAT_UPDATE'],
{
'chatid' : chatid,
'name' : 'appchat_test_new_name',
'owner' : 'ZhuShengBen',
'add_user_list' : ['huqiqi', 'Wang']
})
print response
##
response = api.httpCall(
CORP_API_TYPE['APP_CHAT_UPDATE'],
{
'chatid' : chatid,
'name' : '应用发消息测试',
'owner' : 'ZhuBiaoYi',
'del_user_list' : 'huqiqi',
})
print response
##
response = api.httpCall(
CORP_API_TYPE['APP_CHAT_SEND'],
{
'chatid':chatid,
'msgtype' : 'text',
'text' : {'content':'我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党'},
'climsgid' : 'climsgidclimsgid_%f' % (random.random()),
'safe' : 1,
})
print response
##
response = api.httpCall(
CORP_API_TYPE['APP_CHAT_SEND'],
{
'chatid':chatid,
'msgtype' : 'image',
'climsgid' : 'climsgidclimsgid_%f' % (random.random()),
'image' : {
'media_id':'3A9Jo9CHit_5UTfOVE38_067dUJQlLs30mOa9FC0a4jEGeoQgpLCZgc7rEza6TbfB',
},
'safe' : 1,
})
print response
##
response = api.httpCall(
CORP_API_TYPE['APP_CHAT_SEND'],
{
'chatid':chatid,
'msgtype' : 'file',
'climsgid' : 'climsgidclimsgid_%f' % (random.random()),
'file' : {
'media_id':'35L7MmcpGdyFfqjbGhbECCkGcaNsUajaPQifGLJq_H5E',
},
'safe' : 1,
})
print response
##
response = api.httpCall(
CORP_API_TYPE['APP_CHAT_SEND'],
{
'chatid':chatid,
'climsgid' : 'climsgidclimsgid_%f' % (random.random()),
'msgtype' : 'voice',
'voice' : {
'media_id':'3x1yb34061fDXjyUXy2rWNd-a-hWe-l8eTw2VKyh3bDQ',
},
'safe' : 1,
})
print response
##
response = api.httpCall(
CORP_API_TYPE['APP_CHAT_SEND'],
{
'chatid':chatid,
'climsgid' : 'climsgidclimsgid_%f' % (random.random()),
'msgtype' : 'video',
'video' : {
'media_id':'3neA1ypnC3k5QnAZqvyVvCesFYUrXietU5F-Ipnj6ZobiD-PuFlXngzPplWXibw9r',
},
'safe' : 1,
})
print response
##
response = api.httpCall(
CORP_API_TYPE['APP_CHAT_SEND'],
{
'chatid':chatid,
'climsgid' : 'climsgidclimsgid_%f' % (random.random()),
'msgtype' : 'news',
"news" : {
"articles" : [
{
"title" : "图文消息",
"description" : "今年中秋节公司有豪礼相送",
"url" : "URL",
"picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png",
"btntxt":"更多",
},
{
"title" : "图文消息",
"description" : "今年中秋节公司有豪礼相送",
"url" : "URL",
"picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png",
"btntxt":"更多",
},
{
"title" : "图文消息",
"description" : "今年中秋节公司有豪礼相送",
"url" : "URL",
"picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png",
"btntxt":"更多",
},
]},
'safe' : 1,
},
)
print response
##
response = api.httpCall(
CORP_API_TYPE['APP_CHAT_SEND'],
{
'chatid':chatid,
'msgtype' : 'textcard',
'climsgid' : 'climsgidclimsgid_%f' % (random.random()),
'textcard' : {
'title':'我是文本卡片消息',
'description' : 'aaaaaaa',
'url' : 'www.qq.com',
'btntxt' : '更多',
},
'safe' : 1,
})
print response
##
response = api.httpCall(
CORP_API_TYPE['APP_CHAT_SEND'],
{
'chatid':chatid,
"msgtype" : "mpnews",
"mpnews": {
"articles" : [
{
"title" : "图文消息(mpnews)",
"thumb_media_id" : "3uFTZs4MRTr-OwUArqaoXPyqtuedcwCUW1x4sgKcOeQc",
"author" : "author",
"content" : "content",
"digest" : "我是图文"
},
{
"title" : "图文消息(mpnews)",
"thumb_media_id" : "3uFTZs4MRTr-OwUArqaoXPyqtuedcwCUW1x4sgKcOeQc",
"author" : "author",
"content" : "content",
"digest" : "我是图文"
},
{
"title" : "图文消息(mpnews)",
"thumb_media_id" : "3uFTZs4MRTr-OwUArqaoXPyqtuedcwCUW1x4sgKcOeQc",
"author" : "author",
"content" : "content",
"digest" : "我是图文"
},
]
},
'climsgid' : 'climsgidclimsgid_%f' % (random.random()),
'safe' : 1,
})
print response
except ApiException as e :
print e.errCode, e.errMsg
@@ -1,42 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File UserTest.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-24
#
#
import sys
sys.path.append("../src/")
import random
from CorpApi import *
from TestConf import *
## test
api = CorpApi(TestConf['CORP_ID'], TestConf['APP_SECRET'])
try :
##
response = api.httpCall(
CORP_API_TYPE['MESSAGE_SEND'],
{
"touser": "ZhuShengBen",
"agentid": 1000002,
'msgtype' : 'text',
'climsgid' : 'climsgidclimsgid_%f' % (random.random()),
'text' : {
'content':'方法论',
},
'safe' : 0,
})
print response
except ApiException as e :
print e.errCode, e.errMsg
@@ -1,37 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File UserTest.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-24
#
#
import sys
sys.path.append("../src/")
import random
from CorpApi import *
from TestConf import *
## test
api = CorpApi(TestConf['CORP_ID'], TestConf['APP_SECRET'])
try :
##
response = api.httpCall(
CORP_API_TYPE['MINIPROGRAM_CODE_TO_SESSION_KEY'],
{
"js_code" : "sVqtL3itg0L30LTGJtZ_isKC0efG5FqGw470fVp8Dpw",
"grant_type" : "authorization_code"
})
print response
except ApiException as e :
print e.errCode, e.errMsg
@@ -1,51 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File ServiceCorpTest.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-24
#
#
import sys
sys.path.append("../src/")
from ServiceCorpApi import *
from TestConf import *
## 第三方服务商接口的使用方法
api = ServiceCorpApi(
"SUITE_ID",
"SUITE_SECRET",
"SUITE_TICKET"
);
try :
pre_auth_code = api.httpCall(SERVICE_CORP_API_TYPE['GET_PRE_AUTH_CODE']).get('pre_auth_code')
print pre_auth_code
except ApiException as e :
print e.errCode, e.errMsg
## 第三方服务商使用永久授权码调用企业接口的方法
api = ServiceCorpApi(
"SUITE_ID",
"SUITE_SECRET",
"SUITE_TICKET",
'AUTH_CORPID',
'PERMANENT_CODE'
);
try :
response = api.httpCall(
CORP_API_TYPE['USER_GET'],
{
'userid' : 'zhangsan',
})
print response
except ApiException as e :
print e.errCode, e.errMsg
@@ -1,30 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File ServiceProviderTest.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-26
#
#
import sys
sys.path.append("../src/")
from ServiceProviderApi import *
from TestConf import *
api = ServiceProviderApi('CORPID', 'PROVIDER_SECRET')
try :
response = api.httpCall(
SERVICE_PROVIDER_API_TYPE['GET_LOGIN_INFO'],
{
'auth_code' : 'XXXXXXX',
})
print response
except ApiException as e :
print e.errCode, e.errMsg
@@ -1,37 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File conf.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-24
#
#
# 请将下面参数改为自己的企业相关参数再进行测试
TestConf = {
# 企业的id,在管理端->"我的企业" 可以看到
"CORP_ID" : "ww55ca070cb9b7eb22",
# "通讯录同步"应用的secret, 开启api接口同步后,可以在管理端->"通讯录同步"看到
"CONTACT_SYNC_SECRET" : "ktmzrVIlUH0UW63zi7-JyzsgTL9NfwUhHde6or6zwQY",
# 某个自建应用的id及secret, 在管理端 -> 企业应用 -> 自建应用, 点进相应应用可以看到
"APP_ID" : 1000002,
"APP_SECRET" : "v1Z2KSw2WqPFECAwn2R0a1dFsanVF5sE4IE6X5ogveQ",
# 打卡应用的 id 及secrete 在管理端 -> 企业应用 -> 基础应用 -> 打卡,
# 点进去,有个"api"按钮,点开后,会看到
"CHECKIN_APP_ID" : 3010011,
"CHECKIN_APP_SECRET" : "3Qz2OGPvE1Eb6WKpEDfczvyQjL5Lr1CjrDTKn0RHdLE",
# 审批应用的 id 及secrete 在管理端 -> 企业应用 -> 基础应用 -> 审批,
# 点进去,有个"api"按钮,点开后,会看到
"APPROVAL_APP_ID" : 3010040,
"APPROVAL_APP_SECRET" : "1vrlwItWpz_5Qkud55aImQPCvpzi51H3F2j-1OQzhYE",
}
@@ -1,63 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File UserTest.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-24
#
#
import sys
sys.path.append("../src/")
from CorpApi import *
from TestConf import *
## test
api = CorpApi(TestConf['CORP_ID'], TestConf['CONTACT_SYNC_SECRET'])
try :
##
response = api.httpCall(
CORP_API_TYPE['USER_CREATE'],
{
'userid' : 'zhangsan',
'name' : 'zhangsanfeng',
'mobile' : '131488888888',
'email' : 'zhangsan@ipp.cas.cn',
'department' : 1,
})
print response
##
response = api.httpCall(
CORP_API_TYPE['USER_GET'],
{
'userid' : 'zhangsan',
})
print response
##
response = api.httpCall(
CORP_API_TYPE['USER_DELETE'],
{
'userid' : 'zhangsan',
})
print response
except ApiException as e :
print e.errCode, e.errMsg
##
response = api.httpCall(
CORP_API_TYPE['USER_DELETE'],
{
'userid' : 'zhangsan',
})
print response
@@ -1,146 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File AbstractApi.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-24
#
#
import sys
import os
import re
import json
import requests
sys.path.append("../../")
from conf import DEBUG
class ApiException(Exception) :
def __init__(self, errCode, errMsg) :
self.errCode = errCode
self.errMsg = errMsg
class AbstractApi(object) :
def __init__(self) :
return
def getAccessToken(self) :
raise NotImplementedError
def refreshAccessToken(self) :
raise NotImplementedError
def getSuiteAccessToken(self) :
raise NotImplementedError
def refreshSuiteAccessToken(self) :
raise NotImplementedError
def getProviderAccessToken(self) :
raise NotImplementedError
def refreshProviderAccessToken(self) :
raise NotImplementedError
def httpCall(self, urlType, args=None) :
shortUrl = urlType[0]
method = urlType[1]
response = {}
for retryCnt in range(0, 3) :
if 'POST' == method :
url = self.__makeUrl(shortUrl)
response = self.__httpPost(url, args)
elif 'GET' == method :
url = self.__makeUrl(shortUrl)
url = self.__appendArgs(url, args)
response = self.__httpGet(url)
else :
raise ApiException(-1, "unknown method type")
# check if token expired
if self.__tokenExpired(response.get('errcode')) :
self.__refreshToken(shortUrl)
retryCnt += 1
continue
else :
break
return self.__checkResponse(response)
@staticmethod
def __appendArgs(url, args) :
if args is None :
return url
for key, value in args.items() :
if '?' in url :
url += ('&' + key + '=' + value)
else :
url += ('?' + key + '=' + value)
return url
@staticmethod
def __makeUrl(shortUrl) :
base = "https://qyapi.weixin.qq.com"
if shortUrl[0] == '/' :
return base + shortUrl
else :
return base + '/' + shortUrl
def __appendToken(self, url) :
if 'SUITE_ACCESS_TOKEN' in url :
return url.replace('SUITE_ACCESS_TOKEN', self.getSuiteAccessToken())
elif 'PROVIDER_ACCESS_TOKEN' in url :
return url.replace('PROVIDER_ACCESS_TOKEN', self.getProviderAccessToken())
elif 'ACCESS_TOKEN' in url :
return url.replace('ACCESS_TOKEN', self.getAccessToken())
else :
return url
def __httpPost(self, url, args) :
realUrl = self.__appendToken(url)
if DEBUG is True :
print realUrl, args
return requests.post(realUrl, data = json.dumps(args, ensure_ascii = False).encode('utf-8')).json()
def __httpGet(self, url) :
realUrl = self.__appendToken(url)
if DEBUG is True :
print realUrl
return requests.get(realUrl).json()
def __post_file(self, url, media_file):
return requests.post(url, file=media_file).json()
@staticmethod
def __checkResponse(response):
errCode = response.get('errcode')
errMsg = response.get('errmsg')
if errCode is 0:
return response
else:
raise ApiException(errCode, errMsg)
@staticmethod
def __tokenExpired(errCode) :
if errCode == 40014 or errCode == 42001 or errCode == 42007 or errCode == 42009 :
return True
else :
return False
def __refreshToken(self, url) :
if 'SUITE_ACCESS_TOKEN' in url :
self.refreshSuiteAccessToken()
elif 'PROVIDER_ACCESS_TOKEN' in url :
self.refreshProviderAccessToken()
elif 'ACCESS_TOKEN' in url :
self.refreshAccessToken()
-104
View File
@@ -1,104 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File CorpApi.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-24
#
#
from AbstractApi import *
CORP_API_TYPE = {
'GET_ACCESS_TOKEN' : ['/cgi-bin/gettoken', 'GET'],
'USER_CREATE' : ['/cgi-bin/user/create?access_token=ACCESS_TOKEN', 'POST'],
'USER_GET' : ['/cgi-bin/user/get?access_token=ACCESS_TOKEN', 'GET'],
'USER_UPDATE' : ['/cgi-bin/user/update?access_token=ACCESS_TOKEN', 'POST'],
'USER_DELETE' : ['/cgi-bin/user/delete?access_token=ACCESS_TOKEN', 'GET'],
'USER_BATCH_DELETE': ['/cgi-bin/user/batchdelete?access_token=ACCESS_TOKEN', 'POST'],
'USER_SIMPLE_LIST': ['/cgi-bin/user/simplelist?access_token=ACCESS_TOKEN', 'GET'],
'USER_LIST' : ['/cgi-bin/user/list?access_token=ACCESS_TOKEN', 'GET'],
'USERID_TO_OPENID' : ['/cgi-bin/user/convert_to_openid?access_token=ACCESS_TOKEN', 'POST'],
'OPENID_TO_USERID' : ['/cgi-bin/user/convert_to_userid?access_token=ACCESS_TOKEN', 'POST'],
'USER_AUTH_SUCCESS': ['/cgi-bin/user/authsucc?access_token=ACCESS_TOKEN', 'GET'],
'DEPARTMENT_CREATE': ['/cgi-bin/department/create?access_token=ACCESS_TOKEN', 'POST'],
'DEPARTMENT_UPDATE': ['/cgi-bin/department/update?access_token=ACCESS_TOKEN', 'POST'],
'DEPARTMENT_DELETE': ['/cgi-bin/department/delete?access_token=ACCESS_TOKEN', 'GET'],
'DEPARTMENT_LIST' : ['/cgi-bin/department/list?access_token=ACCESS_TOKEN', 'GET'],
'TAG_CREATE' : ['/cgi-bin/tag/create?access_token=ACCESS_TOKEN', 'POST'],
'TAG_UPDATE' : ['/cgi-bin/tag/update?access_token=ACCESS_TOKEN', 'POST'],
'TAG_DELETE' : ['/cgi-bin/tag/delete?access_token=ACCESS_TOKEN', 'GET'],
'TAG_GET_USER' : ['/cgi-bin/tag/get?access_token=ACCESS_TOKEN', 'GET'],
'TAG_ADD_USER' : ['/cgi-bin/tag/addtagusers?access_token=ACCESS_TOKEN', 'POST'],
'TAG_DELETE_USER' : ['/cgi-bin/tag/deltagusers?access_token=ACCESS_TOKEN', 'POST'],
'TAG_GET_LIST' : ['/cgi-bin/tag/list?access_token=ACCESS_TOKEN', 'GET'],
'BATCH_JOB_GET_RESULT' : ['/cgi-bin/batch/getresult?access_token=ACCESS_TOKEN', 'GET'],
'BATCH_INVITE' : ['/cgi-bin/batch/invite?access_token=ACCESS_TOKEN', 'POST'],
'AGENT_GET' : ['/cgi-bin/agent/get?access_token=ACCESS_TOKEN', 'GET'],
'AGENT_SET' : ['/cgi-bin/agent/set?access_token=ACCESS_TOKEN', 'POST'],
'AGENT_GET_LIST' : ['/cgi-bin/agent/list?access_token=ACCESS_TOKEN', 'GET'],
'MENU_CREATE' : ['/cgi-bin/menu/create?access_token=ACCESS_TOKEN', 'POST'], ## TODO
'MENU_GET' : ['/cgi-bin/menu/get?access_token=ACCESS_TOKEN', 'GET'],
'MENU_DELETE' : ['/cgi-bin/menu/delete?access_token=ACCESS_TOKEN', 'GET'],
'MESSAGE_SEND' : ['/cgi-bin/message/send?access_token=ACCESS_TOKEN', 'POST'],
'MESSAGE_REVOKE' : ['/cgi-bin/message/revoke?access_token=ACCESS_TOKEN', 'POST'],
'MEDIA_GET' : ['/cgi-bin/media/get?access_token=ACCESS_TOKEN', 'GET'],
'GET_USER_INFO_BY_CODE' : ['/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN', 'GET'],
'GET_USER_DETAIL' : ['/cgi-bin/user/getuserdetail?access_token=ACCESS_TOKEN', 'POST'],
'GET_TICKET' : ['/cgi-bin/ticket/get?access_token=ACCESS_TOKEN', 'GET'],
'GET_JSAPI_TICKET' : ['/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN', 'GET'],
'GET_CHECKIN_OPTION' : ['/cgi-bin/checkin/getcheckinoption?access_token=ACCESS_TOKEN', 'POST'],
'GET_CHECKIN_DATA' : ['/cgi-bin/checkin/getcheckindata?access_token=ACCESS_TOKEN', 'POST'],
'GET_APPROVAL_DATA': ['/cgi-bin/corp/getapprovaldata?access_token=ACCESS_TOKEN', 'POST'],
'GET_INVOICE_INFO' : ['/cgi-bin/card/invoice/reimburse/getinvoiceinfo?access_token=ACCESS_TOKEN', 'POST'],
'UPDATE_INVOICE_STATUS' :
['/cgi-bin/card/invoice/reimburse/updateinvoicestatus?access_token=ACCESS_TOKEN', 'POST'],
'BATCH_UPDATE_INVOICE_STATUS' :
['/cgi-bin/card/invoice/reimburse/updatestatusbatch?access_token=ACCESS_TOKEN', 'POST'],
'BATCH_GET_INVOICE_INFO' :
['/cgi-bin/card/invoice/reimburse/getinvoiceinfobatch?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_CREATE' : ['/cgi-bin/appchat/create?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_GET' : ['/cgi-bin/appchat/get?access_token=ACCESS_TOKEN', 'GET'],
'APP_CHAT_UPDATE' : ['/cgi-bin/appchat/update?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_SEND' : ['/cgi-bin/appchat/send?access_token=ACCESS_TOKEN', 'POST'],
'MINIPROGRAM_CODE_TO_SESSION_KEY' : ['/cgi-bin/miniprogram/jscode2session?access_token=ACCESS_TOKEN', 'GET'],
}
class CorpApi(AbstractApi) :
def __init__(self, corpid, secret) :
self.corpid = corpid
self.secret = secret
self.access_token = None
def getAccessToken(self) :
if self.access_token is None :
self.refreshAccessToken()
return self.access_token
def refreshAccessToken(self) :
response = self.httpCall(
CORP_API_TYPE['GET_ACCESS_TOKEN'],
{
'corpid' : self.corpid,
'corpsecret': self.secret,
})
self.access_token = response.get('access_token')
@@ -1,70 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File ServiceCorp.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-24
#
#
from CorpApi import *
SERVICE_CORP_API_TYPE = {
'GET_CORP_TOKEN' : ['/cgi-bin/service/get_corp_token?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'],
'GET_SUITE_TOKEN' : ['/cgi-bin/service/get_suite_token', 'POST'],
'GET_PRE_AUTH_CODE' : ['/cgi-bin/service/get_pre_auth_code?suite_access_token=SUITE_ACCESS_TOKEN', 'GET'],
'SET_SESSION_INFO' : ['/cgi-bin/service/set_session_info?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'],
'GET_PERMANENT_CODE': ['/cgi-bin/service/get_permanent_code?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'],
'GET_AUTH_INFO' : ['/cgi-bin/service/get_auth_info?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'],
'GET_ADMIN_LIST' : ['/cgi-bin/service/get_admin_list?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'],
'GET_USER_INFO_BY_3RD' : ['/cgi-bin/service/getuserinfo3rd?suite_access_token=SUITE_ACCESS_TOKEN', 'GET'],
'GET_USER_DETAIL_BY_3RD' : ['/cgi-bin/service/getuserdetail3rd?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'],
}
class ServiceCorpApi(CorpApi) :
def __init__(self, suite_id, suite_secret, suite_ticket, auth_corpid=None, permanent_code=None) :
self.suite_id = suite_id
self.suite_secret = suite_secret
self.suite_ticket = suite_ticket
# 调用 CorpAPI 的function 需要设置这两个参数
self.auth_corpid = auth_corpid
self.permanent_code = permanent_code
self.access_token = None
self.suite_access_token = None
## override CorpApi 的 refreshAccessToken 使用第三方服务商的方法
def getAccessToken(self) :
if self.access_token is None :
self.refreshAccessToken()
return self.access_token
def refreshAccessToken(self) :
response = self.httpCall(
SERVICE_CORP_API_TYPE['GET_CORP_TOKEN'],
{
"auth_corpid" : self.auth_corpid,
"permanent_code": self.permanent_code,
})
self.access_token = response.get('access_token')
##
def getSuiteAccessToken(self) :
if self.suite_access_token is None :
self.refreshSuiteAccessToken()
return self.suite_access_token
def refreshSuiteAccessToken(self) :
response = self.httpCall(
SERVICE_CORP_API_TYPE['GET_SUITE_TOKEN'],
{
"suite_id" : self.suite_id,
"suite_secret" : self.suite_secret,
"suite_ticket" : self.suite_ticket,
})
self.suite_access_token= response.get('suite_access_token')
@@ -1,45 +0,0 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File ServiceProviderApi.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-26
#
#
from AbstractApi import *
SERVICE_PROVIDER_API_TYPE = {
'GET_PROVIDER_TOKEN': ['/cgi-bin/service/get_provider_token', 'POST'],
'GET_LOGIN_INFO' : ['/cgi-bin/service/get_login_info?access_token=PROVIDER_ACCESS_TOKEN', 'POST'],
'GET_REGISTER_CODE' : ['/cgi-bin/service/get_register_code?provider_access_token=PROVIDER_ACCESS_TOKEN', 'POST'],
'GET_REGISTER_INFO' : ['/cgi-bin/service/get_register_info?provider_access_token=PROVIDER_ACCESS_TOKEN', 'POST'],
'SET_AGENT_SCOPE' : ['/cgi-bin/agent/set_scope', 'POST'], ### TODO
'SET_CONTACT_SYNC_SUCCESS' : ['/cgi-bin/sync/contact_sync_success', 'GET'],
}
class ServiceProviderApi(AbstractApi) :
def __init__(self, corpid, provider_secret) :
self.corpid = corpid
self.provider_secret = provider_secret
self.provider_access_token = None
def getProviderAccessToken(self) :
if self.provider_access_token is None :
self.refreshProviderAccessToken()
return self.provider_access_token
def refreshProviderAccessToken(self) :
response = self.httpCall(
SERVICE_PROVIDER_API_TYPE['GET_PROVIDER_TOKEN'],
{
'corpid' : self.corpid,
'provider_secret': self.provider_secret,
})
self.provider_access_token = response.get('provider_access_token')
@@ -1,5 +0,0 @@
注意事项
1.WXBizMsgCrypt.py文件封装了WXBizMsgCrypt接口类(Python3以及以上版本使用 WXBizMsgCrypt3.py),提供了用户接入企业微信的三个接口,Sample.py文件提供了如何使用这三个接口的示例,ierror.py提供了错误码。
2.WXBizMsgCrypt封装了VerifyURL, DecryptMsg, EncryptMsg三个接口,分别用于开发者验证回调url,收到用户回复消息的解密以及开发者回复消息的加密过程。使用方法可以参考Sample.py文件。
3.加解密协议请参考企业微信官方文档。
4.本代码用到了pycrypto第三方库,请开发者自行安装此库再使用。
-108
View File
@@ -1,108 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 03:55:41 PM CST
# File Name: Sample.py
# Description: WXBizMsgCrypt 使用demo文件
#########################################################################
from WXBizMsgCrypt import WXBizMsgCrypt
import xml.etree.cElementTree as ET
import sys
if __name__ == "__main__":
# 企业在企业微信后台上设置的密钥相关配置在这里 TODO
sToken = "xxxxxxx"
sEncodingAESKey = "xxxxxxx"
sCorpID = "ww1436e0e65a779aee"
'''
------------使用示例一:验证回调URL---------------
*企业开启回调模式时,企业号会向验证url发送一个get请求
假设点击验证时,企业收到类似请求:
* GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3&timestamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D
* HTTP/1.1 Host: qy.weixin.qq.com
接收到该请求时,企业应 1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr),
这一步注意作URL解码。
2.验证消息体签名的正确性
3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信
第2,3步可以用企业微信提供的库函数VerifyURL来实现。
'''
wxcpt=WXBizMsgCrypt(sToken,sEncodingAESKey,sCorpID)
#sVerifyMsgSig=HttpUtils.ParseUrl("msg_signature")
#ret = wxcpt.VerifyAESKey()
#print ret
sVerifyMsgSig="012bc692d0a58dd4b10f8dfe5c4ac00ae211ebeb"
#sVerifyTimeStamp=HttpUtils.ParseUrl("timestamp")
sVerifyTimeStamp="1476416373"
#sVerifyNonce=HttpUitls.ParseUrl("nonce")
sVerifyNonce="47744683"
#sVerifyEchoStr=HttpUtils.ParseUrl("echostr")
sVerifyEchoStr="fsi1xnbH4yQh0+PJxcOdhhK6TDXkjMyhEPA7xB2TGz6b+g7xyAbEkRxN/3cNXW9qdqjnoVzEtpbhnFyq6SVHyA=="
ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr)
if(ret!=0):
print "ERR: VerifyURL ret: " + str(ret)
sys.exit(1)
#验证URL成功,将sEchoStr返回给企业号
#HttpUtils.SetResponse(sEchoStr)
'''
------------使用示例二:对用户回复的消息解密---------------
用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档
假设企业收到企业微信的回调消息如下:
POST /cgi-bin/wxpush? msg_signature=477715d11cdb4164915debcba66cb864d751f3e6&timestamp=1409659813&nonce=1372623149 HTTP/1.1
Host: qy.weixin.qq.com
Content-Length: 613
<xml> <ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName><Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt>
<AgentID><![CDATA[218]]></AgentID>
</xml>
企业收到post请求之后应该 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce)
2.验证消息体签名的正确性。 3.将post请求的数据进行xml解析,并将<Encrypt>标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档
第2,3步可以用企业微信提供的库函数DecryptMsg来实现。
'''
# sReqMsgSig = HttpUtils.ParseUrl("msg_signature")
sReqMsgSig = "0c3914025cb4b4d68103f6bfc8db550f79dcf48e"
sReqTimeStamp = "1476422779"
sReqNonce = "1597212914"
sReqData = "<xml><ToUserName><![CDATA[ww1436e0e65a779aee]]></ToUserName>\n<Encrypt><![CDATA[Kl7kjoSf6DMD1zh7rtrHjFaDapSCkaOnwu3bqLc5tAybhhMl9pFeK8NslNPVdMwmBQTNoW4mY7AIjeLvEl3NyeTkAgGzBhzTtRLNshw2AEew+kkYcD+Fq72Kt00fT0WnN87hGrW8SqGc+NcT3mu87Ha3dz1pSDi6GaUA6A0sqfde0VJPQbZ9U+3JWcoD4Z5jaU0y9GSh010wsHF8KZD24YhmZH4ch4Ka7ilEbjbfvhKkNL65HHL0J6EYJIZUC2pFrdkJ7MhmEbU2qARR4iQHE7wy24qy0cRX3Mfp6iELcDNfSsPGjUQVDGxQDCWjayJOpcwocugux082f49HKYg84EpHSGXAyh+/oxwaWbvL6aSDPOYuPDGOCI8jmnKiypE+]]></Encrypt>\n<AgentID><![CDATA[1000002]]></AgentID>\n</xml>"
ret,sMsg=wxcpt.DecryptMsg( sReqData, sReqMsgSig, sReqTimeStamp, sReqNonce)
print ret,sMsg
if( ret!=0 ):
print "ERR: DecryptMsg ret: " + str(ret)
sys.exit(1)
# 解密成功,sMsg即为xml格式的明文
# TODO: 对明文的处理
# For example:
xml_tree = ET.fromstring(sMsg)
content = xml_tree.find("Content").text
print content
# ...
# ...
'''
------------使用示例三:企业回复用户消息的加密---------------
企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的xml串。
假设企业需要回复用户的明文如下:
<xml>
<ToUserName><![CDATA[mycreate]]></ToUserName>
<FromUserName><![CDATA[wx5823bf96d3bd56c7]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
<AgentID>128</AgentID>
</xml>
为了将此段明文回复给用户,企业应: 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。
2.将明文加密得到密文。 3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。 4.将密文,消息体签名,时间戳,随机数字串拼接成xml格式的字符串,发送给企业号。
以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。
'''
sRespData = "<xml><ToUserName>ww1436e0e65a779aee</ToUserName><FromUserName>ChenJiaShun</FromUserName><CreateTime>1476422779</CreateTime><MsgType>text</MsgType><Content>你好</Content><MsgId>1456453720</MsgId><AgentID>1000002</AgentID></xml>"
ret,sEncryptMsg=wxcpt.EncryptMsg(sRespData, sReqNonce, sReqTimeStamp)
if( ret!=0 ):
print "ERR: EncryptMsg ret: " + str(ret)
sys.exit(1)
#ret == 0 加密成功,企业需要将sEncryptMsg返回给企业号
#TODO:
#HttpUitls.SetResponse(sEncryptMsg) #测试解密
@@ -1,274 +0,0 @@
#!/usr/bin/env python
#-*- encoding:utf-8 -*-
""" 对企业微信发送给企业后台的消息加解密示例代码.
@copyright: Copyright (c) 1998-2014 Tencent Inc.
"""
# ------------------------------------------------------------------------
import base64
import string
import random
import hashlib
import time
import struct
from Crypto.Cipher import AES
import xml.etree.cElementTree as ET
import sys
import socket
stdi,stdo,stde=sys.stdin,sys.stdout,sys.stderr
reload(sys)
sys.stdin,sys.stdout,sys.stderr=stdi,stdo,stde
import ierror
sys.setdefaultencoding('utf-8')
"""
关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
"""
class FormatException(Exception):
pass
def throw_exception(message, exception_class=FormatException):
"""my define raise exception function"""
raise exception_class(message)
class SHA1:
"""计算企业微信的消息签名接口"""
def getSHA1(self, token, timestamp, nonce, encrypt):
"""用SHA1算法生成安全签名
@param token: 票据
@param timestamp: 时间戳
@param encrypt: 密文
@param nonce: 随机字符串
@return: 安全签名
"""
try:
sortlist = [token, timestamp, nonce, encrypt]
sortlist.sort()
sha = hashlib.sha1()
sha.update("".join(sortlist))
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
except Exception,e:
print e
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
class XMLParse:
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
# xml消息模板
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
<MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
<TimeStamp>%(timestamp)s</TimeStamp>
<Nonce><![CDATA[%(nonce)s]]></Nonce>
</xml>"""
def extract(self, xmltext):
"""提取出xml数据包中的加密消息
@param xmltext: 待提取的xml字符串
@return: 提取出的加密消息字符串
"""
try:
xml_tree = ET.fromstring(xmltext)
encrypt = xml_tree.find("Encrypt")
return ierror.WXBizMsgCrypt_OK, encrypt.text
except Exception,e:
print e
return ierror.WXBizMsgCrypt_ParseXml_Error,None
def generate(self, encrypt, signature, timestamp, nonce):
"""生成xml消息
@param encrypt: 加密后的消息密文
@param signature: 安全签名
@param timestamp: 时间戳
@param nonce: 随机字符串
@return: 生成的xml字符串
"""
resp_dict = {
'msg_encrypt' : encrypt,
'msg_signaturet': signature,
'timestamp' : timestamp,
'nonce' : nonce,
}
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
return resp_xml
class PKCS7Encoder():
"""提供基于PKCS7算法的加解密接口"""
block_size = 32
def encode(self, text):
""" 对需要加密的明文进行填充补位
@param text: 需要进行填充补位操作的明文
@return: 补齐明文字符串
"""
text_length = len(text)
# 计算需要填充的位数
amount_to_pad = self.block_size - (text_length % self.block_size)
if amount_to_pad == 0:
amount_to_pad = self.block_size
# 获得补位所用的字符
pad = chr(amount_to_pad)
return text + pad * amount_to_pad
def decode(self, decrypted):
"""删除解密后明文的补位字符
@param decrypted: 解密后的明文
@return: 删除补位字符后的明文
"""
pad = ord(decrypted[-1])
if pad<1 or pad >32:
pad = 0
return decrypted[:-pad]
class Prpcrypt(object):
"""提供接收和推送给企业微信消息的加解密接口"""
def __init__(self,key):
#self.key = base64.b64decode(key+"=")
self.key = key
# 设置加解密模式为AES的CBC模式
self.mode = AES.MODE_CBC
def encrypt(self,text,receiveid):
"""对明文进行加密
@param text: 需要加密的明文
@return: 加密得到的字符串
"""
# 16位随机字符串添加到明文开头
text = self.get_random_str() + struct.pack("I",socket.htonl(len(text))) + text + receiveid
# 使用自定义的填充方式对明文进行补位填充
pkcs7 = PKCS7Encoder()
text = pkcs7.encode(text)
# 加密
cryptor = AES.new(self.key,self.mode,self.key[:16])
try:
ciphertext = cryptor.encrypt(text)
# 使用BASE64对加密后的字符串进行编码
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
except Exception,e:
print e
return ierror.WXBizMsgCrypt_EncryptAES_Error,None
def decrypt(self,text,receiveid):
"""对解密后的明文进行补位删除
@param text: 密文
@return: 删除填充补位后的明文
"""
try:
cryptor = AES.new(self.key,self.mode,self.key[:16])
# 使用BASE64对密文进行解码,然后AES-CBC解密
plain_text = cryptor.decrypt(base64.b64decode(text))
except Exception,e:
print e
return ierror.WXBizMsgCrypt_DecryptAES_Error,None
try:
pad = ord(plain_text[-1])
# 去掉补位字符串
#pkcs7 = PKCS7Encoder()
#plain_text = pkcs7.encode(plain_text)
# 去除16位随机字符串
content = plain_text[16:-pad]
xml_len = socket.ntohl(struct.unpack("I",content[ : 4])[0])
xml_content = content[4 : xml_len+4]
from_receiveid = content[xml_len+4:]
except Exception,e:
print e
return ierror.WXBizMsgCrypt_IllegalBuffer,None
if from_receiveid != receiveid:
return ierror.WXBizMsgCrypt_ValidateCorpid_Error,None
return 0,xml_content
def get_random_str(self):
""" 随机生成16位字符串
@return: 16位字符串
"""
rule = string.letters + string.digits
str = random.sample(rule, 16)
return "".join(str)
class WXBizMsgCrypt(object):
#构造函数
def __init__(self,sToken,sEncodingAESKey,sReceiveId):
try:
self.key = base64.b64decode(sEncodingAESKey+"=")
assert len(self.key) == 32
except:
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
self.m_sToken = sToken
self.m_sReceiveId = sReceiveId
#验证URL
#@param sMsgSignature: 签名串,对应URL参数的msg_signature
#@param sTimeStamp: 时间戳,对应URL参数的timestamp
#@param sNonce: 随机串,对应URL参数的nonce
#@param sEchoStr: 随机串,对应URL参数的echostr
#@param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
#@return:成功0,失败返回对应的错误码
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
sha1 = SHA1()
ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret,sReplyEchoStr = pc.decrypt(sEchoStr,self.m_sReceiveId)
return ret,sReplyEchoStr
def EncryptMsg(self, sReplyMsg, sNonce, timestamp = None):
#将企业回复用户的消息加密打包
#@param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
#@param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
#@param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
#sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
#return:成功0sEncryptMsg,失败返回对应的错误码None
pc = Prpcrypt(self.key)
ret,encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId)
if ret != 0:
return ret,None
if timestamp is None:
timestamp = str(int(time.time()))
# 生成安全签名
sha1 = SHA1()
ret,signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
if ret != 0:
return ret,None
xmlParse = XMLParse()
return ret,xmlParse.generate(encrypt, signature, timestamp, sNonce)
def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
# 检验消息的真实性,并且获取解密后的明文
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
# @param sNonce: 随机串,对应URL参数的nonce
# @param sPostData: 密文,对应POST请求的数据
# xml_content: 解密后的原文,当return返回0时有效
# @return: 成功0,失败返回对应的错误码
# 验证安全签名
xmlParse = XMLParse()
ret,encrypt = xmlParse.extract(sPostData)
if ret != 0:
return ret, None
sha1 = SHA1()
ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret,xml_content = pc.decrypt(encrypt,self.m_sReceiveId)
return ret,xml_content
@@ -1,20 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
# File Name: ierror.py
# Description:定义错误码含义
#########################################################################
WXBizMsgCrypt_OK = 0
WXBizMsgCrypt_ValidateSignature_Error = -40001
WXBizMsgCrypt_ParseXml_Error = -40002
WXBizMsgCrypt_ComputeSignature_Error = -40003
WXBizMsgCrypt_IllegalAesKey = -40004
WXBizMsgCrypt_ValidateCorpid_Error = -40005
WXBizMsgCrypt_EncryptAES_Error = -40006
WXBizMsgCrypt_DecryptAES_Error = -40007
WXBizMsgCrypt_IllegalBuffer = -40008
WXBizMsgCrypt_EncodeBase64_Error = -40009
WXBizMsgCrypt_DecodeBase64_Error = -40010
WXBizMsgCrypt_GenReturnXml_Error = -40011
@@ -1,5 +0,0 @@
注意事项
1.WXBizMsgCrypt.py文件封装了WXBizMsgCrypt接口类,提供了用户接入企业微信的三个接口,Sample.py文件提供了如何使用这三个接口的示例,ierror.py提供了错误码。
2.WXBizMsgCrypt封装了VerifyURL, DecryptMsg, EncryptMsg三个接口,分别用于开发者验证回调url,收到用户回复消息的解密以及开发者回复消息的加密过程。使用方法可以参考Sample.py文件。
3.加解密协议请参考企业微信官方文档。
4.本代码用到了pycrypto第三方库,请开发者自行安装此库再使用。
@@ -1,108 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 03:55:41 PM CST
# File Name: Sample.py
# Description: WXBizJsonMsgCrypt 使用demo文件
#########################################################################
from WXBizJsonMsgCrypt import WXBizJsonMsgCrypt
import sys
if __name__ == "__main__":
# 企业在企业微信后台上设置的密钥相关配置在这里 TODO
sToken = "xxxxxxx"
sEncodingAESKey = "xxxxxxx"
sCorpID = "ww1436e0e65a779aee"
'''
------------使用示例一:验证回调URL---------------
*企业开启回调模式时,企业号会向验证url发送一个get请求
假设点击验证时,企业收到类似请求:
* GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3&timestamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D
* HTTP/1.1 Host: qy.weixin.qq.com
接收到该请求时,企业应 1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr),
这一步注意作URL解码。
2.验证消息体签名的正确性
3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信
第2,3步可以用企业微信提供的库函数VerifyURL来实现。
'''
wxcpt=WXBizJsonMsgCrypt(sToken,sEncodingAESKey,sCorpID)
sVerifyMsgSig="012bc692d0a58dd4b10f8dfe5c4ac00ae211ebeb"
sVerifyTimeStamp="1476416373"
sVerifyNonce="47744683"
sVerifyEchoStr="fsi1xnbH4yQh0+PJxcOdhhK6TDXkjMyhEPA7xB2TGz6b+g7xyAbEkRxN/3cNXW9qdqjnoVzEtpbhnFyq6SVHyA=="
ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr)
if(ret!=0):
print "ERR: VerifyURL ret: " + str(ret)
sys.exit(1)
else:
print "done VerifyURL"
#验证URL成功,将sEchoStr返回给企业号
print "=============================="
'''
------------使用示例二:对用户回复的消息解密---------------
用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档
假设企业收到企业微信的回调消息如下:
POST /cgi-bin/wxpush? msg_signature=e3647471e395139e2308c1fa963f2d648a00b90e&timestamp=1409659813&nonce=1372623149 HTTP/1.1
Host: qy.weixin.qq.com
{
"tousername": "wx5823bf96d3bd56c7",
"encrypt": "cjhLUX7UU4yCSelv1vz7T0zT8huF51bAMVWriNvO1FMegHrQZNrtvRxbwf0fUPsFvwqR0U0fgiJNEA5Y30F2MoI2S7vv3EjVQ68C0cjw9frBoUE2Hj0BvFp9h3u6Vbsg4lc1C8AtHdaN8orKuNKkLRLuYEL52R1J3v8olJGZRLnRdVKIivixmX/eQpzgeExtp20jI1HxRP1AAZ6xZoILdqDPO549LO4WeG+685JRUTdiwcY5fjZlqeMxuT4PpMn1X9OWsS7NRj06Wa5E3Tvg4twjWp39KPfOdRte6P1T4JU=",
"agentid": 218
}
企业收到post请求之后应该 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce)
2.验证消息体签名的正确性。 3.将post请求的数据进行json解析,并将"encrypt"标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档
第2,3步可以用企业微信提供的库函数DecryptMsg来实现。
'''
sReqNonce = "1372623149"
sReqTimeStamp = "1409659813"
sReqMsgSig = "e3647471e395139e2308c1fa963f2d648a00b90e"
sReqData = '{ "tousername": "wx5823bf96d3bd56c7", "encrypt": "cjhLUX7UU4yCSelv1vz7T0zT8huF51bAMVWriNvO1FMegHrQZNrtvRxbwf0fUPsFvwqR0U0fgiJNEA5Y30F2MoI2S7vv3EjVQ68C0cjw9frBoUE2Hj0BvFp9h3u6Vbsg4lc1C8AtHdaN8orKuNKkLRLuYEL52R1J3v8olJGZRLnRdVKIivixmX/eQpzgeExtp20jI1HxRP1AAZ6xZoILdqDPO549LO4WeG+685JRUTdiwcY5fjZlqeMxuT4PpMn1X9OWsS7NRj06Wa5E3Tvg4twjWp39KPfOdRte6P1T4JU=", "agentid": 218 }';
ret,sMsg=wxcpt.DecryptMsg( sReqData, sReqMsgSig, sReqTimeStamp, sReqNonce)
if( ret!=0 ):
print "ERR: DecryptMsg ret: " + str(ret)
sys.exit(1)
else:
print sMsg
# 解密成功,sMsg即为json格式的明文
# TODO: 对明文的处理
# ...
# ...
print "=============================="
'''
------------使用示例三:企业回复用户消息的加密---------------
企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的json串。
假设企业需要回复用户的明文如下:
{
"ToUserName": "mycreate",
"FromUserName":"wx5823bf96d3bd56c7",
"CreateTime": 1348831860,
"MsgType": "text",
"Content": "this is a test",
"MsgId": 1234567890123456,
"AgentID": 128
}
为了将此段明文回复给用户,企业应: 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。
2.将明文加密得到密文。 3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。 4.将密文,消息体签名,时间戳,随机数字串拼接成json格式的字符串,发送给企业号。
以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。
'''
#sRespData = ' { "ToUserName": "mycreate", "FromUserName":"wx5823bf96d3bd56c7", "CreateTime": 1348831860, "MsgType": "text", "Content": "this is a test", "MsgId": 1234567890123456, "AgentID": 128 }';
sRespData = '{ "ToUserName": "wx5823bf96d3bd56c7", "FromUserName": :mycreate", "CreateTime": 1409659813, "MsgType": "text", "Content": "hello", "MsgId": 4561255354251345929, "AgentID": 218}'
ret,sEncryptMsg=wxcpt.EncryptMsg(sRespData, sReqNonce, sReqTimeStamp)
if( ret!=0 ):
print "ERR: EncryptMsg ret: " + str(ret)
sys.exit(1)
else:
print sEncryptMsg
#ret == 0 加密成功,企业需要将sEncryptMsg返回给企业号
print "=============================="
@@ -1,275 +0,0 @@
#!/usr/bin/env python
#-*- encoding:utf-8 -*-
""" 对企业微信发送给企业后台的消息加解密示例代码.
@copyright: Copyright (c) 1998-2020 Tencent Inc.
"""
# ------------------------------------------------------------------------
import base64
import string
import random
import hashlib
import time
import struct
from Crypto.Cipher import AES
import sys
import socket
import json
reload(sys)
import ierror
sys.setdefaultencoding('utf-8')
"""
关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
"""
class FormatException(Exception):
pass
def throw_exception(message, exception_class=FormatException):
"""my define raise exception function"""
raise exception_class(message)
class SHA1:
"""计算企业微信的消息签名接口"""
def getSHA1(self, token, timestamp, nonce, encrypt):
"""用SHA1算法生成安全签名
@param token: 票据
@param timestamp: 时间戳
@param encrypt: 密文
@param nonce: 随机字符串
@return: 安全签名
"""
try:
sortlist = [token, timestamp, nonce, encrypt]
sortlist.sort()
sha = hashlib.sha1()
sha.update("".join(sortlist))
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
except Exception,e:
print e
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
class JsonParse:
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
# json消息模板
AES_TEXT_RESPONSE_TEMPLATE = '''{
"encrypt": "%(msg_encrypt)s",
"msgsignature": "%(msg_signaturet)s",
"timestamp": "%(timestamp)s",
"nonce": "%(nonce)s"
}'''
def extract(self, jsontext):
"""提取出json数据包中的加密消息
@param jsontext: 待提取的json字符串
@return: 提取出的加密消息字符串
"""
try:
json_dict = json.loads(jsontext)
return ierror.WXBizMsgCrypt_OK, json_dict['encrypt']
except Exception,e:
print e
return ierror.WXBizMsgCrypt_ParseJson_Error, None
def generate(self, encrypt, signature, timestamp, nonce):
"""生成json消息
@param encrypt: 加密后的消息密文
@param signature: 安全签名
@param timestamp: 时间戳
@param nonce: 随机字符串
@return: 生成的json字符串
"""
resp_dict = {
'msg_encrypt' : encrypt,
'msg_signaturet': signature,
'timestamp' : timestamp,
'nonce' : nonce,
}
resp_json = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
return resp_json
class PKCS7Encoder():
"""提供基于PKCS7算法的加解密接口"""
block_size = 32
def encode(self, text):
""" 对需要加密的明文进行填充补位
@param text: 需要进行填充补位操作的明文
@return: 补齐明文字符串
"""
text_length = len(text)
# 计算需要填充的位数
amount_to_pad = self.block_size - (text_length % self.block_size)
if amount_to_pad == 0:
amount_to_pad = self.block_size
# 获得补位所用的字符
pad = chr(amount_to_pad)
return text + pad * amount_to_pad
def decode(self, decrypted):
"""删除解密后明文的补位字符
@param decrypted: 解密后的明文
@return: 删除补位字符后的明文
"""
pad = ord(decrypted[-1])
if pad<1 or pad >32:
pad = 0
return decrypted[:-pad]
class Prpcrypt(object):
"""提供接收和推送给企业微信消息的加解密接口"""
def __init__(self,key):
#self.key = base64.b64decode(key+"=")
self.key = key
# 设置加解密模式为AES的CBC模式
self.mode = AES.MODE_CBC
def encrypt(self,text,receiveid):
"""对明文进行加密
@param text: 需要加密的明文
@return: 加密得到的字符串
"""
# 16位随机字符串添加到明文开头
text = self.get_random_str() + struct.pack("I",socket.htonl(len(text))) + text + receiveid
# 使用自定义的填充方式对明文进行补位填充
pkcs7 = PKCS7Encoder()
text = pkcs7.encode(text)
# 加密
cryptor = AES.new(self.key,self.mode,self.key[:16])
try:
ciphertext = cryptor.encrypt(text)
# 使用BASE64对加密后的字符串进行编码
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
except Exception,e:
print e
return ierror.WXBizMsgCrypt_EncryptAES_Error,None
def decrypt(self,text,receiveid):
"""对解密后的明文进行补位删除
@param text: 密文
@return: 删除填充补位后的明文
"""
try:
cryptor = AES.new(self.key,self.mode,self.key[:16])
# 使用BASE64对密文进行解码,然后AES-CBC解密
plain_text = cryptor.decrypt(base64.b64decode(text))
except Exception,e:
print e
return ierror.WXBizMsgCrypt_DecryptAES_Error,None
try:
pad = ord(plain_text[-1])
# 去掉补位字符串
#pkcs7 = PKCS7Encoder()
#plain_text = pkcs7.encode(plain_text)
# 去除16位随机字符串
content = plain_text[16:-pad]
json_len = socket.ntohl(struct.unpack("I",content[ : 4])[0])
json_content = content[4 : json_len+4]
from_receiveid = content[json_len+4:]
except Exception,e:
print e
return ierror.WXBizMsgCrypt_IllegalBuffer,None
if from_receiveid != receiveid:
print "receiveid not match"
print from_receiveid
return ierror.WXBizMsgCrypt_ValidateCorpid_Error,None
return 0,json_content
def get_random_str(self):
""" 随机生成16位字符串
@return: 16位字符串
"""
rule = string.letters + string.digits
str = random.sample(rule, 16)
return "".join(str)
class WXBizJsonMsgCrypt(object):
#构造函数
def __init__(self,sToken,sEncodingAESKey,sReceiveId):
try:
self.key = base64.b64decode(sEncodingAESKey+"=")
assert len(self.key) == 32
except:
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
self.m_sToken = sToken
self.m_sReceiveId = sReceiveId
#验证URL
#@param sMsgSignature: 签名串,对应URL参数的msg_signature
#@param sTimeStamp: 时间戳,对应URL参数的timestamp
#@param sNonce: 随机串,对应URL参数的nonce
#@param sEchoStr: 随机串,对应URL参数的echostr
#@param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
#@return:成功0,失败返回对应的错误码
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
sha1 = SHA1()
ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret,sReplyEchoStr = pc.decrypt(sEchoStr,self.m_sReceiveId)
return ret,sReplyEchoStr
def EncryptMsg(self, sReplyMsg, sNonce, timestamp = None):
#将企业回复用户的消息加密打包
#@param sReplyMsg: 企业号待回复用户的消息,json格式的字符串
#@param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
#@param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
#sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串,
#return:成功0sEncryptMsg,失败返回对应的错误码None
pc = Prpcrypt(self.key)
ret,encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId)
if ret != 0:
return ret,None
if timestamp is None:
timestamp = str(int(time.time()))
# 生成安全签名
sha1 = SHA1()
ret,signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
if ret != 0:
return ret,None
jsonParse = JsonParse()
return ret,jsonParse.generate(encrypt, signature, timestamp, sNonce)
def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
# 检验消息的真实性,并且获取解密后的明文
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
# @param sNonce: 随机串,对应URL参数的nonce
# @param sPostData: 密文,对应POST请求的数据
# json_content: 解密后的原文,当return返回0时有效
# @return: 成功0,失败返回对应的错误码
# 验证安全签名
jsonParse = JsonParse()
ret,encrypt = jsonParse.extract(sPostData)
if ret != 0:
return ret, None
sha1 = SHA1()
ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
print "signature not match"
print signature
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret,json_content = pc.decrypt(encrypt,self.m_sReceiveId)
return ret,json_content
@@ -1,20 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
# File Name: ierror.py
# Description:定义错误码含义
#########################################################################
WXBizMsgCrypt_OK = 0
WXBizMsgCrypt_ValidateSignature_Error = -40001
WXBizMsgCrypt_ParseJson_Error = -40002
WXBizMsgCrypt_ComputeSignature_Error = -40003
WXBizMsgCrypt_IllegalAesKey = -40004
WXBizMsgCrypt_ValidateCorpid_Error = -40005
WXBizMsgCrypt_EncryptAES_Error = -40006
WXBizMsgCrypt_DecryptAES_Error = -40007
WXBizMsgCrypt_IllegalBuffer = -40008
WXBizMsgCrypt_EncodeBase64_Error = -40009
WXBizMsgCrypt_DecodeBase64_Error = -40010
WXBizMsgCrypt_GenReturnJson_Error = -40011
@@ -1,5 +0,0 @@
注意事项
1.WXBizMsgCrypt.py文件封装了WXBizMsgCrypt接口类,提供了用户接入企业微信的三个接口,Sample.py文件提供了如何使用这三个接口的示例,ierror.py提供了错误码。
2.WXBizMsgCrypt封装了VerifyURL, DecryptMsg, EncryptMsg三个接口,分别用于开发者验证回调url,收到用户回复消息的解密以及开发者回复消息的加密过程。使用方法可以参考Sample.py文件。
3.加解密协议请参考企业微信官方文档。
4.本代码用到了pycrypto第三方库,请开发者自行安装此库再使用。
@@ -1,123 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 03:55:41 PM CST
# File Name: Sample.py
# Description: WXBizJsonMsgCrypt 使用demo文件
#########################################################################
from WXBizJsonMsgCrypt import WXBizJsonMsgCrypt
import sys
import json
if __name__ == "__main__":
# 企业在企业微信后台上设置的密钥相关配置在这里 TODO
sToken = "xxxxxxx"
sEncodingAESKey = "xxxxxxx"
sCorpID = "ww1436e0e65a779aee"
'''
------------使用示例一:验证回调URL---------------
*企业开启回调模式时,企业号会向验证url发送一个get请求
假设点击验证时,企业收到类似请求:
* GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3&timestamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D
* HTTP/1.1 Host: qy.weixin.qq.com
接收到该请求时,企业应 1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr),
这一步注意作URL解码。
2.验证消息体签名的正确性
3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信
第2,3步可以用企业微信提供的库函数VerifyURL来实现。
'''
wxcpt=WXBizJsonMsgCrypt(sToken,sEncodingAESKey,sCorpID)
sVerifyMsgSig="012bc692d0a58dd4b10f8dfe5c4ac00ae211ebeb"
sVerifyTimeStamp="1476416373"
sVerifyNonce="47744683"
sVerifyEchoStr="fsi1xnbH4yQh0+PJxcOdhhK6TDXkjMyhEPA7xB2TGz6b+g7xyAbEkRxN/3cNXW9qdqjnoVzEtpbhnFyq6SVHyA=="
ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr)
if(ret!=0):
print("ERR: VerifyURL ret: " + str(ret))
sys.exit(1)
else:
print("done VerifyURL")
#验证URL成功,将sEchoStr返回给企业号
print("==============================")
'''
------------使用示例二:对用户回复的消息解密---------------
用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档
假设企业收到企业微信的回调消息如下:
POST /cgi-bin/wxpush? msg_signature=e3647471e395139e2308c1fa963f2d648a00b90e&timestamp=1409659813&nonce=1372623149 HTTP/1.1
Host: qy.weixin.qq.com
{
"tousername": "wx5823bf96d3bd56c7",
"encrypt": "cjhLUX7UU4yCSelv1vz7T0zT8huF51bAMVWriNvO1FMegHrQZNrtvRxbwf0fUPsFvwqR0U0fgiJNEA5Y30F2MoI2S7vv3EjVQ68C0cjw9frBoUE2Hj0BvFp9h3u6Vbsg4lc1C8AtHdaN8orKuNKkLRLuYEL52R1J3v8olJGZRLnRdVKIivixmX/eQpzgeExtp20jI1HxRP1AAZ6xZoILdqDPO549LO4WeG+685JRUTdiwcY5fjZlqeMxuT4PpMn1X9OWsS7NRj06Wa5E3Tvg4twjWp39KPfOdRte6P1T4JU=",
"agentid": 218
}
企业收到post请求之后应该 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce)
2.验证消息体签名的正确性。 3.将post请求的数据进行json解析,并将"encrypt"标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档
第2,3步可以用企业微信提供的库函数DecryptMsg来实现。
'''
sReqNonce = "1372623149"
sReqTimeStamp = "1409659813"
sReqMsgSig = "e3647471e395139e2308c1fa963f2d648a00b90e"
sReqData = '{ "tousername": "wx5823bf96d3bd56c7", "encrypt": "cjhLUX7UU4yCSelv1vz7T0zT8huF51bAMVWriNvO1FMegHrQZNrtvRxbwf0fUPsFvwqR0U0fgiJNEA5Y30F2MoI2S7vv3EjVQ68C0cjw9frBoUE2Hj0BvFp9h3u6Vbsg4lc1C8AtHdaN8orKuNKkLRLuYEL52R1J3v8olJGZRLnRdVKIivixmX/eQpzgeExtp20jI1HxRP1AAZ6xZoILdqDPO549LO4WeG+685JRUTdiwcY5fjZlqeMxuT4PpMn1X9OWsS7NRj06Wa5E3Tvg4twjWp39KPfOdRte6P1T4JU=", "agentid": 218 }';
ret,sMsg=wxcpt.DecryptMsg( sReqData, sReqMsgSig, sReqTimeStamp, sReqNonce)
if( ret!=0 ):
print("ERR: DecryptMsg ret: " + str(ret))
sys.exit(1)
else:
print(sMsg)
# 解密成功,sMsg即为json格式的明文
# TODO: 对明文的处理
# ...
# ...
print("==============================")
'''
------------使用示例三:企业回复用户消息的加密---------------
企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的json串。
假设企业需要回复用户的明文如下:
{
"ToUserName": "mycreate",
"FromUserName":"wx5823bf96d3bd56c7",
"CreateTime": 1348831860,
"MsgType": "text",
"Content": "this is a test",
"MsgId": 1234567890123456,
"AgentID": 128
}
为了将此段明文回复给用户,企业应: 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。
2.将明文加密得到密文。 3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。 4.将密文,消息体签名,时间戳,随机数字串拼接成json格式的字符串,发送给企业号。
以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。
'''
#sRespData = ' { "ToUserName": "mycreate", "FromUserName":"wx5823bf96d3bd56c7", "CreateTime": 1348831860, "MsgType": "text", "Content": "this is a test", "MsgId": 1234567890123456, "AgentID": 128 }';
sRespData = '{ "ToUserName": "wx5823bf96d3bd56c7", "FromUserName": :mycreate", "CreateTime": 1409659813, "MsgType": "text", "Content": "hello", "MsgId": 4561255354251345929, "AgentID": 218}'
ret,sEncryptMsg=wxcpt.EncryptMsg(sRespData, sReqNonce, sReqTimeStamp)
if( ret!=0 ):
print("ERR: EncryptMsg ret: " + str(ret))
sys.exit(1)
else:
print(sEncryptMsg)
#ret == 0 加密成功,企业需要将sEncryptMsg返回给企业号
print("==============================")
'''
对上面加密的包进行解密
'''
sReqMsgSig = json.loads(sEncryptMsg)['msgsignature']
sReqTimeStamp = json.loads(sEncryptMsg)['timestamp']
sReqNonce = json.loads(sEncryptMsg)['nonce']
ret,sMsg=wxcpt.DecryptMsg( sEncryptMsg, sReqMsgSig, sReqTimeStamp, sReqNonce)
if( ret!=0 ):
print("ERR: DecryptMsg ret: " + str(ret))
sys.exit(1)
else:
print(sMsg)
@@ -1,282 +0,0 @@
#!/usr/bin/env python
#-*- encoding:utf-8 -*-
""" 对企业微信发送给企业后台的消息加解密示例代码.
@copyright: Copyright (c) 1998-2020 Tencent Inc.
"""
# ------------------------------------------------------------------------
import base64
import string
import random
import hashlib
import time
import struct
from Crypto.Cipher import AES
import sys
import socket
import json
import ierror
"""
关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
"""
class FormatException(Exception):
pass
def throw_exception(message, exception_class=FormatException):
"""my define raise exception function"""
raise exception_class(message)
class SHA1:
"""计算企业微信的消息签名接口"""
def getSHA1(self, token, timestamp, nonce, encrypt):
"""用SHA1算法生成安全签名
@param token: 票据
@param timestamp: 时间戳
@param encrypt: 密文
@param nonce: 随机字符串
@return: 安全签名
"""
try:
# 确保所有输入都是字符串类型
if isinstance(encrypt, bytes):
encrypt = encrypt.decode('utf-8')
sortlist = [str(token), str(timestamp), str(nonce), str(encrypt)]
sortlist.sort()
sha = hashlib.sha1()
sha.update("".join(sortlist).encode('utf-8'))
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
except Exception as e:
print(e)
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
class JsonParse:
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
# json消息模板
AES_TEXT_RESPONSE_TEMPLATE = '''{
"encrypt": "%(msg_encrypt)s",
"msgsignature": "%(msg_signaturet)s",
"timestamp": "%(timestamp)s",
"nonce": "%(nonce)s"
}'''
def extract(self, jsontext):
"""提取出json数据包中的加密消息
@param jsontext: 待提取的json字符串
@return: 提取出的加密消息字符串
"""
try:
json_dict = json.loads(jsontext)
return ierror.WXBizMsgCrypt_OK, json_dict['encrypt']
except Exception as e:
print(e)
return ierror.WXBizMsgCrypt_ParseJson_Error, None
def generate(self, encrypt, signature, timestamp, nonce):
"""生成json消息
@param encrypt: 加密后的消息密文
@param signature: 安全签名
@param timestamp: 时间戳
@param nonce: 随机字符串
@return: 生成的json字符串
"""
resp_dict = {
'msg_encrypt' : encrypt,
'msg_signaturet': signature,
'timestamp' : timestamp,
'nonce' : nonce,
}
resp_json = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
return resp_json
class PKCS7Encoder():
"""提供基于PKCS7算法的加解密接口"""
block_size = 32
def encode(self, text):
""" 对需要加密的明文进行填充补位
@param text: 需要进行填充补位操作的明文(bytes类型)
@return: 补齐明文字符串(bytes类型)
"""
text_length = len(text)
# 计算需要填充的位数
amount_to_pad = self.block_size - (text_length % self.block_size)
if amount_to_pad == 0:
amount_to_pad = self.block_size
# 获得补位所用的字符
pad = bytes([amount_to_pad])
# 确保text是bytes类型
if isinstance(text, str):
text = text.encode('utf-8')
return text + pad * amount_to_pad
def decode(self, decrypted):
"""删除解密后明文的补位字符
@param decrypted: 解密后的明文
@return: 删除补位字符后的明文
"""
pad = ord(decrypted[-1])
if pad<1 or pad >32:
pad = 0
return decrypted[:-pad]
class Prpcrypt(object):
"""提供接收和推送给企业微信消息的加解密接口"""
def __init__(self,key):
#self.key = base64.b64decode(key+"=")
self.key = key
# 设置加解密模式为AES的CBC模式
self.mode = AES.MODE_CBC
def encrypt(self, text, receiveid):
"""对明文进行加密
@param text: 需要加密的明文
@return: 加密得到的字符串
"""
# 16位随机字符串添加到明文开头
text = text.encode()
text = self.get_random_str() + struct.pack("I", socket.htonl(len(text))) + text + receiveid.encode()
# 使用自定义的填充方式对明文进行补位填充
pkcs7 = PKCS7Encoder()
text = pkcs7.encode(text)
# 加密
cryptor = AES.new(self.key, self.mode, self.key[:16])
try:
ciphertext = cryptor.encrypt(text)
# 使用BASE64对加密后的字符串进行编码
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_EncryptAES_Error, None
def decrypt(self,text,receiveid):
"""对解密后的明文进行补位删除
@param text: 密文
@return: 删除填充补位后的明文
"""
try:
cryptor = AES.new(self.key,self.mode,self.key[:16])
# 使用BASE64对密文进行解码,然后AES-CBC解密
plain_text = cryptor.decrypt(base64.b64decode(text))
except Exception as e:
print(e)
return ierror.WXBizMsgCrypt_DecryptAES_Error,None
try:
pad = plain_text[-1]
# 去掉补位字符串
#pkcs7 = PKCS7Encoder()
#plain_text = pkcs7.encode(plain_text)
# 去除16位随机字符串
content = plain_text[16:-pad]
json_len = socket.ntohl(struct.unpack("I",content[ : 4])[0])
json_content = content[4 : json_len+4].decode('utf-8')
from_receiveid = content[json_len+4:].decode('utf-8')
except Exception as e:
print(e)
return ierror.WXBizMsgCrypt_IllegalBuffer,None
if from_receiveid != receiveid:
print("receiveid not match", receiveid, from_receiveid)
return ierror.WXBizMsgCrypt_ValidateCorpid_Error,None
return 0,json_content
def get_random_str(self):
""" 随机生成16位字符串
@return: 16位字符串
"""
return str(random.randint(1000000000000000, 9999999999999999)).encode()
class WXBizJsonMsgCrypt(object):
#构造函数
def __init__(self,sToken,sEncodingAESKey,sReceiveId):
try:
self.key = base64.b64decode(sEncodingAESKey+"=")
assert len(self.key) == 32
except:
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
self.m_sToken = sToken
self.m_sReceiveId = sReceiveId
#验证URL
#@param sMsgSignature: 签名串,对应URL参数的msg_signature
#@param sTimeStamp: 时间戳,对应URL参数的timestamp
#@param sNonce: 随机串,对应URL参数的nonce
#@param sEchoStr: 随机串,对应URL参数的echostr
#@param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
#@return:成功0,失败返回对应的错误码
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
sha1 = SHA1()
ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret,sReplyEchoStr = pc.decrypt(sEchoStr,self.m_sReceiveId)
return ret,sReplyEchoStr
def EncryptMsg(self, sReplyMsg, sNonce, timestamp = None):
#将企业回复用户的消息加密打包
#@param sReplyMsg: 企业号待回复用户的消息,json格式的字符串
#@param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
#@param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
#sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串,
#return:成功0sEncryptMsg,失败返回对应的错误码None
pc = Prpcrypt(self.key)
ret,encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId)
encrypt = encrypt.decode('utf-8')
if ret != 0:
return ret,None
if timestamp is None:
timestamp = str(int(time.time()))
# 生成安全签名
sha1 = SHA1()
ret,signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
if ret != 0:
return ret,None
jsonParse = JsonParse()
return ret,jsonParse.generate(encrypt, signature, timestamp, sNonce)
def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
# 检验消息的真实性,并且获取解密后的明文
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
# @param sNonce: 随机串,对应URL参数的nonce
# @param sPostData: 密文,对应POST请求的数据
# json_content: 解密后的原文,当return返回0时有效
# @return: 成功0,失败返回对应的错误码
# 验证安全签名
jsonParse = JsonParse()
ret,encrypt = jsonParse.extract(sPostData)
if ret != 0:
return ret, None
sha1 = SHA1()
ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
print("signature not match")
print(signature)
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret,json_content = pc.decrypt(encrypt,self.m_sReceiveId)
return ret,json_content
@@ -1,20 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
# File Name: ierror.py
# Description:定义错误码含义
#########################################################################
WXBizMsgCrypt_OK = 0
WXBizMsgCrypt_ValidateSignature_Error = -40001
WXBizMsgCrypt_ParseJson_Error = -40002
WXBizMsgCrypt_ComputeSignature_Error = -40003
WXBizMsgCrypt_IllegalAesKey = -40004
WXBizMsgCrypt_ValidateCorpid_Error = -40005
WXBizMsgCrypt_EncryptAES_Error = -40006
WXBizMsgCrypt_DecryptAES_Error = -40007
WXBizMsgCrypt_IllegalBuffer = -40008
WXBizMsgCrypt_EncodeBase64_Error = -40009
WXBizMsgCrypt_DecodeBase64_Error = -40010
WXBizMsgCrypt_GenReturnJson_Error = -40011
@@ -1,5 +0,0 @@
注意事项
1.WXBizMsgCrypt.py文件封装了WXBizMsgCrypt接口类(Python3以及以上版本使用 WXBizMsgCrypt3.py),提供了用户接入企业微信的三个接口,Sample.py文件提供了如何使用这三个接口的示例,ierror.py提供了错误码。
2.WXBizMsgCrypt封装了VerifyURL, DecryptMsg, EncryptMsg三个接口,分别用于开发者验证回调url,收到用户回复消息的解密以及开发者回复消息的加密过程。使用方法可以参考Sample.py文件。
3.加解密协议请参考企业微信官方文档。
4.本代码用到了pycrypto第三方库,请开发者自行安装此库再使用。
@@ -1,123 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 03:55:41 PM CST
# File Name: Sample.py
# Description: WXBizJsonMsgCrypt 使用demo文件
#########################################################################
from WXBizJsonMsgCrypt import WXBizJsonMsgCrypt
import sys
import json
if __name__ == "__main__":
# 企业在企业微信后台上设置的密钥相关配置在这里 TODO
sToken = "xxxxxxx"
sEncodingAESKey = "xxxxxxx"
sCorpID = "ww1436e0e65a779aee"
'''
------------使用示例一:验证回调URL---------------
*企业开启回调模式时,企业号会向验证url发送一个get请求
假设点击验证时,企业收到类似请求:
* GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3&timestamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D
* HTTP/1.1 Host: qy.weixin.qq.com
接收到该请求时,企业应 1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr),
这一步注意作URL解码。
2.验证消息体签名的正确性
3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信
第2,3步可以用企业微信提供的库函数VerifyURL来实现。
'''
wxcpt=WXBizJsonMsgCrypt(sToken,sEncodingAESKey,sCorpID)
sVerifyMsgSig="012bc692d0a58dd4b10f8dfe5c4ac00ae211ebeb"
sVerifyTimeStamp="1476416373"
sVerifyNonce="47744683"
sVerifyEchoStr="fsi1xnbH4yQh0+PJxcOdhhK6TDXkjMyhEPA7xB2TGz6b+g7xyAbEkRxN/3cNXW9qdqjnoVzEtpbhnFyq6SVHyA=="
ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr)
if(ret!=0):
print("ERR: VerifyURL ret: " + str(ret))
sys.exit(1)
else:
print("done VerifyURL")
#验证URL成功,将sEchoStr返回给企业号
print("==============================")
'''
------------使用示例二:对用户回复的消息解密---------------
用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档
假设企业收到企业微信的回调消息如下:
POST /cgi-bin/wxpush? msg_signature=e3647471e395139e2308c1fa963f2d648a00b90e&timestamp=1409659813&nonce=1372623149 HTTP/1.1
Host: qy.weixin.qq.com
{
"tousername": "wx5823bf96d3bd56c7",
"encrypt": "cjhLUX7UU4yCSelv1vz7T0zT8huF51bAMVWriNvO1FMegHrQZNrtvRxbwf0fUPsFvwqR0U0fgiJNEA5Y30F2MoI2S7vv3EjVQ68C0cjw9frBoUE2Hj0BvFp9h3u6Vbsg4lc1C8AtHdaN8orKuNKkLRLuYEL52R1J3v8olJGZRLnRdVKIivixmX/eQpzgeExtp20jI1HxRP1AAZ6xZoILdqDPO549LO4WeG+685JRUTdiwcY5fjZlqeMxuT4PpMn1X9OWsS7NRj06Wa5E3Tvg4twjWp39KPfOdRte6P1T4JU=",
"agentid": 218
}
企业收到post请求之后应该 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce)
2.验证消息体签名的正确性。 3.将post请求的数据进行json解析,并将"encrypt"标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档
第2,3步可以用企业微信提供的库函数DecryptMsg来实现。
'''
sReqNonce = "1372623149"
sReqTimeStamp = "1409659813"
sReqMsgSig = "e3647471e395139e2308c1fa963f2d648a00b90e"
sReqData = '{ "tousername": "wx5823bf96d3bd56c7", "encrypt": "cjhLUX7UU4yCSelv1vz7T0zT8huF51bAMVWriNvO1FMegHrQZNrtvRxbwf0fUPsFvwqR0U0fgiJNEA5Y30F2MoI2S7vv3EjVQ68C0cjw9frBoUE2Hj0BvFp9h3u6Vbsg4lc1C8AtHdaN8orKuNKkLRLuYEL52R1J3v8olJGZRLnRdVKIivixmX/eQpzgeExtp20jI1HxRP1AAZ6xZoILdqDPO549LO4WeG+685JRUTdiwcY5fjZlqeMxuT4PpMn1X9OWsS7NRj06Wa5E3Tvg4twjWp39KPfOdRte6P1T4JU=", "agentid": 218 }';
ret,sMsg=wxcpt.DecryptMsg( sReqData, sReqMsgSig, sReqTimeStamp, sReqNonce)
if( ret!=0 ):
print("ERR: DecryptMsg ret: " + str(ret))
sys.exit(1)
else:
print(sMsg)
# 解密成功,sMsg即为json格式的明文
# TODO: 对明文的处理
# ...
# ...
print("==============================")
'''
------------使用示例三:企业回复用户消息的加密---------------
企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的json串。
假设企业需要回复用户的明文如下:
{
"ToUserName": "mycreate",
"FromUserName":"wx5823bf96d3bd56c7",
"CreateTime": 1348831860,
"MsgType": "text",
"Content": "this is a test",
"MsgId": 1234567890123456,
"AgentID": 128
}
为了将此段明文回复给用户,企业应: 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。
2.将明文加密得到密文。 3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。 4.将密文,消息体签名,时间戳,随机数字串拼接成json格式的字符串,发送给企业号。
以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。
'''
#sRespData = ' { "ToUserName": "mycreate", "FromUserName":"wx5823bf96d3bd56c7", "CreateTime": 1348831860, "MsgType": "text", "Content": "this is a test", "MsgId": 1234567890123456, "AgentID": 128 }';
sRespData = '{ "ToUserName": "wx5823bf96d3bd56c7", "FromUserName": :mycreate", "CreateTime": 1409659813, "MsgType": "text", "Content": "hello", "MsgId": 4561255354251345929, "AgentID": 218}'
ret,sEncryptMsg=wxcpt.EncryptMsg(sRespData, sReqNonce, sReqTimeStamp)
if( ret!=0 ):
print("ERR: EncryptMsg ret: " + str(ret))
sys.exit(1)
else:
print(sEncryptMsg)
#ret == 0 加密成功,企业需要将sEncryptMsg返回给企业号
print("==============================")
'''
对上面加密的包进行解密
'''
sReqMsgSig = json.loads(sEncryptMsg)['msgsignature']
sReqTimeStamp = json.loads(sEncryptMsg)['timestamp']
sReqNonce = json.loads(sEncryptMsg)['nonce']
ret,sMsg=wxcpt.DecryptMsg( sEncryptMsg, sReqMsgSig, sReqTimeStamp, sReqNonce)
if( ret!=0 ):
print("ERR: DecryptMsg ret: " + str(ret))
sys.exit(1)
else:
print(sMsg)
@@ -1,280 +0,0 @@
#!/usr/bin/env python
# -*- encoding:utf-8 -*-
""" 对企业微信发送给企业后台的消息加解密示例代码.
@copyright: Copyright (c) 1998-2014 Tencent Inc.
"""
# ------------------------------------------------------------------------
import logging
import base64
import random
import hashlib
import time
import struct
from Crypto.Cipher import AES
import xml.etree.cElementTree as ET
import socket
import ierror
"""
关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
"""
class FormatException(Exception):
pass
def throw_exception(message, exception_class=FormatException):
"""my define raise exception function"""
raise exception_class(message)
class SHA1:
"""计算企业微信的消息签名接口"""
def getSHA1(self, token, timestamp, nonce, encrypt):
"""用SHA1算法生成安全签名
@param token: 票据
@param timestamp: 时间戳
@param encrypt: 密文
@param nonce: 随机字符串
@return: 安全签名
"""
try:
sortlist = [token, timestamp, nonce, encrypt]
sortlist.sort()
sha = hashlib.sha1()
sha.update("".join(sortlist).encode())
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
class XMLParse:
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
# xml消息模板
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
<MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
<TimeStamp>%(timestamp)s</TimeStamp>
<Nonce><![CDATA[%(nonce)s]]></Nonce>
</xml>"""
def extract(self, xmltext):
"""提取出xml数据包中的加密消息
@param xmltext: 待提取的xml字符串
@return: 提取出的加密消息字符串
"""
try:
xml_tree = ET.fromstring(xmltext)
encrypt = xml_tree.find("Encrypt")
return ierror.WXBizMsgCrypt_OK, encrypt.text
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_ParseXml_Error, None
def generate(self, encrypt, signature, timestamp, nonce):
"""生成xml消息
@param encrypt: 加密后的消息密文
@param signature: 安全签名
@param timestamp: 时间戳
@param nonce: 随机字符串
@return: 生成的xml字符串
"""
resp_dict = {
'msg_encrypt': encrypt,
'msg_signaturet': signature,
'timestamp': timestamp,
'nonce': nonce,
}
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
return resp_xml
class PKCS7Encoder():
"""提供基于PKCS7算法的加解密接口"""
block_size = 32
def encode(self, text):
""" 对需要加密的明文进行填充补位
@param text: 需要进行填充补位操作的明文
@return: 补齐明文字符串
"""
text_length = len(text)
# 计算需要填充的位数
amount_to_pad = self.block_size - (text_length % self.block_size)
if amount_to_pad == 0:
amount_to_pad = self.block_size
# 获得补位所用的字符
pad = chr(amount_to_pad)
return text + (pad * amount_to_pad).encode()
def decode(self, decrypted):
"""删除解密后明文的补位字符
@param decrypted: 解密后的明文
@return: 删除补位字符后的明文
"""
pad = ord(decrypted[-1])
if pad < 1 or pad > 32:
pad = 0
return decrypted[:-pad]
class Prpcrypt(object):
"""提供接收和推送给企业微信消息的加解密接口"""
def __init__(self, key):
# self.key = base64.b64decode(key+"=")
self.key = key
# 设置加解密模式为AES的CBC模式
self.mode = AES.MODE_CBC
def encrypt(self, text, receiveid):
"""对明文进行加密
@param text: 需要加密的明文
@return: 加密得到的字符串
"""
# 16位随机字符串添加到明文开头
text = text.encode()
text = self.get_random_str() + struct.pack("I", socket.htonl(len(text))) + text + receiveid.encode()
# 使用自定义的填充方式对明文进行补位填充
pkcs7 = PKCS7Encoder()
text = pkcs7.encode(text)
# 加密
cryptor = AES.new(self.key, self.mode, self.key[:16])
try:
ciphertext = cryptor.encrypt(text)
# 使用BASE64对加密后的字符串进行编码
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_EncryptAES_Error, None
def decrypt(self, text, receiveid):
"""对解密后的明文进行补位删除
@param text: 密文
@return: 删除填充补位后的明文
"""
try:
cryptor = AES.new(self.key, self.mode, self.key[:16])
# 使用BASE64对密文进行解码,然后AES-CBC解密
plain_text = cryptor.decrypt(base64.b64decode(text))
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_DecryptAES_Error, None
try:
pad = plain_text[-1]
# 去掉补位字符串
# pkcs7 = PKCS7Encoder()
# plain_text = pkcs7.encode(plain_text)
# 去除16位随机字符串
content = plain_text[16:-pad]
xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0])
xml_content = content[4: xml_len + 4]
from_receiveid = content[xml_len + 4:]
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_IllegalBuffer, None
if from_receiveid.decode('utf8') != receiveid:
return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None
return 0, xml_content
def get_random_str(self):
""" 随机生成16位字符串
@return: 16位字符串
"""
return str(random.randint(1000000000000000, 9999999999999999)).encode()
class WXBizMsgCrypt(object):
# 构造函数
def __init__(self, sToken, sEncodingAESKey, sReceiveId):
try:
self.key = base64.b64decode(sEncodingAESKey + "=")
assert len(self.key) == 32
except:
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
self.m_sToken = sToken
self.m_sReceiveId = sReceiveId
# 验证URL
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
# @param sNonce: 随机串,对应URL参数的nonce
# @param sEchoStr: 随机串,对应URL参数的echostr
# @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
# @return:成功0,失败返回对应的错误码
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
sha1 = SHA1()
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId)
return ret, sReplyEchoStr
def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None):
# 将企业回复用户的消息加密打包
# @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
# @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
# @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
# sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
# return:成功0sEncryptMsg,失败返回对应的错误码None
pc = Prpcrypt(self.key)
ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId)
encrypt = encrypt.decode('utf8')
if ret != 0:
return ret, None
if timestamp is None:
timestamp = str(int(time.time()))
# 生成安全签名
sha1 = SHA1()
ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
if ret != 0:
return ret, None
xmlParse = XMLParse()
return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce)
def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
# 检验消息的真实性,并且获取解密后的明文
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
# @param sNonce: 随机串,对应URL参数的nonce
# @param sPostData: 密文,对应POST请求的数据
# xml_content: 解密后的原文,当return返回0时有效
# @return: 成功0,失败返回对应的错误码
# 验证安全签名
xmlParse = XMLParse()
ret, encrypt = xmlParse.extract(sPostData)
if ret != 0:
return ret, None
sha1 = SHA1()
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId)
return ret, xml_content
@@ -1,20 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
# File Name: ierror.py
# Description:定义错误码含义
#########################################################################
WXBizMsgCrypt_OK = 0
WXBizMsgCrypt_ValidateSignature_Error = -40001
WXBizMsgCrypt_ParseXml_Error = -40002
WXBizMsgCrypt_ComputeSignature_Error = -40003
WXBizMsgCrypt_IllegalAesKey = -40004
WXBizMsgCrypt_ValidateCorpid_Error = -40005
WXBizMsgCrypt_EncryptAES_Error = -40006
WXBizMsgCrypt_DecryptAES_Error = -40007
WXBizMsgCrypt_IllegalBuffer = -40008
WXBizMsgCrypt_EncodeBase64_Error = -40009
WXBizMsgCrypt_DecodeBase64_Error = -40010
WXBizMsgCrypt_GenReturnXml_Error = -40011
-16
View File
@@ -1,16 +0,0 @@
#!/usr/bin/python
# -*- coding:utf-8 -*-
##
# Copyright (C) 2018 All rights reserved.
#
# @File conf.py
# @Brief
# @Author abelzhu, abelzhu@tencent.com
# @Version 1.0
# @Date 2018-02-23
#
#
## 设置为true会打印一些调试信息
DEBUG = True
-221
View File
@@ -1,221 +0,0 @@
加解密方案说明
概述
企业微信在推送消息给企业时,会对消息内容做AES加密,以XML格式POST到企业应用的URL上。
企业在被动响应时,也需要对数据加密,以XML格式返回给企业微信。
本章节即是对加解密方法的说明。
阅读本章节前,需要了解以下术语:
msg_signature: 消息签名,用于验证请求是否来自企业微信(防止攻击者伪造)。
EncodingAESKey:用于消息体的加密,长度固定为43个字符,从a-z, A-Z, 0-9共62个字符中选取,是AESKey的Base64编码。解码后即为32字节长的AESKey
AESKey=Base64_Decode(EncodingAESKey + “=”)
AESKey:AES算法的密钥,长度为32字节。
AES采用CBC模式,数据采用PKCS#7填充至32字节的倍数;IV初始向量大小为16字节,取AESKey前16字节,详见:http://tools.ietf.org/html/rfc2315
msg:为消息体明文,格式为XML/JSON
msg_encrypt:明文消息msg加密处理后的Base64编码。
使用已有库
鉴于加解密算法相对复杂,企业微信提供了算法库。
目前已有c++/python/php/java/golang/c#等语言版本。均提供了解密、加密、验证URL三个接口,企业可根据自身需要下载,下载地址。
使用现有库,用户不必细究加解密原理。对于找不到相应语言库的用户,请阅读后文原理详解自行实现。欢迎大家分享~
以c++为例,使用示例见下载的文件夹中的Sample.cpp, 此处做简单说明。
初始化加解密类
回调xml示例:
WXBizMsgCrypt wxcpt(sToken,sEncodingAESKey,sReceiveId);
回调json示例
WXBizJsonMsgCrypt wxcpt(sToken,sEncodingAESKey,sReceiveId);
要求传参数sToken,sEncodingAESKey,sReceiveId。
sToken,sEncodingAESKey即设置接收消息的参数章节所述配置的Token、EncodingAESKey。
特别注意, sReceiveId 在不同场景下有不同含义,见附注。
验证URL函数
本函数实现:
签名校验
解密数据包,得到明文消息内容
int VerifyURL(const string &sMsgSignature, const string &sTimeStamp, const string &sNonce, const string &sEchoStr, string &sReplyEchoStr);
参数说明
参数 必须 说明
sMsgSignature 是 从接收消息的URL中获取的msg_signature参数
sTimeStamp 是 从接收消息的URL中获取的timestamp参数
sNonce 是 从接收消息的URL中获取的nonce参数
sEchoStr 是 从接收消息的URL中获取的echostr参数。注意,此参数必须是urldecode后的值
sReplyEchoStr 是 解密后的明文消息内容,用于回包。注意,必须原样返回,不要做加引号或其它处理
解密函数
本函数实现:
签名校验
解密数据包,得到明文消息结构体
int DecryptMsg(const string &sMsgSignature, const string &sTimeStamp, const string &sNonce, const string &sPostData, string &sMsg);
参数说明
参数 必须 说明
sMsgSignature 是 从接收消息的URL中获取的msg_signature参数
sTimeStamp 是 从接收消息的URL中获取的timestamp参数
sNonce 是 从接收消息的URL中获取的nonce参数
sPostData 是 从接收消息的URL中获取的整个post数据
sMsg 是 用于返回解密后的msg,以xml组织,参见普通消息格式和事件消息格式
加密函数
本函数实现:
加密明文消息结构体
生成签名
构造被动响应包
int EncryptMsg(const string &sReplyMsg, const string &sTimeStamp, const string &sNonce, string &sEncryptMsg);
参数说明
参数 必须 说明
sReplyMsg 是 返回的消息体原文
sTimeStamp 是 时间戳,调用方生成
sNonce 是 随机数,调用方生成
sEncryptMsg 是 用于返回的密文,以xml组织,参见被动回复消息格式
原理详解
目前官方已提供了php、python、c++等版本的加解密库,如果开发者需要进行别的语言的开发,需要自行根据加解密原理实现算法。
消息体签名校验
为了让企业确认调用来自企业微信,企业微信在回调给接收消息url时会带上消息签名,以参数msg_signature标识,企业需要验证此参数的正确性后再解密。
验证步骤如下:
计算签名
dev_msg_signature=sha1(sort(token、timestamp、nonce、msg_encrypt))。
sort的含义是将参数值按照字母字典排序,然后从小到大拼接成一个字符串
sha1处理结果要编码为可见字符,编码的方式是把每字节散列值打印为%02x(即16进制,C printf语法)格式,全部小写
比较dev_msg_signature和msg_signature是否相等,相等则表示验证通过
在被动响应消息时,企业同样需要用如上方法生成签名并传给企业微信
明文msg的加密过程
拼接明文字符串
rand_msg = random(16B) + msg_len(4B) + msg + receiveid
明文字符串由16个字节的随机字符串、4个字节的msg长度、明文msg和receiveid拼接组成。其中msg_len为msg的字节数,网络字节序;sReceiveId 在不同场景下有不同含义,见附注
明文字符串
对明文字符串加密并Base64编码
msg_encrypt = Base64_Encode(AES_Encrypt(rand_msg))
将明文字符串AESKey加密后,再进行Base64编码,即获得密文msg_encrypt。
密文解密得到msg的过程
对密文BASE64解码
aes_msg=Base64_Decode(msg_encrypt)
使用AESKey做AES-256-CBC解密
rand_msg=AES_Decrypt(aes_msg)
去掉rand_msg头部的16个随机字节和4个字节的msg_len,截取msg_len长度的部分即为msg,剩下的为尾部的receiveid
验证解密后的receiveid、msg_len。注意,receiveid在不同场景含义不同。
举例说明
假设在服务商管理端为某个套件有如下配置参数:
corpId = "wx5823bf96d3bd56c7"
token = "QDG6eK"
encodingAesKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C"
收到来自企业微信的回调为:
xml请求示例:
POST /cgi-bin/wxpush?msg_signature=477715d11cdb4164915debcba66cb864d751f3e6&timestamp=1409659813&nonce=1372623149 HTTP/1.1
Host: qy.weixin.qq.com
Content-Length: 603
<xml>
<ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName>
<Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt>
<AgentID><![CDATA[218]]></AgentID>
</xml>
json请求示例:
注意这里的 tousernameencryptagentid均为小写
POST /cgi-bin/wxpush?msg_signature=477715d11cdb4164915debcba66cb864d751f3e6&timestamp=1409659813&nonce=1372623149 HTTP/1.1
Host: qy.weixin.qq.com
Content-Length: 364
{
"tousername": "wx5823bf96d3bd56c7",
"encrypt": "No8isRLoXqFMhLlpe7R/DA7UbJ88DKJxDhJH/UVG3o1ib0Fhzdd3qWYHH/KL1mITv5qOCp2FbyILqfI7zazrp/ARgSHR177OCrv8O9UrMHWdnOaMXaz+mLd5X5VWm5r2J3Qpm+NdTQRPhHbce88frKF3wqTaZunKW7ae87bRZUfaq5tLFnyTsf6aiy0su3SsQ06dQGKPcHfYHY3upB881008Q9t9xeAZ/uqfXpYQgSLQfaX+fk/K/FQEl4QpLk94eD1YjluFY3uLnKp40zDyxgeWRAmgTtmx1eLwediVqZ8=",
"agentid": "218"
}
第一步:准备相关参数
AESKey = Base64_Decode(EncodingAESKey + "=")
signature = "477715d11cdb4164915debcba66cb864d751f3e6";
timestamps = "1409659813";
nonce = "1372623149";
msg_encrypt = "RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==";
第二步:校验签名
token、timestamp、nonce、msg_encrypt 这四个参数按照字典序排序
"1372623149"
"1409659813"
"QDG6eK"
"RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q=="
拼接为一个字符串
sort_str = "13726231491409659813QDG6eKRypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q=="
对该字符串进行sha1计算得到签名
signature = sha1(sort_str) = "477715d11cdb4164915debcba66cb864d751f3e6"
对比从URL得到的签名,发现两者一致,签名通过,说明没被篡改,是安全的
第三步: 解密消息
对密文base64解码
aes_msg = base64_decode(msg_encrypt)
使用AESKey做AES解密(注意,不是EncodingAESKey
rand_msg = aes_decrypt(aes_msg, AESKey)
去掉rand_msg头部的16个随机字节和4个字节的msg_len,截取msg_len长度的部分即为msg,剩下的为尾部的receiveid
下面为类似python的伪代码
content = rand_msg[16:] # 去掉前16随机字节
msg_len = str_to_uint(content[0:4]) # 取出4字节的msg_len
msg = content[4:msg_len+4] # 截取msg_len 长度的msg
receiveid = content[msg_len+4:] = "wx5823bf96d3bd56c7" # 剩余字节为receiveid
对于回调xml解密后得到明文为:
<xml>
<ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName>
<FromUserName><![CDATA[mycreate]]></FromUserName>
<CreateTime>1409659813</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[hello]]></Content>
<MsgId>4561255354251345929</MsgId>
<AgentID>218</AgentID>
</xml>
对于回调json解密后的明文为:
{
"ToUserName": "wx5823bf96d3bd56c7",
"FromUserName": "mycreate",
"CreateTime": "1409659813",
"MsgType": "text",
"Content": "hello",
"MsgId": "4561255354251345929",
"AgentID": "218"
}
根据明文中的MsgType可知,此为应用消息回调,因此receiveid应该为corpid,对比receiveid与corpid是否一致。
附注:ReceiveId 含义
加解密库里,ReceiveId 在各个场景的含义不同:
企业应用的回调,表示corpid
第三方事件的回调,表示suiteid
机器人场景的回调,是一个空字符串
-77
View File
@@ -1,77 +0,0 @@
发送应用消息
最后更新:2025/09/24
接口定义
应用支持推送文本、图片、视频、文件、图文等类型。
请求方式:POSTHTTPS
请求地址: https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN
参数说明:
参数 是否必须 说明
access_token 是 调用接口凭证
- 各个消息类型的具体POST格式请阅后续“消息类型”部分。
- 如果有在管理端对应用设置“在微工作台中始终进入主页”,应用在微信端只能接收到文本消息,并且文本消息的长度限制为20字节,超过20字节会被截断。同时其他消息类型也会转换为文本消息,提示用户到企业微信查看。
- 支持id转译,将userid/部门id转成对应的用户名/部门名,在企业授权了会话内容存档接口权限时,也可以将消息id和群id转成对应的消息内容/群名称,目前仅文本/文本卡片/图文/图文(mpnews)/任务卡片/小程序通知/模版消息/模板卡片消息这八种消息类型的部分字段支持。具体支持的范围和语法,请查看附录id转译说明。
- 支持重复消息检查,当指定 "enable_duplicate_check": 1开启: 表示在一定时间间隔内,同样内容(请求json)的消息,不会重复收到;时间间隔可通过duplicate_check_interval指定,默认1800秒。
- 从2021年2月4日开始,企业关联添加的「小程序」应用,也可以发送文本、图片、视频、文件、图文等各种类型的消息了。
调用建议:大部分企业应用在每小时的0分或30分触发推送消息,容易造成资源挤占,从而投递不够及时,建议尽量避开这两个时间点进行调用。
频率限制:每应用不可超过账号上限数*200人次/天(注:若调用api一次发给1000人,算1000人次;若企业账号上限是500人,则每个应用每天可发送100000人次的消息)。每应用对同一个成员不可超过30次/分钟,1000次/小时,超过部分会被丢弃不下发
返回示例:
{
"errcode" : 0,
"errmsg" : "ok",
"invaliduser" : "userid1|userid2",
"invalidparty" : "partyid1|partyid2",
"invalidtag": "tagid1|tagid2",
"unlicenseduser" : "userid3|userid4",
"msgid": "xxxx",
"response_code": "xyzxyz"
}
如果部分接收人无权限或不存在,发送仍然执行,但会返回无效的部分(即invaliduser或invalidparty或invalidtag或unlicenseduser),常见的原因是接收人不在应用的可见范围内。
权限包含应用可见范围和基础接口权限(基础账号、互通账号均可),unlicenseduser中的用户在应用可见范围内但没有基础接口权限。
如果全部接收人无权限或不存在,则本次调用返回失败,errcode为81013。
返回包中的userid,不区分大小写,统一转为小写
---
文本卡片消息
请求示例:
```
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1 | PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "textcard",
"agentid" : 1,
"textcard" : {
"title" : "领奖通知",
"description" : "<div class=\"gray\">2016年9月26日</div> <div class=\"normal\">恭喜你抽中iPhone 7一台,领奖码:xxxx</div><div class=\"highlight\">请于2016年10月10日前联系行政同事领取</div>",
"url" : "URL",
"btntxt":"更多"
},
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
```
参数说明:
参数 是否必须 说明
touser 否 成员ID列表(消息接收者,多个接收者用‘|’分隔,最多支持1000个)。特殊情况:指定为@all,则向关注该企业应用的全部成员发送
toparty 否 部门ID列表,多个接收者用‘|’分隔,最多支持100个。当touser为@all时忽略本参数
totag 否 标签ID列表,多个接收者用‘|’分隔,最多支持100个。当touser为@all时忽略本参数
msgtype 是 消息类型,此时固定为:textcard
agentid 是 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值
title 是 标题,不超过128个字符,超过会自动截断(支持id转译)
description 是 描述,不超过512个字符,超过会自动截断(支持id转译)
url 是 点击后跳转的链接。最长2048字节,请确保包含了协议头(http/https)
btntxt 否 按钮文字。 默认为“详情”, 不超过4个文字,超过自动截断。
enable_id_trans 否 表示是否开启id转译,0表示否,1表示是,默认0
enable_duplicate_check 否 表示是否开启重复消息检查,0表示否,1表示是,默认0
duplicate_check_interval 否 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
特殊说明:
卡片消息的展现形式非常灵活,支持使用br标签或者空格来进行换行处理,也支持使用div标签来使用不同的字体颜色,目前内置了3种文字颜色:灰色(gray)、高亮(highlight)、默认黑色(normal),将其作为div标签的class属性即可,具体用法请参考上面的示例。
+478
View File
@@ -0,0 +1,478 @@
```bash
C:\Users\Voole>curl ^"http://www.gxdh.gov.cn/xxgk/zdlyxxgk/ggzypzly/zfcgly/cggg/^" ^
More? -H ^"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7^" ^
More? -H ^"Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6^" ^
More? -H ^"Connection: keep-alive^" ^
More? -b ^"Hm_lvt_49d07951332748974d2b32f1fc826392=1761567101; _trs_uv=mh93l70k_3622_kalm^" ^
More? -H ^"Referer: http://www.gxdh.gov.cn/xxgk/zdlyxxgk/ggzypzly/zfcgly/cggg/^" ^
More? -H ^"Upgrade-Insecure-Requests: 1^" ^
More? -H ^"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0^" ^
More? --insecure
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
采购公告 -
广西河池大化瑶族自治县人民政府门户网站
</title>
<meta name="renderer" content="webkit">
<meta http-equiv="x-ua-Compatible" content="IE=Edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="Sitename" content="广西河池大化瑶族自治县人民政府门户网站">
<meta name="SiteDomain" content="http://www.gxdh.gov.cn/">
<meta name="SiteIDCode" content="4512290003">
<meta name="ColumnName" content="采购公告" />
<meta name="ColumnDescription" content="发布关于采购公告相关信息" />
<meta name="ColumnKeywords" content="采购公告,采购公告">
<meta name="ColumnType" content="采购公告">
<link rel="stylesheet" href="/cssq/main.css">
<link rel="stylesheet" href="/cssq/gxdh/css/main.css">
<link rel="stylesheet" href="" id="skin">
<link rel="stylesheet" href="/cssq/base.min.css">
<script src="/jsq/fenye.js"></script>
<link rel="stylesheet" type="text/css" href="http://www.gxdh.gov.cn/material/css/mobile-header.css" />
</head>
<body>
<div class="header">
<div class="site-top">
<div class="wrap">
<div class="site-date fn-left"></div>
<div class="top-right fn-right">
<!--
<a href="" target="_blank">登录</a>
<a href="" target="_blank">注册</a>
-->
<!-- <a class="fn-left" href="http://www.pingguo.gov.cn/lxwm/20190422-1790072.shtml" target="_blank"><i class="icon i-weibo-top"></i></a>
<a class="fn-left" href="http://www.pingguo.gov.cn/lxwm/20190422-1790072.shtml" target="_blank"><i class="icon i-webchat-top"></i></a> -->
<a id="gxzf_t2s" href="javascript:void(0);" onclick="goUserCenter()">用户空间</a>|
<script>
function goUserCenter() {
window.open('http://tyrz.zwfw.gxzf.gov.cn/am/auth/login?goto=aHR0cDovL3R5cnouendmdy5neHpmLmdvdi5jbi9hbS9vYXV0aDIvYXV0aG9yaXplP3NlcnZpY2U9aW5pdFNlcnZpY2UmcmVzcG9uc2VfdHlwZT1jb2RlJmNsaWVudF9pZD1neHp3ZncmY2xpZW50X3NlY3JldD0xMTExMTEmc2NvcGU9YWxsJnJlZGlyZWN0X3VyaT1odHRwJTNBLy96d2Z3Lmd4emYuZ292LmNuL2d4endmdy9tZW1iZXIvbG9naW4vdG9sb2dpbi5kbyUzRmdvdG91cmwlM0RodHRwJTI1M0EvL3p3ZncuZ3h6Zi5nb3YuY24=&lackFlag=NA');
var cont = $('meta[name="ColumnType"]').attr('content');
if (cont == '首页') {
$.getJSON(json, function (res) {
$("#sliderNav").empty();
var arr = randomArray(res, 7); //json数组,展示个数1
appEnd(arr);
});
}
}
</script>
<a id="gxzf_t2s" href="http://www.gxdh.gov.cn/gx-znwd/home/gxdh" target="_blank">智能问答</a>|
<a id="gxzf_t2s" href="javascript: void(0); " target="_self">简体版</a>|
<a id="gxzf_s2t" href="javascript: void(0); " target="_self">繁体版</a>
<!--
|<a href="#">English</a>
-->
<div class="ipv6">支持IPv6</div>
<div id="gt_btn" class="ipv6 i-wza i-wza-people">无障碍</div>
<div class="ipv6 i-zzzq">
<a href="http://www.gxdh.gov.cn/zzzq/" target="_blank">长者专区</a>
</div>
<div class="i-jy"><a href="http://dsjfzj.gxzf.gov.cn/fzlm/gxzfwzjyhdh/index.shtml#10" target="_blank">集</a></div>
</div>
</div>
</div>
<div class="site-banner">
<div class="site-warp">
<a class="logo-wrap" href="/" target="_blank">
<img id="icon_gh" width="120" src="/imagesq/icon-gh.png" />
<img class="Company_name" src="http://www.gxdh.gov.cn/material/images/logo_ash.png" alt=" 广西河池大化瑶族自治县人民政府门户网站" />
</a>
<div class="site-search fn-right">
<div class="site-box">
<input class="site-search-txt" placeholder="请输入搜索内容" name="searchWord" type="text" />
<input type="hidden" name="siteId" value="121" />
<input class="i-site site-search-submit" type="button" value="" onClick="doSearch(this)" />
</div>
</div>
</div>
</div>
<div class="nav-list">
<ul class="nav">
<li class="i-index"><a href="http://www.gxdh.gov.cn/" target="_blank">首页</a></li>
<li class="i-city"><a href="http://www.gxdh.gov.cn/zjdh/" target="_blank">走进大化</a></li>
<li class="i-info-site"><a href="http://www.gxdh.gov.cn/xxgk/" target="_blank">政府信息公开</a></li>
<li class="i-info-site"><a href="http://www.gxdh.gov.cn/ggfw/" target="_blank">公共服务</a></li>
<li class="i-int"><a href="https://zwfw.gxzf.gov.cn/eportal/ui?regionCode=3107103&pageId=3cd3d403fb4443d5becb61375b2b093a" target="_blank">网上办事</a></li>
<li class="i-info-site"><a href="http://www.gxdh.gov.cn/zwdt/" target="_blank">政务地图</a></li>
<li class="i-exc"><a href="http://www.gxdh.gov.cn/zmhd/" target="_blank">互动交流</a></li>
<li class="i-data-site"><a href="http://www.gxdh.gov.cn/sjfb/" target="_blank">数据发布</a></li>
</ul>
</div>
</div>
<!--<style type="text/css">
.site-warp { background: url('http://www.gxdh.gov.cn/material/images/banner-bg.gif') 140px bottom no-repeat #fff;}
</style>-->
<style type="text/css">
.site-banner,.site-warp{height:405px;background: none; position: relative;}
.site-top{background: #eaf3fe66;}
.site-search{margin-top: 0px; border: none; width: 338px;}
.site-box{box-sizing: border-box; background: #eaf3fe66; padding: 8px; overflow: hidden;}
.logo{margin-top: 35px; position: absolute; left: 50%; transform: translateX(-50%);}
.nav{width: 1140px; margin: 0 auto;}
.nav li{margin: 0 14px;}
body{background: url(http://www.gxdh.gov.cn/material/images/dhrm_bg.jpg) top center no-repeat #f1f6fc;}
.ipv6{background-color: #4da0e2; color: #fff; font-size: 10px; line-height: 24px;float: right; margin-top: 3px;
padding: 0 10px; border-radius: 13px; height: 26px;}
.i-wza-people{padding-left: 25px;background-position: 7px 0px;}
.logo-wrap{display: block;width: 100%;margin: auto;}
img.Company_name.grayscale, img.Company_name {display: block; width: 800px; margin: 0 auto;}
img#icon_gh {display: block;width: 120px;margin: 0 auto; margin-top: 45px;}
.i-zzzq {
background-image: url(/material/images/zzzq/icon-laoren.png);
background-position: 10px 5px;
background-repeat: no-repeat;
background-size: 15px;
margin-right: 10px;
}
.i-zzzq a {
display: block;
width: 60px;
text-align: right;
color: #FFFFFF;
}
</style>
<div class="wrap bg-white">
<div class="crumb-nav">
当前位置:
<a href="../../../../../" target="_self" title="首页" class="CurrChnlCls">首页</a>&nbsp;&gt;&nbsp;<a href="../../../../" target="_self" title="政府信息公开" class="CurrChnlCls">政府信息公开</a>&nbsp;&gt;&nbsp;<a href="../../../" target="_self" title="重点领域信息 公开" class="CurrChnlCls">重点领域信息公开</a>&nbsp;&gt;&nbsp;<a href="../../" target="_self" title="公共资源配置领域" class="CurrChnlCls">公共资源配置领域</a>&nbsp;&gt;&nbsp;<a href="../" target="_self" title="政府采购领域" class="CurrChnlCls">政府采购领域</a>&nbsp;&gt;&nbsp;<a href="./" target="_self" title="采购公告" class="CurrChnlCls">采购公告</a>
</div>
<div id="morelist" class="more">
<div class="r_lmgd lmgd" style="display: none;"
data-gd="0">
<p>
</p>
</div>
<ul class="more-list">
<li><span>
2026-01-28
</span>
<a href="./t27193876.shtml" target="_blank" title="2025年度第二批中央水库移民后期扶持基金项目建设计划的竞争性 磋商招标公告">2025年度第二批中央水库移民后期扶持基金项目建设计划的竞争性磋商招标公告</a>
</li>
<li><span>
2026-01-28
</span>
<a href="./t27193548.shtml" target="_blank" title="大化镇坡了社区智慧村屯应急预警项目邀请报价的公告">大化镇坡 了社区智慧村屯应急预警项目邀请报价的公告</a>
</li>
<li><span>
2026-01-27
</span>
<a href="./t27195202.shtml" target="_blank" title="大化瑶族自治县七百弄乡2025年粤桂协作社会帮扶资金项目监理单 位邀请报名公告">大化瑶族自治县七百弄乡2025年粤桂协作社会帮扶资金项目监理单位邀请报名公告</a>
</li>
<li><span>
2026-01-27
</span>
<a href="./t27189570.shtml" target="_blank" title="宝安区援助大化瑶族自治县中医医院附属项目设计单位邀请报名公 告">宝安区援助大化瑶族自治县中医医院附属项目设计单位邀请报名公告</a>
</li>
<li><span>
2026-01-26
</span>
<a href="./t27187394.shtml" target="_blank" title="关于大化瑶族自治县紧密型县域医共体强基工程建设项目建议书及 可行性研究报告编制服务单位邀请报名公告">关于大化瑶族自治县紧密型县域医共体强基工程建设项目建议书及可行性研究报告编制服务单位邀请报名公告</a>
</li>
<li><span>
2026-01-23
</span>
<a href="./t27171613.shtml" target="_blank" title="关于2026年巩固拓展脱贫攻坚成果和乡村振兴项目预算审查服务公 司公开招募的公告">关于2026年巩固拓展脱贫攻坚成果和乡村振兴项目预算审查服务公司公开招募的公告</a>
</li>
</ul>
<div class="r_lmgd lmgd" style="display: none;"
data-gd="0">
<p>
</p>
</div>
<ul class="more-list">
<li><span>
2026-01-23
</span>
<a href="./t27169946.shtml" target="_blank" title="大化瑶族自治县大化镇达悟村东红屯村容村貌改造项目等3个项目设计单位邀请报名公告">大化瑶族自治县大化镇达悟村东红屯村容村貌改造项目等3个项目设计单位邀请报名公告</a>
</li>
<li><span>
2026-01-23
</span>
<a href="./t27168862.shtml" target="_blank" title="大化瑶族自治县七百弄乡2025年粤桂协作社会帮扶资金项目施工单 位邀请报名公告">大化瑶族自治县七百弄乡2025年粤桂协作社会帮扶资金项目施工单位邀请报名公告</a>
</li>
<li><span>
2026-01-23
</span>
<a href="./t27172680.shtml" target="_blank" title="大化瑶族自治县住房和城乡建设局关于2026年生态环境保护专项资 金农村环境综合整治项目(污水处理)工程设计预算、 项目监理、招标代理、预算审核等 服务邀请报名的公告">大化瑶族自治县住房和城乡建设局关于2026年生态环境保护专项资金农村环境综合整治项目(污水处理)工程设计预算、 项目监理、招标代理、预算审核等 服务邀请报名的公告</a>
</li>
<li><span>
2026-01-19
</span>
<a href="./t27150343.shtml" target="_blank" title="关于择优录用项目水保验收和报备服务单位的公告">关于择优录用 项目水保验收和报备服务单位的公告</a>
</li>
<li><span>
2026-01-12
</span>
<a href="./t27103117.shtml" target="_blank" title="大化瑶族自治县中医医院排洪管道改造及支护项目监理单位邀请报 名公告">大化瑶族自治县中医医院排洪管道改造及支护项目监理单位邀请报名公告</a>
</li>
<li><span>
2026-01-09
</span>
<a href="./t27098415.shtml" target="_blank" title="大化县2024年农村人居环境整治新增项目邀请报名公告">大化县2024年农村人居环境整治新增项目邀请报名公告</a>
</li>
</ul>
<div class="r_lmgd lmgd" style="display: none;"
data-gd="0">
<p>
</p>
</div>
<ul class="more-list">
<li><span>
2025-12-26
</span>
<a href="./t27057491.shtml" target="_blank" title="大化瑶族自治县文化广电体育和旅游局关于承办大化红水河亲水徒 步大会暨UTO大化越野跑山赛”项目招标公告">大化瑶族自治县文化广电体育和旅游局关于承办大化红水河亲水徒步大会暨UTO大化越野跑山赛”项目招标公告</a>
</li>
<li><span>
2025-12-22
</span>
<a href="./t27033543.shtml" target="_blank" title="大化瑶族自治县卫生健康局关于大化瑶族自治县中医医院排洪管道 改造及支护项目建设邀请报名公告">大化瑶族自治县卫生健康局关于大化瑶族自治县中医医院排洪管道改造及支护项目建设邀请报名公告</a>
</li>
<li><span>
2025-12-18
</span>
<a href="./t27023381.shtml" target="_blank" title="大化瑶族自治县大化镇仁良村党群服务中心地质勘测邀请报名公告">大化瑶族自治县大化镇仁良村党群服务中心地质勘测邀请报名公告</a>
</li>
<li><span>
2025-12-12
</span>
<a href="./t26628863.shtml" target="_blank" title="大化瑶族自治县教育局关于大化瑶族自治县实验中学机器人设备采 购项目邀请报价的公告">大化瑶族自治县教育局关于大化瑶族自治县实验中学机器人设备采购项目邀请报价的公告</a>
</li>
<li><span>
2025-12-10
</span>
<a href="./t26522720.shtml" target="_blank" title="大化瑶族自治县住房和城乡建设局关于组织开展大化瑶族自治县住 宅专项维修资金专户管理银行服务项目招标代理机构的议标报名公告">大化瑶族自治县住房和城乡建设局关于组织开展大化瑶族自治县住宅专项维修资金专户管理银行服务项目招标代理机构的议标报名公告</a>
</li>
<li><span>
2025-12-05
</span>
<a href="./t26276809.shtml" target="_blank" title="大化瑶族自治县发展和改革局关于2026年巩固拓展脱贫攻坚成果同 乡村振兴项目设计服务公司公开招募公告">大化瑶族自治县发展和改革局关于2026年巩固拓展脱贫攻坚成果同乡村振兴项目设计服务公司公开招募公告</a>
</li>
</ul>
<div class="r_lmgd lmgd" style="display: none;"
data-gd="0">
<p>
</p>
</div>
<ul class="more-list">
<li><span>
2025-12-01
</span>
<a href="./t26230612.shtml" target="_blank" title="大化瑶族自治县特殊教育学校特教教具康复设备采购邀请报价公告">大化瑶族自治县特殊教育学校特教教具康复设备采购邀请报价公告</a>
</li>
<li><span>
2025-11-27
</span>
<a href="./t26214442.shtml" target="_blank" title="大化县贡川乡坡线屯风貌改造提升工程—建筑改造项目、大化县重点乡镇棚户区改造项目北景镇2016年棚户区改造项目(I标段)、 大化县重点乡镇棚户区改造项目大化县都阳镇2016年棚户区改造项目(II标段)结算审核咨询工作招标公告">大化县贡川乡坡线屯风貌改造提升工程—建筑改造项目、大化县重点乡镇棚户区改造项目北景镇2016年棚户区改造项目(I标段)、 大化县重 点乡镇棚户区改造项目大化县都阳镇2016年棚户区改造项目(II标段)结算审核咨询工作招标公告</a>
</li>
<li><span>
2025-11-26
</span>
<a href="./t26211785.shtml" target="_blank" title="大化县2025年度城市C级危房维修加固 工程项目(大化县工程局住 宅楼五区1栋)维修加固方案编制、预算审核及监理单位招标公告">大化县2025年度城市C级危房维修加固 工程项目(大化县工程局住宅楼五区1栋)维修加固方案编制、预算审核及监理单位招标公告</a>
</li>
<li><span>
2025-11-24
</span>
<a href="./t26197396.shtml" target="_blank" title="大化镇上旗村望远至下刁、东盘、内角屯道路扩建工程监理单位邀 请报名公告">大化镇上旗村望远至下刁、东盘、内角屯道路扩建工程监理单位邀请报名公告</a>
</li>
<li><span>
2025-11-24
</span>
<a href="./t26197388.shtml" target="_blank" title="大化镇上旗村望远至下刁、东盘、内角屯道路扩建工程施工单位邀 请报名公告">大化镇上旗村望远至下刁、东盘、内角屯道路扩建工程施工单位邀请报名公告</a>
</li>
<li><span>
2025-11-24
</span>
<a href="./t26197374.shtml" target="_blank" title="大化瑶族自治县大化镇凤翔村美凤屯弄卜里甘蔗产业道路硬化项目 等3个项目监理单位邀请报名公告">大化瑶族自治县大化镇凤翔村美凤屯弄卜里甘蔗产业道路硬化项目等3个项目监理单位邀请报名公告</a>
</li>
</ul>
<div class="r_lmgd lmgd" style="display: none;"
data-gd="0">
<p>
</p>
</div>
<ul class="more-list">
<li><span>
2025-11-24
</span>
<a href="./t26197360.shtml" target="_blank" title="大化瑶族自治县大化镇凤翔村美凤屯弄卜里甘蔗产业道路硬化项目 等3个项目施工单位邀请报名公告">大化瑶族自治县大化镇凤翔村美凤屯弄卜里甘蔗产业道路硬化项目等3个项目施工单位邀请报名公告</a>
</li>
<li><span>
2025-11-17
</span>
<a href="./t26175508.shtml" target="_blank" title="大化瑶族自治县人力资源和社会保障局关于面向社会公开询价采购2026年基层公共就业服务项目的公告">大化瑶族自治县人力资源和社会保障局关于面向社会公开询价采购2026年基层公共就业服务项目的公告</a>
</li>
<li><span>
2025-11-17
</span>
<a href="./t26178684.shtml" target="_blank" title="大化瑶族自治县高级中学科教综合楼桩基检测专项服务项目分散采 购询价公告">大化瑶族自治县高级中学科教综合楼桩基检测专项服务项目分散采购询价公告</a>
</li>
<li><span>
2025-11-13
</span>
<a href="./t26164807.shtml" target="_blank" title="大化瑶族自治县人力资源和社会保障局关于为大化瑶族自治县2026 年度公益性岗位在岗人员购买意外伤害商业保险的询价公告">大化瑶族自治县人力资源和社会保障局关于为大化瑶族自治县2026年度公益性岗位在岗人员购买意外伤害商业保险的询价公告</a>
</li>
<li><span>
2025-11-13
</span>
<a href="./t26164559.shtml" target="_blank" title="大化瑶族自治县乡镇枢纽农村产业服务中心项目监理服务机构邀请 报名公告">大化瑶族自治县乡镇枢纽农村产业服务中心项目监理服务机构邀请报名公告</a>
</li>
<li><span>
2025-11-12
</span>
<a href="./t26157706.shtml" target="_blank" title="大化瑶族自治县2025年革命老区转移支付增量资金建设项目邀请报 名公告">大化瑶族自治县2025年革命老区转移支付增量资金建设项目邀请报名公告</a>
</li>
</ul>
<div class="page">
<div class="more-page">
<script>createPageHTML(22, 0, "index", "shtml", "647");</script>
&nbsp;
</div>
</div>
</div>
</div>
<div class="footer-site">
<div class="wrap">
<ul style="width: 100%;">
<li class="footer-link-wrong">
<script id="_jiucuo_" sitecode='4512290003' src='https://zfwzgl.www.gov.cn/exposure/jiucuo.js'></script>
</li>
<li class="footer-link-gov"><a href="http://bszs.conac.cn/sitename?method=show&amp;id=27249F7FD0875813E053012819AC8426" target="_blank"><img id="imgConac" src="/imagesq/dzjg.png" data-bd-imgshare-binded="1" vspace="0" hspace="0" border="0" /></a></li>
<li class="footer-txt">
<p>
<a href="http://www.gxdh.gov.cn/map.shtml" target="_blank">网站地图</a>
<a href="http://www.gxdh.gov.cn/lxwm/t885224.shtml" target="_BLANK" title="联系我们">联系我们</a></p>
<p>主办:大化瑶族自治县人民政府办公室</p>
<p>承办:大化瑶族自治县大数据发展局</p>
<p>地址:大化瑶族自治县大化镇花锦路2号&nbsp;&nbsp;&nbsp;&nbsp;邮编:530800</p>
<p>联系电话:0778-5872869仅受理网站建设维护相关事宜</p>
<p>邮箱:dhdsjj2019@126.com</p>
<p>
<a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=45122902000008" target="_blank">
<img src="/imagesq/ba.png" /> 桂公网安备 45122902000008号
</a> <a href="http://beian.miit.gov.cn" target="_blank">桂ICP备14001878号-1
 </a>网站标识码 4512290003
</p>
</li>
<!--<li class="footer-link-wrong" style="width: 250px;padding-top: 0px;">
<a href="http://www.gxzf.gov.cn/wxhkhd/message.shtml?n=广西河池大化瑶族自治县人民政府门户网站&aid=822&sid=129" target="_blank">
<img src="http://www.gxdh.gov.cn/material/images/wzjc.png" />
</a>
</li>-->
<li class="footer-web" style="padding-left: 9px;">
<img src="http://www.gxdh.gov.cn/wbwx/material/dhyzzzxzfmhwz1.jpg" width="100px" alt="微信公众号" /><p>微信公众号</p>
</li>
</ul>
</div>
</div>
<style type="text/css">
.footer-txt{width: 552px;}
</style>
<!--百度统计BEG-->
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?49d07951332748974d2b32f1fc826392";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<!--百度统计END-->
<script src="/jsq/jquery.min.js"></script>
<script src="/jsq/jquery.cookie.js"></script>
<script src="/jsq/jquery.s2t.js"></script>
<script src="/jsq/jquery.qrcode.min.js"></script>
<script src="/jsq/chinese_status.js"></script>
<script src="/jsq/checklink.min.js"></script>
<script src="/jsq/main.js"></script>
<script src="/jsq/wza.js"></script>
<script src="/jsq/api_gwy.js"></script>
<script src="http://www.gxdh.gov.cn/material/js/Access_conf.js"></script>
<!--无障碍-->
<script type="text/javascript">document.write("<script src='/accessTool/Access_Initial.js?"+Math.random()+"'><\/script>");</script>
<script>
var version =Date.parse(new Date());
document.write('<script src="/jsq/api_func.js?v='+version+'"><\/script>');
SITEID = 129;
APPID = 25;
</script>
</script><script id="_trs_ta_js" src="//ta.trs.cn/c/js/ta.js?mpid=3622" async="async" defer="defer"></script>
<script src="http://www.gxdh.gov.cn/material/js/M-index.js" type="text/javascript" charset="utf-8"></script>
</body>
<script>
document.addEventListener('DOMContentLoaded', function () {
const elements = document.querySelectorAll('div[data-gd="0"]');
elements.forEach(function (element) {
element.style.display = 'none';
});
});
</script>
</html>