Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c548a8b5bd | |||
| bd8df98e9a | |||
| b37e8ce581 | |||
| f00068f804 | |||
| 72dc40186d | |||
| 4000438f49 | |||
| 85cd7e9d3e | |||
| 04d254cfef | |||
| b52d9c069d | |||
| 5f8ebd2da5 |
-231
@@ -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.
@@ -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.
@@ -25,7 +25,7 @@ crawler:
|
|||||||
proxies: [] # 代理列表
|
proxies: [] # 代理列表
|
||||||
request_delay: 1.0 # 请求间延迟
|
request_delay: 1.0 # 请求间延迟
|
||||||
request_delay_max: 3.0 # 请求间最大延迟
|
request_delay_max: 3.0 # 请求间最大延迟
|
||||||
keyword: ["大化", "信息化"] # 关键词筛选(支持多个关键词)
|
keyword: ["大化"] # 关键词筛选(支持多个关键词)
|
||||||
start_date: "" # 开始日期 (YYYY-MM-DD)
|
start_date: "" # 开始日期 (YYYY-MM-DD)
|
||||||
end_date: "" # 结束日期 (YYYY-MM-DD)
|
end_date: "" # 结束日期 (YYYY-MM-DD)
|
||||||
max_pages: 10 # 最大页数
|
max_pages: 10 # 最大页数
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+373
-10
@@ -182,7 +182,47 @@ class DatabaseManager:
|
|||||||
|
|
||||||
# 创建表的SQL语句
|
# 创建表的SQL语句
|
||||||
create_tables_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 (
|
CREATE TABLE IF NOT EXISTS announcements (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
title VARCHAR(500) NOT NULL,
|
title VARCHAR(500) NOT NULL,
|
||||||
@@ -195,6 +235,7 @@ class DatabaseManager:
|
|||||||
crawled_at TIMESTAMP,
|
crawled_at TIMESTAMP,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
crawl_mode VARCHAR(20) DEFAULT 'auto',
|
||||||
content_hash VARCHAR(32) UNIQUE,
|
content_hash VARCHAR(32) UNIQUE,
|
||||||
keyword_matched BOOLEAN DEFAULT FALSE,
|
keyword_matched BOOLEAN DEFAULT FALSE,
|
||||||
date_filtered BOOLEAN DEFAULT TRUE,
|
date_filtered BOOLEAN DEFAULT TRUE,
|
||||||
@@ -223,12 +264,32 @@ class DatabaseManager:
|
|||||||
FOREIGN KEY (source_code) REFERENCES announcement_sources(code)
|
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_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_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_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_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_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()
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
@@ -330,13 +391,101 @@ class DatabaseManager:
|
|||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
INSERT INTO announcements (
|
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,
|
title, publish_date, purchase_name, content_url, source_code, source_name,
|
||||||
announcement_type, crawled_at, content_hash, keyword_matched,
|
announcement_type, crawled_at, content_hash, keyword_matched,
|
||||||
date_filtered, is_new, is_today
|
date_filtered, is_new, is_today
|
||||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
ON CONFLICT (content_hash) DO NOTHING
|
ON CONFLICT (content_hash) DO NOTHING
|
||||||
"""
|
"""
|
||||||
|
|
||||||
values = []
|
values = []
|
||||||
for announcement in announcements:
|
for announcement in announcements:
|
||||||
values.append((
|
values.append((
|
||||||
@@ -359,10 +508,11 @@ class DatabaseManager:
|
|||||||
with get_db_cursor() as cursor:
|
with get_db_cursor() as cursor:
|
||||||
extras.execute_batch(cursor, sql, values)
|
extras.execute_batch(cursor, sql, values)
|
||||||
affected_rows = cursor.rowcount
|
affected_rows = cursor.rowcount
|
||||||
logger.info(f"批量保存公告完成,成功保存 {affected_rows} 条")
|
logger.info(f"批量保存公告到{table_name}完成,影响行数: {affected_rows}")
|
||||||
return affected_rows
|
return affected_rows
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"批量保存公告失败: {str(e)}")
|
logger.error(f"批量保存公告到{table_name}失败: {str(e)}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@retry_on_exception(RetryConfig(max_retries=3))
|
@retry_on_exception(RetryConfig(max_retries=3))
|
||||||
@@ -504,21 +654,96 @@ class DatabaseManager:
|
|||||||
if not self.config.database.enabled:
|
if not self.config.database.enabled:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
# 统计所有表的综合信息
|
||||||
sql = """
|
sql = """
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_announcements,
|
SUM(total_count) as total_announcements,
|
||||||
COUNT(CASE WHEN is_today THEN 1 END) as today_announcements,
|
SUM(today_count) as today_announcements,
|
||||||
COUNT(CASE WHEN is_new THEN 1 END) as new_announcements,
|
SUM(new_count) as new_announcements,
|
||||||
COUNT(DISTINCT source_code) as sources_count,
|
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
|
MAX(crawled_at) as last_crawl_time
|
||||||
FROM announcements
|
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:
|
try:
|
||||||
with get_db_cursor() as cursor:
|
with get_db_cursor() as cursor:
|
||||||
cursor.execute(sql)
|
cursor.execute(sql)
|
||||||
result = cursor.fetchone()
|
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:
|
except Exception as e:
|
||||||
logger.error(f"获取统计信息失败: {str(e)}")
|
logger.error(f"获取统计信息失败: {str(e)}")
|
||||||
return {}
|
return {}
|
||||||
@@ -536,11 +761,22 @@ class DatabaseManager:
|
|||||||
if not self.config.database.enabled:
|
if not self.config.database.enabled:
|
||||||
return False
|
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:
|
try:
|
||||||
with get_db_cursor() as cursor:
|
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
|
return cursor.fetchone() is not None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"检查公告存在性失败: {str(e)}")
|
logger.error(f"检查公告存在性失败: {str(e)}")
|
||||||
@@ -583,6 +819,133 @@ class DatabaseManager:
|
|||||||
logger.error(f"获取最近公告失败: {str(e)}")
|
logger.error(f"获取最近公告失败: {str(e)}")
|
||||||
return []
|
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
|
_db_manager = None
|
||||||
|
|||||||
@@ -33,15 +33,25 @@ class ColoredFormatter(logging.Formatter):
|
|||||||
# 如果已经着色,直接返回原始格式
|
# 如果已经着色,直接返回原始格式
|
||||||
return super().format(record)
|
return super().format(record)
|
||||||
|
|
||||||
|
# 保存原始值
|
||||||
|
original_levelname = record.levelname
|
||||||
|
original_msg = record.msg
|
||||||
|
|
||||||
# 添加颜色
|
# 添加颜色
|
||||||
if record.levelname in self.COLORS:
|
if record.levelname in self.COLORS:
|
||||||
# 为levelname添加颜色
|
# 为levelname添加颜色
|
||||||
colored_levelname = f"{self.COLORS[record.levelname]}{record.levelname}{self.RESET}"
|
record.levelname = f"{self.COLORS[record.levelname]}{record.levelname}{self.RESET}"
|
||||||
# 使用原始levelname来确定消息的颜色
|
# 为消息添加颜色
|
||||||
record.msg = f"{self.COLORS[record.levelname]}{record.msg}{self.RESET}"
|
record.msg = f"{self.COLORS[original_levelname]}{record.msg}{self.RESET}"
|
||||||
record.levelname = colored_levelname
|
|
||||||
|
|
||||||
return super().format(record)
|
# 格式化
|
||||||
|
result = super().format(record)
|
||||||
|
|
||||||
|
# 恢复原始值,避免影响其他处理器
|
||||||
|
record.levelname = original_levelname
|
||||||
|
record.msg = original_msg
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class Logger:
|
class Logger:
|
||||||
@@ -59,6 +69,7 @@ class Logger:
|
|||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
self._loggers = {}
|
self._loggers = {}
|
||||||
self._config = None
|
self._config = None
|
||||||
|
self._file_handler = None # 全局文件处理器,避免重复创建
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
def init_logger(self, name: str = "gx_gp_monitor", config=None) -> logging.Logger:
|
def init_logger(self, name: str = "gx_gp_monitor", config=None) -> logging.Logger:
|
||||||
@@ -89,6 +100,9 @@ class Logger:
|
|||||||
logger = logging.getLogger(name)
|
logger = logging.getLogger(name)
|
||||||
logger.setLevel(getattr(logging, self._config.log_level.value))
|
logger.setLevel(getattr(logging, self._config.log_level.value))
|
||||||
|
|
||||||
|
# 防止消息传播到父logger,避免重复记录
|
||||||
|
logger.propagate = False
|
||||||
|
|
||||||
# 避免重复添加处理器
|
# 避免重复添加处理器
|
||||||
if logger.handlers:
|
if logger.handlers:
|
||||||
return logger
|
return logger
|
||||||
@@ -115,20 +129,23 @@ class Logger:
|
|||||||
|
|
||||||
logger.addHandler(console_handler)
|
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 = Path(self._config.log_file).parent
|
||||||
log_dir.mkdir(parents=True, exist_ok=True)
|
log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
file_handler = logging.handlers.RotatingFileHandler(
|
self._file_handler = logging.handlers.RotatingFileHandler(
|
||||||
self._config.log_file,
|
self._config.log_file,
|
||||||
maxBytes=self._config.log_max_size,
|
maxBytes=self._config.log_max_size,
|
||||||
backupCount=self._config.log_backup_count,
|
backupCount=self._config.log_backup_count,
|
||||||
encoding='utf-8'
|
encoding='utf-8'
|
||||||
)
|
)
|
||||||
file_handler.setLevel(getattr(logging, self._config.log_level.value))
|
self._file_handler.setLevel(getattr(logging, self._config.log_level.value))
|
||||||
file_handler.setFormatter(formatter)
|
self._file_handler.setFormatter(formatter)
|
||||||
logger.addHandler(file_handler)
|
|
||||||
|
# 为所有logger添加全局文件处理器
|
||||||
|
if self._file_handler is not None:
|
||||||
|
logger.addHandler(self._file_handler)
|
||||||
|
|
||||||
self._loggers[name] = logger
|
self._loggers[name] = logger
|
||||||
return logger
|
return logger
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class Announcement:
|
|||||||
crawled_at: Optional[datetime] = None # 爬取时间
|
crawled_at: Optional[datetime] = None # 爬取时间
|
||||||
created_at: Optional[datetime] = None # 创建时间
|
created_at: Optional[datetime] = None # 创建时间
|
||||||
updated_at: Optional[datetime] = None # 更新时间
|
updated_at: Optional[datetime] = None # 更新时间
|
||||||
|
crawl_mode: str = "auto" # 爬取模式:auto(自动)/manual(手动)
|
||||||
|
|
||||||
# 去重字段
|
# 去重字段
|
||||||
content_hash: Optional[str] = None # 内容哈希,用于去重
|
content_hash: Optional[str] = None # 内容哈希,用于去重
|
||||||
|
|||||||
Binary file not shown.
@@ -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()
|
||||||
Executable
+239
@@ -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)
|
||||||
@@ -1,858 +0,0 @@
|
|||||||
2026-01-07 17:16:51 - __main__ - [32mINFO[0m - === 广西政府采购网公告监控系统启动 ===
|
|
||||||
2026-01-07 17:16:51 - __main__ - [32mINFO[0m - 版本: 1.0.0
|
|
||||||
2026-01-07 17:16:51 - __main__ - [32mINFO[0m - 配置文件: 默认配置
|
|
||||||
2026-01-07 17:16:52 - core.reliability - [32mINFO[0m - 健康检查通过
|
|
||||||
2026-01-07 17:16:52 - core.database - [32mINFO[0m - 数据库连接池初始化成功
|
|
||||||
2026-01-07 17:16:52 - core.database - [32mINFO[0m - 开始初始化数据库表结构
|
|
||||||
2026-01-07 17:16:52 - core.database - [32mINFO[0m - 数据库表结构初始化完成
|
|
||||||
2026-01-07 17:16:52 - storage.postgresql - [32mINFO[0m - 存储初始化完成
|
|
||||||
2026-01-07 17:16:52 - __main__ - [32mINFO[0m - 应用初始化完成
|
|
||||||
2026-01-07 17:16:52 - scheduler.scheduler - [32mINFO[0m - 添加定时任务: daily_crawl (0 8,14,18 * * *)
|
|
||||||
2026-01-07 17:16:52 - scheduler.scheduler - [32mINFO[0m - 添加定时任务: data_cleanup (0 2 * * *)
|
|
||||||
2026-01-07 17:20:31 - __main__ - [32mINFO[0m - === 广西政府采购网公告监控系统启动 ===
|
|
||||||
2026-01-07 17:20:31 - __main__ - [32mINFO[0m - 版本: 1.0.0
|
|
||||||
2026-01-07 17:20:31 - __main__ - [32mINFO[0m - 配置文件: 默认配置
|
|
||||||
2026-01-07 17:20:31 - core.reliability - [32mINFO[0m - 健康检查通过
|
|
||||||
2026-01-07 17:20:31 - core.database - [32mINFO[0m - 数据库连接池初始化成功
|
|
||||||
2026-01-07 17:20:31 - core.database - [32mINFO[0m - 开始初始化数据库表结构
|
|
||||||
2026-01-07 17:20:31 - core.database - [32mINFO[0m - 数据库表结构初始化完成
|
|
||||||
2026-01-07 17:20:31 - storage.postgresql - [32mINFO[0m - 存储初始化完成
|
|
||||||
2026-01-07 17:20:31 - __main__ - [32mINFO[0m - 应用初始化完成
|
|
||||||
2026-01-07 17:20:31 - scheduler.scheduler - [32mINFO[0m - 添加定时任务: daily_crawl (0 8,14,18 * * *)
|
|
||||||
2026-01-07 17:20:31 - scheduler.scheduler - [32mINFO[0m - 添加定时任务: data_cleanup (0 2 * * *)
|
|
||||||
2026-01-07 17:21:02 - __main__ - [32mINFO[0m - === 广西政府采购网公告监控系统启动 ===
|
|
||||||
2026-01-07 17:21:02 - __main__ - [32mINFO[0m - 版本: 1.0.0
|
|
||||||
2026-01-07 17:21:02 - __main__ - [32mINFO[0m - 配置文件: 默认配置
|
|
||||||
2026-01-07 17:21:02 - core.reliability - [32mINFO[0m - 健康检查通过
|
|
||||||
2026-01-07 17:21:02 - core.database - [32mINFO[0m - 数据库连接池初始化成功
|
|
||||||
2026-01-07 17:21:02 - core.database - [32mINFO[0m - 开始初始化数据库表结构
|
|
||||||
2026-01-07 17:21:02 - core.database - [32mINFO[0m - 数据库表结构初始化完成
|
|
||||||
2026-01-07 17:21:02 - storage.postgresql - [32mINFO[0m - 存储初始化完成
|
|
||||||
2026-01-07 17:21:02 - __main__ - [32mINFO[0m - 应用初始化完成
|
|
||||||
2026-01-07 17:21:02 - scheduler.scheduler - [32mINFO[0m - 添加定时任务: daily_crawl (0 8,14,18 * * *)
|
|
||||||
2026-01-07 17:21:02 - scheduler.scheduler - [32mINFO[0m - 添加定时任务: data_cleanup (0 2 * * *)
|
|
||||||
2026-01-07 17:22:10 - __main__ - [32mINFO[0m - === 广西政府采购网公告监控系统启动 ===
|
|
||||||
2026-01-07 17:22:10 - __main__ - [32mINFO[0m - 版本: 1.0.0
|
|
||||||
2026-01-07 17:22:10 - __main__ - [32mINFO[0m - 配置文件: 默认配置
|
|
||||||
2026-01-07 17:22:10 - core.reliability - [32mINFO[0m - 健康检查通过
|
|
||||||
2026-01-07 17:22:10 - core.database - [32mINFO[0m - 数据库连接池初始化成功
|
|
||||||
2026-01-07 17:22:10 - core.database - [32mINFO[0m - 开始初始化数据库表结构
|
|
||||||
2026-01-07 17:22:10 - core.database - [32mINFO[0m - 数据库表结构初始化完成
|
|
||||||
2026-01-07 17:22:10 - storage.postgresql - [32mINFO[0m - 存储初始化完成
|
|
||||||
2026-01-07 17:22:10 - __main__ - [32mINFO[0m - 应用初始化完成
|
|
||||||
2026-01-07 17:22:10 - scheduler.scheduler - [32mINFO[0m - 添加定时任务: daily_crawl (0 8,14,18 * * *)
|
|
||||||
2026-01-07 17:22:10 - scheduler.scheduler - [32mINFO[0m - 添加定时任务: data_cleanup (0 2 * * *)
|
|
||||||
2026-01-07 17:22:44 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:22:44 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:22:44 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:22:44 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:22:44 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:22:44 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:22:44 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:22:44 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:22:44 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:22:44 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: daily_crawl (0 8,14,18 * * *)[0m
|
|
||||||
2026-01-07 17:22:44 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: data_cleanup (0 2 * * *)[0m
|
|
||||||
2026-01-07 17:22:46 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:22:46 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:22:46 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:22:47 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:22:47 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:22:47 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:22:47 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:22:47 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:22:47 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:22:47 - __main__ - [32mINFO[0m - [32m开始执行爬取任务[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [32mINFO[0m - [32m开始爬取 13 个公告来源[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 采购公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 结果公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 合同公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 更正公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 招标文件预公示 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 单一来源公示 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 电子卖场公示 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 履约验收公示 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 工程类公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 框架协议征集公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 框架协议入围结果公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 框架协议成交结果汇总公告 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [31mERROR[0m - [31m爬取来源 采购意向公开 时发生未预期错误: 'Logger' object has no attribute 'log_crawl_start'[0m
|
|
||||||
2026-01-07 17:22:47 - crawler.spider - [32mINFO[0m - [32m爬取完成: 共处理 13 个来源,成功 0 个,失败 13 个,获取 0 条公告[0m
|
|
||||||
2026-01-07 17:22:47 - __main__ - [32mINFO[0m - [32m爬取到 0 条原始公告[0m
|
|
||||||
2026-01-07 17:22:47 - __main__ - [31mERROR[0m - [31m爬取任务执行失败: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:22:47 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: daily_crawl (0 8,14,18 * * *)[0m
|
|
||||||
2026-01-07 17:22:47 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: data_cleanup (0 2 * * *)[0m
|
|
||||||
2026-01-07 17:23:10 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:23:10 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:23:10 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:23:10 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:23:10 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:23:10 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:23:10 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:23:10 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:23:10 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:23:10 - __main__ - [32mINFO[0m - [32m开始执行爬取任务[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [32mINFO[0m - [32m开始爬取 13 个公告来源[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 采购公告 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 结果公告 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 合同公告 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 更正公告 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 招标文件预公示 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 单一来源公示 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 电子卖场公示 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 履约验收公示 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 工程类公告 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 框架协议征集公告 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 框架协议入围结果公告 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 框架协议成交结果汇总公告 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [31mERROR[0m - [31m爬取来源 采购意向公开 时发生未预期错误: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - crawler.spider - [32mINFO[0m - [32m爬取完成: 共处理 13 个来源,成功 0 个,失败 13 个,获取 0 条公告[0m
|
|
||||||
2026-01-07 17:23:10 - __main__ - [32mINFO[0m - [32m爬取到 0 条原始公告[0m
|
|
||||||
2026-01-07 17:23:10 - __main__ - [31mERROR[0m - [31m爬取任务执行失败: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:23:10 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: daily_crawl (0 8,14,18 * * *)[0m
|
|
||||||
2026-01-07 17:23:10 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: data_cleanup (0 2 * * *)[0m
|
|
||||||
2026-01-07 17:23:34 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:23:34 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:23:34 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:23:35 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:23:35 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:23:35 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:23:35 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:23:35 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:23:35 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:23:35 - __main__ - [32mINFO[0m - [32m开始执行爬取任务[0m
|
|
||||||
2026-01-07 17:23:35 - crawler.spider - [32mINFO[0m - [32m开始爬取 13 个公告来源[0m
|
|
||||||
2026-01-07 17:23:35 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购公告[0m
|
|
||||||
2026-01-07 17:24:05 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:05 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:05 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:05 - crawler.parsers - [32mINFO[0m - [32m成功解析 97/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:05 - gx_gp_monitor - [32mINFO[0m - [32m采购公告 爬取完成,共获取 97 条公告,耗时 30.46秒[0m
|
|
||||||
2026-01-07 17:24:05 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 结果公告[0m
|
|
||||||
2026-01-07 17:24:05 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:05 - gx_gp_monitor - [32mINFO[0m - [32m结果公告 爬取完成,共获取 100 条公告,耗时 0.18秒[0m
|
|
||||||
2026-01-07 17:24:05 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 合同公告[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:06 - gx_gp_monitor - [32mINFO[0m - [32m合同公告 爬取完成,共获取 100 条公告,耗时 0.17秒[0m
|
|
||||||
2026-01-07 17:24:06 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 更正公告[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:06 - gx_gp_monitor - [32mINFO[0m - [32m更正公告 爬取完成,共获取 100 条公告,耗时 0.23秒[0m
|
|
||||||
2026-01-07 17:24:06 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 招标文件预公示[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [32mINFO[0m - [32m成功解析 78/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:06 - gx_gp_monitor - [32mINFO[0m - [32m招标文件预公示 爬取完成,共获取 78 条公告,耗时 0.17秒[0m
|
|
||||||
2026-01-07 17:24:06 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 单一来源公示[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [32mINFO[0m - [32m成功解析 0/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.spider - [32mINFO[0m - [32m单一来源公示 第1页解析到0条公告[0m
|
|
||||||
2026-01-07 17:24:06 - gx_gp_monitor - [32mINFO[0m - [32m单一来源公示 爬取完成,共获取 0 条公告,耗时 0.18秒[0m
|
|
||||||
2026-01-07 17:24:06 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 电子卖场公示[0m
|
|
||||||
2026-01-07 17:24:06 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:06 - gx_gp_monitor - [32mINFO[0m - [32m电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.31秒[0m
|
|
||||||
2026-01-07 17:24:06 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 履约验收公示[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:07 - gx_gp_monitor - [32mINFO[0m - [32m履约验收公示 爬取完成,共获取 100 条公告,耗时 0.16秒[0m
|
|
||||||
2026-01-07 17:24:07 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 工程类公告[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:07 - gx_gp_monitor - [32mINFO[0m - [32m工程类公告 爬取完成,共获取 100 条公告,耗时 0.27秒[0m
|
|
||||||
2026-01-07 17:24:07 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议征集公告[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [32mINFO[0m - [32m成功解析 62/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:07 - gx_gp_monitor - [32mINFO[0m - [32m框架协议征集公告 爬取完成,共获取 62 条公告,耗时 0.20秒[0m
|
|
||||||
2026-01-07 17:24:07 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议入围结果公告[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:07 - crawler.parsers - [32mINFO[0m - [32m成功解析 13/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:07 - gx_gp_monitor - [32mINFO[0m - [32m框架协议入围结果公告 爬取完成,共获取 13 条公告,耗时 0.17秒[0m
|
|
||||||
2026-01-07 17:24:07 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议成交结果汇总公告[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [31mERROR[0m - [31m解析单个公告记录失败: 'NoneType' object has no attribute 'strip'[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [32mINFO[0m - [32m成功解析 0/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.spider - [32mINFO[0m - [32m框架协议成交结果汇总公告 第1页解析到0条公告[0m
|
|
||||||
2026-01-07 17:24:08 - gx_gp_monitor - [32mINFO[0m - [32m框架协议成交结果汇总公告 爬取完成,共获取 0 条公告,耗时 0.29秒[0m
|
|
||||||
2026-01-07 17:24:08 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购意向公开[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:24:08 - gx_gp_monitor - [32mINFO[0m - [32m采购意向公开 爬取完成,共获取 100 条公告,耗时 0.17秒[0m
|
|
||||||
2026-01-07 17:24:08 - crawler.spider - [32mINFO[0m - [32m爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 950 条公告[0m
|
|
||||||
2026-01-07 17:24:08 - __main__ - [32mINFO[0m - [32m爬取到 950 条原始公告[0m
|
|
||||||
2026-01-07 17:24:08 - __main__ - [31mERROR[0m - [31m爬取任务执行失败: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:24:08 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: daily_crawl (0 8,14,18 * * *)[0m
|
|
||||||
2026-01-07 17:24:08 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: data_cleanup (0 2 * * *)[0m
|
|
||||||
2026-01-07 17:24:17 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:24:17 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:24:17 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:24:17 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:24:17 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:24:17 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:24:17 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:24:17 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:24:17 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:24:17 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: daily_crawl (0 8,14,18 * * *)[0m
|
|
||||||
2026-01-07 17:24:17 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: data_cleanup (0 2 * * *)[0m
|
|
||||||
2026-01-07 17:26:34 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:26:34 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:26:34 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:26:34 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:26:34 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:26:34 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:26:34 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:26:34 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:26:34 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:26:34 - __main__ - [32mINFO[0m - [32m开始执行爬取任务[0m
|
|
||||||
2026-01-07 17:26:34 - crawler.spider - [32mINFO[0m - [32m开始爬取 13 个公告来源[0m
|
|
||||||
2026-01-07 17:26:34 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购公告[0m
|
|
||||||
2026-01-07 17:26:34 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:34 - gx_gp_monitor - [32mINFO[0m - [32m采购公告 爬取完成,共获取 100 条公告,耗时 0.48秒[0m
|
|
||||||
2026-01-07 17:26:34 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 结果公告[0m
|
|
||||||
2026-01-07 17:26:35 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:35 - gx_gp_monitor - [32mINFO[0m - [32m结果公告 爬取完成,共获取 100 条公告,耗时 0.21秒[0m
|
|
||||||
2026-01-07 17:26:35 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 合同公告[0m
|
|
||||||
2026-01-07 17:26:35 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:35 - gx_gp_monitor - [32mINFO[0m - [32m合同公告 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-07 17:26:35 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 更正公告[0m
|
|
||||||
2026-01-07 17:26:35 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:35 - gx_gp_monitor - [32mINFO[0m - [32m更正公告 爬取完成,共获取 100 条公告,耗时 0.23秒[0m
|
|
||||||
2026-01-07 17:26:35 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 招标文件预公示[0m
|
|
||||||
2026-01-07 17:26:35 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:35 - gx_gp_monitor - [32mINFO[0m - [32m招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.20秒[0m
|
|
||||||
2026-01-07 17:26:35 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 单一来源公示[0m
|
|
||||||
2026-01-07 17:26:35 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:35 - gx_gp_monitor - [32mINFO[0m - [32m单一来源公示 爬取完成,共获取 100 条公告,耗时 0.17秒[0m
|
|
||||||
2026-01-07 17:26:35 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 电子卖场公示[0m
|
|
||||||
2026-01-07 17:26:36 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:36 - gx_gp_monitor - [32mINFO[0m - [32m电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.40秒[0m
|
|
||||||
2026-01-07 17:26:36 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 履约验收公示[0m
|
|
||||||
2026-01-07 17:26:36 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:36 - gx_gp_monitor - [32mINFO[0m - [32m履约验收公示 爬取完成,共获取 100 条公告,耗时 0.22秒[0m
|
|
||||||
2026-01-07 17:26:36 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 工程类公告[0m
|
|
||||||
2026-01-07 17:26:36 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:36 - gx_gp_monitor - [32mINFO[0m - [32m工程类公告 爬取完成,共获取 100 条公告,耗时 0.27秒[0m
|
|
||||||
2026-01-07 17:26:36 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议征集公告[0m
|
|
||||||
2026-01-07 17:26:36 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:36 - gx_gp_monitor - [32mINFO[0m - [32m框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-07 17:26:36 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议入围结果公告[0m
|
|
||||||
2026-01-07 17:26:37 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:37 - gx_gp_monitor - [32mINFO[0m - [32m框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.25秒[0m
|
|
||||||
2026-01-07 17:26:37 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议成交结果汇总公告[0m
|
|
||||||
2026-01-07 17:26:37 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:37 - gx_gp_monitor - [32mINFO[0m - [32m框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-07 17:26:37 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购意向公开[0m
|
|
||||||
2026-01-07 17:26:37 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:37 - gx_gp_monitor - [32mINFO[0m - [32m采购意向公开 爬取完成,共获取 100 条公告,耗时 0.17秒[0m
|
|
||||||
2026-01-07 17:26:37 - crawler.spider - [32mINFO[0m - [32m爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告[0m
|
|
||||||
2026-01-07 17:26:37 - __main__ - [32mINFO[0m - [32m爬取到 1300 条原始公告[0m
|
|
||||||
2026-01-07 17:26:37 - __main__ - [31mERROR[0m - [31m爬取任务执行失败: attempted relative import beyond top-level package[0m
|
|
||||||
2026-01-07 17:26:37 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: daily_crawl (0 8,14,18 * * *)[0m
|
|
||||||
2026-01-07 17:26:37 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: data_cleanup (0 2 * * *)[0m
|
|
||||||
2026-01-07 17:26:57 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:26:57 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:26:57 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:26:58 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:26:58 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:26:58 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:26:58 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:26:58 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:26:58 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:26:58 - __main__ - [32mINFO[0m - [32m开始执行爬取任务[0m
|
|
||||||
2026-01-07 17:26:58 - crawler.spider - [32mINFO[0m - [32m开始爬取 13 个公告来源[0m
|
|
||||||
2026-01-07 17:26:58 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购公告[0m
|
|
||||||
2026-01-07 17:26:58 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:58 - gx_gp_monitor - [32mINFO[0m - [32m采购公告 爬取完成,共获取 100 条公告,耗时 0.37秒[0m
|
|
||||||
2026-01-07 17:26:58 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 结果公告[0m
|
|
||||||
2026-01-07 17:26:58 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:58 - gx_gp_monitor - [32mINFO[0m - [32m结果公告 爬取完成,共获取 100 条公告,耗时 0.23秒[0m
|
|
||||||
2026-01-07 17:26:58 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 合同公告[0m
|
|
||||||
2026-01-07 17:26:58 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:58 - gx_gp_monitor - [32mINFO[0m - [32m合同公告 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-07 17:26:58 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 更正公告[0m
|
|
||||||
2026-01-07 17:26:59 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:59 - gx_gp_monitor - [32mINFO[0m - [32m更正公告 爬取完成,共获取 100 条公告,耗时 0.20秒[0m
|
|
||||||
2026-01-07 17:26:59 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 招标文件预公示[0m
|
|
||||||
2026-01-07 17:26:59 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:59 - gx_gp_monitor - [32mINFO[0m - [32m招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.21秒[0m
|
|
||||||
2026-01-07 17:26:59 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 单一来源公示[0m
|
|
||||||
2026-01-07 17:26:59 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:59 - gx_gp_monitor - [32mINFO[0m - [32m单一来源公示 爬取完成,共获取 100 条公告,耗时 0.16秒[0m
|
|
||||||
2026-01-07 17:26:59 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 电子卖场公示[0m
|
|
||||||
2026-01-07 17:26:59 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:59 - gx_gp_monitor - [32mINFO[0m - [32m电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.38秒[0m
|
|
||||||
2026-01-07 17:26:59 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 履约验收公示[0m
|
|
||||||
2026-01-07 17:26:59 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:26:59 - gx_gp_monitor - [32mINFO[0m - [32m履约验收公示 爬取完成,共获取 100 条公告,耗时 0.18秒[0m
|
|
||||||
2026-01-07 17:26:59 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 工程类公告[0m
|
|
||||||
2026-01-07 17:27:00 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:27:00 - gx_gp_monitor - [32mINFO[0m - [32m工程类公告 爬取完成,共获取 100 条公告,耗时 0.25秒[0m
|
|
||||||
2026-01-07 17:27:00 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议征集公告[0m
|
|
||||||
2026-01-07 17:27:00 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:27:00 - gx_gp_monitor - [32mINFO[0m - [32m框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.21秒[0m
|
|
||||||
2026-01-07 17:27:00 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议入围结果公告[0m
|
|
||||||
2026-01-07 17:27:00 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:27:00 - gx_gp_monitor - [32mINFO[0m - [32m框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.17秒[0m
|
|
||||||
2026-01-07 17:27:00 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议成交结果汇总公告[0m
|
|
||||||
2026-01-07 17:27:00 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:27:00 - gx_gp_monitor - [32mINFO[0m - [32m框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-07 17:27:00 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购意向公开[0m
|
|
||||||
2026-01-07 17:27:01 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:27:01 - gx_gp_monitor - [32mINFO[0m - [32m采购意向公开 爬取完成,共获取 100 条公告,耗时 0.25秒[0m
|
|
||||||
2026-01-07 17:27:01 - crawler.spider - [32mINFO[0m - [32m爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告[0m
|
|
||||||
2026-01-07 17:27:01 - __main__ - [32mINFO[0m - [32m爬取到 1300 条原始公告[0m
|
|
||||||
2026-01-07 17:27:01 - filters.filters - [32mINFO[0m - [32m来源筛选: 1300 -> 1300 条公告[0m
|
|
||||||
2026-01-07 17:27:01 - filters.filters - [32mINFO[0m - [32m关键词筛选: 1300 -> 18 条公告[0m
|
|
||||||
2026-01-07 17:27:01 - filters.filters - [32mINFO[0m - [32m筛选完成: 总数 1300 -> 筛选后 18 (关键词: 1282, 日期: 0, 去重: 0, 来源: 0)[0m
|
|
||||||
2026-01-07 17:27:01 - __main__ - [32mINFO[0m - [32m筛选后剩余 18 条公告[0m
|
|
||||||
2026-01-07 17:27:01 - storage.postgresql - [32mINFO[0m - [32m开始保存 18 条公告到数据库[0m
|
|
||||||
2026-01-07 17:27:01 - core.database - [32mINFO[0m - [32m批量保存公告完成,成功保存 1 条[0m
|
|
||||||
2026-01-07 17:27:01 - storage.postgresql - [32mINFO[0m - [32m成功保存 1 条公告到数据库[0m
|
|
||||||
2026-01-07 17:27:01 - storage.md_generator - [32mINFO[0m - [32mMarkdown文件已保存到: onu.md (共 18 条公告)[0m
|
|
||||||
2026-01-07 17:27:01 - notification.wechat - [32mINFO[0m - [32m企业微信服务初始化完成[0m
|
|
||||||
2026-01-07 17:27:01 - notification.wechat - [32mINFO[0m - [32m成功获取企业微信访问令牌[0m
|
|
||||||
2026-01-07 17:27:01 - notification.wechat - [32mINFO[0m - [32mMarkdown消息发送成功[0m
|
|
||||||
2026-01-07 17:27:01 - __main__ - [32mINFO[0m - [32m爬取任务完成: {'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}}[0m
|
|
||||||
2026-01-07 17:27:01 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: daily_crawl (0 8,14,18 * * *)[0m
|
|
||||||
2026-01-07 17:27:01 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: data_cleanup (0 2 * * *)[0m
|
|
||||||
2026-01-07 17:29:05 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:29:05 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:29:05 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:29:06 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:29:06 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:29:06 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:29:06 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:29:06 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:29:06 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:29:06 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: daily_crawl (0 8,14,18 * * *)[0m
|
|
||||||
2026-01-07 17:29:06 - scheduler.scheduler - [32mINFO[0m - [32m添加定时任务: data_cleanup (0 2 * * *)[0m
|
|
||||||
2026-01-07 17:31:58 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:31:58 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:31:58 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:31:58 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:31:58 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:31:58 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:31:58 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:31:58 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:31:58 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:34:32 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:34:32 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:34:32 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:34:32 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:34:32 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:34:32 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:34:32 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:34:32 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:34:32 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:34:35 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:34:35 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:34:35 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:34:35 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:34:35 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:34:35 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:34:35 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:34:35 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:34:35 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:34:35 - __main__ - [32mINFO[0m - [32m开始执行爬取任务[0m
|
|
||||||
2026-01-07 17:34:35 - crawler.spider - [32mINFO[0m - [32m开始爬取 13 个公告来源[0m
|
|
||||||
2026-01-07 17:34:35 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购公告[0m
|
|
||||||
2026-01-07 17:35:05 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:05 - gx_gp_monitor - [32mINFO[0m - [32m采购公告 爬取完成,共获取 100 条公告,耗时 30.44秒[0m
|
|
||||||
2026-01-07 17:35:05 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 结果公告[0m
|
|
||||||
2026-01-07 17:35:06 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:06 - gx_gp_monitor - [32mINFO[0m - [32m结果公告 爬取完成,共获取 100 条公告,耗时 0.26秒[0m
|
|
||||||
2026-01-07 17:35:06 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 合同公告[0m
|
|
||||||
2026-01-07 17:35:06 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:06 - gx_gp_monitor - [32mINFO[0m - [32m合同公告 爬取完成,共获取 100 条公告,耗时 0.20秒[0m
|
|
||||||
2026-01-07 17:35:06 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 更正公告[0m
|
|
||||||
2026-01-07 17:35:06 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:06 - gx_gp_monitor - [32mINFO[0m - [32m更正公告 爬取完成,共获取 100 条公告,耗时 0.23秒[0m
|
|
||||||
2026-01-07 17:35:06 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 招标文件预公示[0m
|
|
||||||
2026-01-07 17:35:06 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:06 - gx_gp_monitor - [32mINFO[0m - [32m招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.18秒[0m
|
|
||||||
2026-01-07 17:35:06 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 单一来源公示[0m
|
|
||||||
2026-01-07 17:35:07 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:07 - gx_gp_monitor - [32mINFO[0m - [32m单一来源公示 爬取完成,共获取 100 条公告,耗时 0.18秒[0m
|
|
||||||
2026-01-07 17:35:07 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 电子卖场公示[0m
|
|
||||||
2026-01-07 17:35:07 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:07 - gx_gp_monitor - [32mINFO[0m - [32m电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.34秒[0m
|
|
||||||
2026-01-07 17:35:07 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 履约验收公示[0m
|
|
||||||
2026-01-07 17:35:07 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:07 - gx_gp_monitor - [32mINFO[0m - [32m履约验收公示 爬取完成,共获取 100 条公告,耗时 0.21秒[0m
|
|
||||||
2026-01-07 17:35:07 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 工程类公告[0m
|
|
||||||
2026-01-07 17:35:07 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:07 - gx_gp_monitor - [32mINFO[0m - [32m工程类公告 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-07 17:35:07 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议征集公告[0m
|
|
||||||
2026-01-07 17:35:07 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:07 - gx_gp_monitor - [32mINFO[0m - [32m框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-07 17:35:07 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议入围结果公告[0m
|
|
||||||
2026-01-07 17:35:08 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:08 - gx_gp_monitor - [32mINFO[0m - [32m框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.18秒[0m
|
|
||||||
2026-01-07 17:35:08 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议成交结果汇总公告[0m
|
|
||||||
2026-01-07 17:35:08 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:08 - gx_gp_monitor - [32mINFO[0m - [32m框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.25秒[0m
|
|
||||||
2026-01-07 17:35:08 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购意向公开[0m
|
|
||||||
2026-01-07 17:35:08 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:35:08 - gx_gp_monitor - [32mINFO[0m - [32m采购意向公开 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-07 17:35:08 - crawler.spider - [32mINFO[0m - [32m爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告[0m
|
|
||||||
2026-01-07 17:35:08 - __main__ - [32mINFO[0m - [32m爬取到 1300 条原始公告[0m
|
|
||||||
2026-01-07 17:35:08 - filters.filters - [32mINFO[0m - [32m来源筛选: 1300 -> 1300 条公告[0m
|
|
||||||
2026-01-07 17:35:08 - filters.filters - [32mINFO[0m - [32m去重筛选: 移除了 18 条重复公告[0m
|
|
||||||
2026-01-07 17:35:08 - filters.filters - [32mINFO[0m - [32m关键词筛选: 1282 -> 0 条公告[0m
|
|
||||||
2026-01-07 17:35:08 - filters.filters - [32mINFO[0m - [32m筛选完成: 总数 1300 -> 筛选后 0 (关键词: 1282, 日期: 0, 去重: 18, 来源: 0)[0m
|
|
||||||
2026-01-07 17:35:08 - __main__ - [32mINFO[0m - [32m筛选后剩余 0 条公告[0m
|
|
||||||
2026-01-07 17:35:08 - storage.md_generator - [32mINFO[0m - [32mMarkdown文件已保存到: onu.md (共 0 条公告)[0m
|
|
||||||
2026-01-07 17:35:08 - __main__ - [32mINFO[0m - [32m爬取任务完成: {'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}}[0m
|
|
||||||
2026-01-07 17:35:38 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-07 17:35:38 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-07 17:35:38 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-07 17:35:38 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-07 17:35:38 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-07 17:35:38 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-07 17:35:38 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-07 17:35:38 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-07 17:35:38 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-07 17:35:38 - __main__ - [32mINFO[0m - [32m开始执行爬取任务[0m
|
|
||||||
2026-01-07 17:35:38 - crawler.spider - [32mINFO[0m - [32m开始爬取 13 个公告来源[0m
|
|
||||||
2026-01-07 17:35:38 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购公告[0m
|
|
||||||
2026-01-07 17:36:09 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:09 - gx_gp_monitor - [32mINFO[0m - [32m采购公告 爬取完成,共获取 100 条公告,耗时 30.49秒[0m
|
|
||||||
2026-01-07 17:36:09 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 结果公告[0m
|
|
||||||
2026-01-07 17:36:09 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:09 - gx_gp_monitor - [32mINFO[0m - [32m结果公告 爬取完成,共获取 100 条公告,耗时 0.20秒[0m
|
|
||||||
2026-01-07 17:36:09 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 合同公告[0m
|
|
||||||
2026-01-07 17:36:09 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:09 - gx_gp_monitor - [32mINFO[0m - [32m合同公告 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-07 17:36:09 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 更正公告[0m
|
|
||||||
2026-01-07 17:36:09 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:09 - gx_gp_monitor - [32mINFO[0m - [32m更正公告 爬取完成,共获取 100 条公告,耗时 0.27秒[0m
|
|
||||||
2026-01-07 17:36:09 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 招标文件预公示[0m
|
|
||||||
2026-01-07 17:36:10 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:10 - gx_gp_monitor - [32mINFO[0m - [32m招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.28秒[0m
|
|
||||||
2026-01-07 17:36:10 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 单一来源公示[0m
|
|
||||||
2026-01-07 17:36:10 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:10 - gx_gp_monitor - [32mINFO[0m - [32m单一来源公示 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-07 17:36:10 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 电子卖场公示[0m
|
|
||||||
2026-01-07 17:36:10 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:10 - gx_gp_monitor - [32mINFO[0m - [32m电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.36秒[0m
|
|
||||||
2026-01-07 17:36:10 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 履约验收公示[0m
|
|
||||||
2026-01-07 17:36:11 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:11 - gx_gp_monitor - [32mINFO[0m - [32m履约验收公示 爬取完成,共获取 100 条公告,耗时 0.27秒[0m
|
|
||||||
2026-01-07 17:36:11 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 工程类公告[0m
|
|
||||||
2026-01-07 17:36:11 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:11 - gx_gp_monitor - [32mINFO[0m - [32m工程类公告 爬取完成,共获取 100 条公告,耗时 0.21秒[0m
|
|
||||||
2026-01-07 17:36:11 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议征集公告[0m
|
|
||||||
2026-01-07 17:36:11 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:11 - gx_gp_monitor - [32mINFO[0m - [32m框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.15秒[0m
|
|
||||||
2026-01-07 17:36:11 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议入围结果公告[0m
|
|
||||||
2026-01-07 17:36:11 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:11 - gx_gp_monitor - [32mINFO[0m - [32m框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.24秒[0m
|
|
||||||
2026-01-07 17:36:11 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议成交结果汇总公告[0m
|
|
||||||
2026-01-07 17:36:11 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:11 - gx_gp_monitor - [32mINFO[0m - [32m框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.16秒[0m
|
|
||||||
2026-01-07 17:36:11 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购意向公开[0m
|
|
||||||
2026-01-07 17:36:11 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-07 17:36:11 - gx_gp_monitor - [32mINFO[0m - [32m采购意向公开 爬取完成,共获取 100 条公告,耗时 0.18秒[0m
|
|
||||||
2026-01-07 17:36:11 - crawler.spider - [32mINFO[0m - [32m爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告[0m
|
|
||||||
2026-01-07 17:36:11 - __main__ - [32mINFO[0m - [32m爬取到 1300 条原始公告[0m
|
|
||||||
2026-01-07 17:36:11 - filters.filters - [32mINFO[0m - [32m来源筛选: 1300 -> 1300 条公告[0m
|
|
||||||
2026-01-07 17:36:12 - filters.filters - [32mINFO[0m - [32m去重筛选: 移除了 18 条重复公告[0m
|
|
||||||
2026-01-07 17:36:12 - filters.filters - [32mINFO[0m - [32m关键词筛选: 1282 -> 0 条公告[0m
|
|
||||||
2026-01-07 17:36:12 - filters.filters - [32mINFO[0m - [32m筛选完成: 总数 1300 -> 筛选后 0 (关键词: 1282, 日期: 0, 去重: 18, 来源: 0)[0m
|
|
||||||
2026-01-07 17:36:12 - __main__ - [32mINFO[0m - [32m筛选后剩余 0 条公告[0m
|
|
||||||
2026-01-07 17:36:12 - storage.md_generator - [32mINFO[0m - [32mMarkdown文件已保存到: onu.md (共 0 条公告)[0m
|
|
||||||
2026-01-07 17:36:12 - __main__ - [32mINFO[0m - [32m爬取任务完成: {'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}}[0m
|
|
||||||
2026-01-08 08:17:30 - __main__ - [32mINFO[0m - [32m=== 广西政府采购网公告监控系统启动 ===[0m
|
|
||||||
2026-01-08 08:17:30 - __main__ - [32mINFO[0m - [32m版本: 1.0.0[0m
|
|
||||||
2026-01-08 08:17:30 - __main__ - [32mINFO[0m - [32m配置文件: 默认配置[0m
|
|
||||||
2026-01-08 08:17:30 - core.reliability - [32mINFO[0m - [32m健康检查通过[0m
|
|
||||||
2026-01-08 08:17:30 - core.database - [32mINFO[0m - [32m数据库连接池初始化成功[0m
|
|
||||||
2026-01-08 08:17:30 - core.database - [32mINFO[0m - [32m开始初始化数据库表结构[0m
|
|
||||||
2026-01-08 08:17:30 - core.database - [32mINFO[0m - [32m数据库表结构初始化完成[0m
|
|
||||||
2026-01-08 08:17:30 - storage.postgresql - [32mINFO[0m - [32m存储初始化完成[0m
|
|
||||||
2026-01-08 08:17:30 - __main__ - [32mINFO[0m - [32m应用初始化完成[0m
|
|
||||||
2026-01-08 08:17:30 - __main__ - [32mINFO[0m - [32m开始执行爬取任务[0m
|
|
||||||
2026-01-08 08:17:30 - crawler.spider - [32mINFO[0m - [32m开始爬取 13 个公告来源[0m
|
|
||||||
2026-01-08 08:17:30 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购公告[0m
|
|
||||||
2026-01-08 08:17:31 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:31 - gx_gp_monitor - [32mINFO[0m - [32m采购公告 爬取完成,共获取 100 条公告,耗时 0.53秒[0m
|
|
||||||
2026-01-08 08:17:31 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 结果公告[0m
|
|
||||||
2026-01-08 08:17:31 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:31 - gx_gp_monitor - [32mINFO[0m - [32m结果公告 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-08 08:17:31 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 合同公告[0m
|
|
||||||
2026-01-08 08:17:31 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:31 - gx_gp_monitor - [32mINFO[0m - [32m合同公告 爬取完成,共获取 100 条公告,耗时 0.20秒[0m
|
|
||||||
2026-01-08 08:17:31 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 更正公告[0m
|
|
||||||
2026-01-08 08:17:31 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:31 - gx_gp_monitor - [32mINFO[0m - [32m更正公告 爬取完成,共获取 100 条公告,耗时 0.28秒[0m
|
|
||||||
2026-01-08 08:17:31 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 招标文件预公示[0m
|
|
||||||
2026-01-08 08:17:31 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:31 - gx_gp_monitor - [32mINFO[0m - [32m招标文件预公示 爬取完成,共获取 100 条公告,耗时 0.18秒[0m
|
|
||||||
2026-01-08 08:17:31 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 单一来源公示[0m
|
|
||||||
2026-01-08 08:17:32 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:32 - gx_gp_monitor - [32mINFO[0m - [32m单一来源公示 爬取完成,共获取 100 条公告,耗时 0.16秒[0m
|
|
||||||
2026-01-08 08:17:32 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 电子卖场公示[0m
|
|
||||||
2026-01-08 08:17:32 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:32 - gx_gp_monitor - [32mINFO[0m - [32m电子卖场公示 爬取完成,共获取 100 条公告,耗时 0.34秒[0m
|
|
||||||
2026-01-08 08:17:32 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 履约验收公示[0m
|
|
||||||
2026-01-08 08:17:32 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:32 - gx_gp_monitor - [32mINFO[0m - [32m履约验收公示 爬取完成,共获取 100 条公告,耗时 0.48秒[0m
|
|
||||||
2026-01-08 08:17:32 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 工程类公告[0m
|
|
||||||
2026-01-08 08:17:33 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:33 - gx_gp_monitor - [32mINFO[0m - [32m工程类公告 爬取完成,共获取 100 条公告,耗时 0.23秒[0m
|
|
||||||
2026-01-08 08:17:33 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议征集公告[0m
|
|
||||||
2026-01-08 08:17:33 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:33 - gx_gp_monitor - [32mINFO[0m - [32m框架协议征集公告 爬取完成,共获取 100 条公告,耗时 0.18秒[0m
|
|
||||||
2026-01-08 08:17:33 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议入围结果公告[0m
|
|
||||||
2026-01-08 08:17:33 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:33 - gx_gp_monitor - [32mINFO[0m - [32m框架协议入围结果公告 爬取完成,共获取 100 条公告,耗时 0.23秒[0m
|
|
||||||
2026-01-08 08:17:33 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 框架协议成交结果汇总公告[0m
|
|
||||||
2026-01-08 08:17:33 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:33 - gx_gp_monitor - [32mINFO[0m - [32m框架协议成交结果汇总公告 爬取完成,共获取 100 条公告,耗时 0.16秒[0m
|
|
||||||
2026-01-08 08:17:33 - gx_gp_monitor - [32mINFO[0m - [32m开始爬取 采购意向公开[0m
|
|
||||||
2026-01-08 08:17:33 - crawler.parsers - [32mINFO[0m - [32m成功解析 100/100 条公告记录[0m
|
|
||||||
2026-01-08 08:17:33 - gx_gp_monitor - [32mINFO[0m - [32m采购意向公开 爬取完成,共获取 100 条公告,耗时 0.19秒[0m
|
|
||||||
2026-01-08 08:17:33 - crawler.spider - [32mINFO[0m - [32m爬取完成: 共处理 13 个来源,成功 13 个,失败 0 个,获取 1300 条公告[0m
|
|
||||||
2026-01-08 08:17:33 - __main__ - [32mINFO[0m - [32m爬取到 1300 条原始公告[0m
|
|
||||||
2026-01-08 08:17:33 - filters.filters - [32mINFO[0m - [32m来源筛选: 1300 -> 1300 条公告[0m
|
|
||||||
2026-01-08 08:17:34 - filters.filters - [32mINFO[0m - [32m去重筛选: 移除了 17 条重复公告[0m
|
|
||||||
2026-01-08 08:17:34 - filters.filters - [32mINFO[0m - [32m关键词筛选: 1283 -> 0 条公告[0m
|
|
||||||
2026-01-08 08:17:34 - filters.filters - [32mINFO[0m - [32m筛选完成: 总数 1300 -> 筛选后 0 (关键词: 1283, 日期: 0, 去重: 17, 来源: 0)[0m
|
|
||||||
2026-01-08 08:17:34 - __main__ - [32mINFO[0m - [32m筛选后剩余 0 条公告[0m
|
|
||||||
2026-01-08 08:17:34 - storage.md_generator - [32mINFO[0m - [32mMarkdown文件已保存到: onu.md (共 0 条公告)[0m
|
|
||||||
2026-01-08 08:17:34 - __main__ - [32mINFO[0m - [32m爬取任务完成: {'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}}[0m
|
|
||||||
+83
-11
@@ -8,6 +8,7 @@ import sys
|
|||||||
import argparse
|
import argparse
|
||||||
import signal
|
import signal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
# 添加项目根目录到Python路径
|
# 添加项目根目录到Python路径
|
||||||
project_root = Path(__file__).parent
|
project_root = Path(__file__).parent
|
||||||
@@ -20,7 +21,7 @@ try:
|
|||||||
from .core.reliability import check_system_health
|
from .core.reliability import check_system_health
|
||||||
from .crawler.spider import crawl_announcements
|
from .crawler.spider import crawl_announcements
|
||||||
from .filters.filters import filter_from_config
|
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 .storage.md_generator import generate_onu_md
|
||||||
from .notification.wechat import send_announcements_notification, send_system_notification
|
from .notification.wechat import send_announcements_notification, send_system_notification
|
||||||
from .wechat.callback_server import get_callback_server
|
from .wechat.callback_server import get_callback_server
|
||||||
@@ -88,16 +89,23 @@ class GXGPMonitorApp:
|
|||||||
print(f"应用初始化失败: {str(e)}", file=sys.stderr)
|
print(f"应用初始化失败: {str(e)}", file=sys.stderr)
|
||||||
return False
|
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:
|
try:
|
||||||
logger.info("开始执行爬取任务")
|
logger.info(f"开始执行搜索任务 (手动搜索: {manual_crawl})")
|
||||||
|
|
||||||
# 执行爬取
|
# 执行爬取
|
||||||
crawl_results = crawl_announcements(keywords, sources)
|
crawl_results = crawl_announcements(keywords, sources)
|
||||||
|
|
||||||
if not crawl_results:
|
if not crawl_results:
|
||||||
logger.info("爬取完成:无数据")
|
logger.info("搜索完成:无数据")
|
||||||
return {"success": True, "results": []}
|
return {"success": True, "results": []}
|
||||||
|
|
||||||
# 收集所有公告
|
# 收集所有公告
|
||||||
@@ -106,29 +114,89 @@ class GXGPMonitorApp:
|
|||||||
if result.announcements:
|
if result.announcements:
|
||||||
all_announcements.extend(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_stats = save_all_announcements_by_source_to_storage(all_announcements, max_per_source=100)
|
||||||
all_saved_count = sum(all_saved_stats.values())
|
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()
|
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)} 条公告")
|
logger.info(f"筛选后剩余 {len(filtered_announcements)} 条公告")
|
||||||
|
|
||||||
|
# 对于手动爬取,不保存筛选后的公告到数据库
|
||||||
|
if not manual_crawl:
|
||||||
# 保存筛选后的公告(用于标记关键词匹配等)
|
# 保存筛选后的公告(用于标记关键词匹配等)
|
||||||
saved_count = save_announcements_to_storage(filtered_announcements)
|
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)
|
md_success = generate_onu_md(filtered_announcements)
|
||||||
|
|
||||||
# 发送通知
|
# 发送通知
|
||||||
notify_success = False
|
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)
|
notify_success = send_announcements_notification(filtered_announcements)
|
||||||
|
elif manual_crawl and filtered_announcements and self.config.wechat_app.enabled:
|
||||||
|
# 手动爬取时不在这里发送消息,由消息处理器负责发送markdown消息
|
||||||
|
notify_success = True # 标记为成功,因为消息会通过其他方式发送
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -145,6 +213,10 @@ class GXGPMonitorApp:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 对于手动爬取,额外返回筛选后的公告列表
|
||||||
|
if manual_crawl:
|
||||||
|
result["filtered_announcements"] = filtered_announcements
|
||||||
|
|
||||||
logger.info(f"爬取任务完成: {result}")
|
logger.info(f"爬取任务完成: {result}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -383,14 +383,21 @@ class WeChatService:
|
|||||||
if len(source_name) > 25: # 限制来源名称长度
|
if len(source_name) > 25: # 限制来源名称长度
|
||||||
source_name = source_name[:22] + "..."
|
source_name = source_name[:22] + "..."
|
||||||
|
|
||||||
|
# 根据来源代码添加前缀标识
|
||||||
|
source_prefix = ""
|
||||||
|
if announcement.source_code == 'dahuagov':
|
||||||
|
source_prefix = "【大化县政府网】"
|
||||||
|
else:
|
||||||
|
source_prefix = "【广西政府采购网】"
|
||||||
|
|
||||||
# 时间格式化
|
# 时间格式化
|
||||||
if announcement.publish_date:
|
if announcement.publish_date:
|
||||||
time_str = announcement.publish_date.strftime("%Y-%m-%d %H:%M")
|
time_str = announcement.publish_date.strftime("%Y-%m-%d %H:%M")
|
||||||
else:
|
else:
|
||||||
time_str = "时间未知"
|
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:公告详情链接
|
||||||
url = announcement.content_url
|
url = announcement.content_url
|
||||||
@@ -434,7 +441,18 @@ class WeChatService:
|
|||||||
|
|
||||||
# 标题和概要
|
# 标题和概要
|
||||||
total_count = len(announcements)
|
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("")
|
lines.append("")
|
||||||
lines.append(f"📊 **共发现 {total_count} 条新公告**")
|
lines.append(f"📊 **共发现 {total_count} 条新公告**")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -541,6 +559,16 @@ class WeChatService:
|
|||||||
|
|
||||||
# 生成标题
|
# 生成标题
|
||||||
total_count = len(announcements)
|
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}条)"
|
title = f"🔔 广西政府采购网公告更新 ({total_count}条)"
|
||||||
|
|
||||||
# 生成描述HTML
|
# 生成描述HTML
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# 广西政府采购网公告监控
|
|
||||||
|
|
||||||
**更新时间**: 2026-01-08 08:17:34
|
|
||||||
|
|
||||||
## 无新公告
|
|
||||||
|
|
||||||
当前时间范围内没有找到符合条件的公告。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*由广西政府采购网公告监控系统生成*
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"""调度模块"""
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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)
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -42,7 +42,8 @@ class MarkdownGenerator:
|
|||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
def generate_markdown(self, announcements: List[Announcement],
|
def generate_markdown(self, announcements: List[Announcement],
|
||||||
title: str = "广西政府采购网公告监控") -> str:
|
title: str = "广西政府采购网公告监控",
|
||||||
|
time_period: str = None) -> str:
|
||||||
"""
|
"""
|
||||||
生成Markdown内容
|
生成Markdown内容
|
||||||
|
|
||||||
@@ -54,22 +55,31 @@ class MarkdownGenerator:
|
|||||||
str: Markdown格式的文本
|
str: Markdown格式的文本
|
||||||
"""
|
"""
|
||||||
if not announcements:
|
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)
|
grouped_announcements = self._group_announcements_by_source(announcements)
|
||||||
|
|
||||||
# 生成Markdown
|
# 生成Markdown
|
||||||
lines = []
|
lines = []
|
||||||
lines.append(f"# {title}")
|
lines.append("# 搜索完成")
|
||||||
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))
|
keyword = "未知"
|
||||||
lines.append("---")
|
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("")
|
lines.append("")
|
||||||
|
|
||||||
# 生成各来源的公告
|
# 生成各来源的公告
|
||||||
@@ -107,11 +117,8 @@ class MarkdownGenerator:
|
|||||||
def _generate_source_section(self, source_name: str, announcements: List[Announcement]) -> List[str]:
|
def _generate_source_section(self, source_name: str, announcements: List[Announcement]) -> List[str]:
|
||||||
"""生成来源章节"""
|
"""生成来源章节"""
|
||||||
lines = []
|
lines = []
|
||||||
anchor = self._create_anchor(source_name)
|
|
||||||
|
|
||||||
lines.append(f"## {source_name}")
|
lines.append(f"## {source_name} - **共 {len(announcements)} 条**")
|
||||||
lines.append("")
|
|
||||||
lines.append(f"**共 {len(announcements)} 条公告**")
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# 生成公告列表
|
# 生成公告列表
|
||||||
@@ -124,59 +131,49 @@ class MarkdownGenerator:
|
|||||||
"""生成单个公告项"""
|
"""生成单个公告项"""
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
# 公告标题
|
# 公告标题(包含超链接)
|
||||||
title_prefix = ""
|
title_line = f"### {index}. [{announcement.title}]({announcement.content_url})"
|
||||||
if self.include_today_highlight and announcement.is_today:
|
|
||||||
title_prefix = "🆕 **【今日】**"
|
|
||||||
|
|
||||||
title_line = f"### {index}. {title_prefix}[{announcement.title}]({announcement.content_url})"
|
|
||||||
lines.append(title_line)
|
lines.append(title_line)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# 公告信息
|
# 公告信息 - 简洁格式
|
||||||
info_items = []
|
info_parts = []
|
||||||
|
|
||||||
if announcement.publish_date:
|
if announcement.publish_date:
|
||||||
publish_date = announcement.publish_date.strftime("%Y-%m-%d")
|
publish_date = announcement.publish_date.strftime("%Y-%m-%d")
|
||||||
info_items.append(f"📅 发布时间: {publish_date}")
|
info_parts.append(publish_date)
|
||||||
|
|
||||||
if announcement.purchase_name:
|
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:
|
if info_parts:
|
||||||
crawled_time = announcement.crawled_at.strftime("%m-%d %H:%M")
|
info_line = " | ".join(info_parts)
|
||||||
info_items.append(f"🤖 爬取时间: {crawled_time}")
|
lines.append(info_line)
|
||||||
|
|
||||||
if info_items:
|
|
||||||
lines.append(" | ".join(info_items))
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# 如果是新公告,添加标记
|
|
||||||
if announcement.is_new:
|
|
||||||
lines.append("*🚀 新公告*")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("---")
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def _generate_empty_markdown(self, title: str) -> str:
|
def _generate_empty_markdown(self, title: str, time_period: str = None) -> str:
|
||||||
"""生成空内容的Markdown"""
|
"""生成空内容的Markdown"""
|
||||||
|
# 解析标题中的关键词
|
||||||
|
keyword = "未知"
|
||||||
|
if "关键词:" in title:
|
||||||
|
keyword_part = title.split("关键词:")[-1].strip()
|
||||||
|
keyword = keyword_part.split()[0] if keyword_part else "未知"
|
||||||
|
|
||||||
lines = [
|
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)
|
return "\n".join(lines)
|
||||||
|
|||||||
@@ -62,6 +62,95 @@ class PostgreSQLStorage:
|
|||||||
# 尝试逐个保存
|
# 尝试逐个保存
|
||||||
return self._save_announcements_fallback(announcements)
|
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],
|
def save_all_announcements_by_source(self, announcements: List[Announcement],
|
||||||
max_per_source: int = 100) -> Dict[str, int]:
|
max_per_source: int = 100) -> Dict[str, int]:
|
||||||
"""
|
"""
|
||||||
@@ -186,6 +275,41 @@ class PostgreSQLStorage:
|
|||||||
# 由于我们在爬取时已经标记,这里主要是确保数据库中的标记正确
|
# 由于我们在爬取时已经标记,这里主要是确保数据库中的标记正确
|
||||||
pass
|
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:
|
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)
|
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:
|
def save_crawl_results(self, results: List[CrawlResult]) -> int:
|
||||||
"""保存爬取结果"""
|
"""保存爬取结果"""
|
||||||
return self._current_storage.save_crawl_results(results)
|
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)
|
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:
|
def cleanup_storage(days: Optional[int] = None) -> int:
|
||||||
"""
|
"""
|
||||||
清理存储中的过期数据
|
清理存储中的过期数据
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -261,5 +261,15 @@ def get_callback_server() -> WeChatCallbackServer:
|
|||||||
|
|
||||||
def create_callback_app() -> Flask:
|
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()
|
server = get_callback_server()
|
||||||
return server.app
|
return server.app
|
||||||
|
|||||||
@@ -34,22 +34,47 @@ class WeChatMenuManager:
|
|||||||
self.menu_data = {
|
self.menu_data = {
|
||||||
"button": [
|
"button": [
|
||||||
{
|
{
|
||||||
"name": "操作",
|
"name": "监控操作",
|
||||||
"sub_button": [
|
"sub_button": [
|
||||||
{
|
{
|
||||||
"type": "click",
|
"type": "click",
|
||||||
"name": "立即爬取",
|
"name": "立即搜索",
|
||||||
"key": "crawl_now"
|
"key": "crawl_now"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "click",
|
"type": "click",
|
||||||
"name": "今日总结",
|
"name": "今日统计",
|
||||||
"key": "today_summary"
|
"key": "today_stats"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "click",
|
"type": "click",
|
||||||
"name": "自定义爬取",
|
"name": "关键词搜索",
|
||||||
"key": "custom_crawl"
|
"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",
|
"type": "click",
|
||||||
"name": "使用说明",
|
"name": "使用说明",
|
||||||
"key": "help_guide"
|
"key": "help_guide"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "click",
|
|
||||||
"name": "联系我们",
|
|
||||||
"key": "contact_us"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1055
-214
File diff suppressed because it is too large
Load Diff
+6366
-530
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
@@ -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 - 开始爬取 单一来源公示
|
||||||
@@ -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
+107029
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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"
|
|
||||||
@@ -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()
|
|
||||||
@@ -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,"采购意向公开"]# 采购意向公开
|
|
||||||
}
|
|
||||||
@@ -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("公告栏目分割线————————————————————————————————————————————————公告栏目分割线")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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文件中")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
# 广西政府采购网公告监控
|
📋 关键词搜索: `未知` - 总公告数: `0`
|
||||||
|
|
||||||
**更新时间**: 2026-01-08 09:29:42
|
**更新时间: 2026-05-06 15:08:57**
|
||||||
|
|
||||||
## 无新公告
|
|
||||||
|
|
||||||
当前时间范围内没有找到符合条件的公告。
|
## 无匹配公告
|
||||||
|
|
||||||
---
|
在指定时间范围内没有找到符合条件的公告。
|
||||||
|
|
||||||
*由广西政府采购网公告监控系统生成*
|
|
||||||
|
|||||||
Executable
+113
@@ -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
|
||||||
@@ -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 "=== 脚本执行完成 ==="
|
|
||||||
@@ -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)
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
984557
|
||||||
@@ -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
|
||||||
@@ -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()
|
|
||||||
@@ -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第三方库,请开发者自行安装此库再使用。
|
|
||||||
@@ -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×tamp=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×tamp=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:成功0,sEncryptMsg,失败返回对应的错误码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×tamp=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×tamp=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:成功0,sEncryptMsg,失败返回对应的错误码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×tamp=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×tamp=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:成功0,sEncryptMsg,失败返回对应的错误码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×tamp=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×tamp=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:成功0,sEncryptMsg,失败返回对应的错误码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
|
|
||||||
@@ -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
@@ -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×tamp=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请求示例:
|
|
||||||
注意这里的 tousername,encrypt,agentid均为小写
|
|
||||||
|
|
||||||
POST /cgi-bin/wxpush?msg_signature=477715d11cdb4164915debcba66cb864d751f3e6×tamp=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
@@ -1,77 +0,0 @@
|
|||||||
发送应用消息
|
|
||||||
最后更新:2025/09/24
|
|
||||||
接口定义
|
|
||||||
应用支持推送文本、图片、视频、文件、图文等类型。
|
|
||||||
|
|
||||||
请求方式:POST(HTTPS)
|
|
||||||
请求地址: 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属性即可,具体用法请参考上面的示例。
|
|
||||||
@@ -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> > <a href="../../../../" target="_self" title="政府信息公开" class="CurrChnlCls">政府信息公开</a> > <a href="../../../" target="_self" title="重点领域信息 公开" class="CurrChnlCls">重点领域信息公开</a> > <a href="../../" target="_self" title="公共资源配置领域" class="CurrChnlCls">公共资源配置领域</a> > <a href="../" target="_self" title="政府采购领域" class="CurrChnlCls">政府采购领域</a> > <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>
|
||||||
|
|
||||||
|
</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&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号 邮编: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>
|
||||||
Reference in New Issue
Block a user