mirror of
https://devops.liangqichi.top/qichi.liang/Orbitin.git
synced 2026-02-10 07:41:29 +08:00
n1
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OrbitIn - Confluence 日志抓取与处理工具包
|
||||
"""
|
||||
from .confluence import ConfluenceClient, HTMLTextExtractor, HandoverLogParser
|
||||
from .database import DailyLogsDatabase
|
||||
|
||||
__version__ = '1.0.0'
|
||||
__all__ = ['ConfluenceClient', 'HTMLTextExtractor', 'HandoverLogParser', 'DailyLogsDatabase']
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OrbitIn - Confluence 日志抓取与处理工具包
|
||||
"""
|
||||
from .confluence import ConfluenceClient, HTMLTextExtractor, HandoverLogParser
|
||||
from .database import DailyLogsDatabase
|
||||
|
||||
__version__ = '1.0.0'
|
||||
__all__ = ['ConfluenceClient', 'HTMLTextExtractor', 'HandoverLogParser', 'DailyLogsDatabase']
|
||||
|
||||
262
src/config.py
262
src/config.py
@@ -1,132 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
统一配置模块
|
||||
集中管理所有配置项,避免硬编码
|
||||
"""
|
||||
import os
|
||||
from typing import Optional
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Config:
|
||||
"""应用配置类"""
|
||||
|
||||
# Confluence 配置
|
||||
CONFLUENCE_BASE_URL = os.getenv('CONFLUENCE_BASE_URL')
|
||||
CONFLUENCE_TOKEN = os.getenv('CONFLUENCE_TOKEN')
|
||||
CONFLUENCE_CONTENT_ID = os.getenv('CONFLUENCE_CONTENT_ID')
|
||||
|
||||
# 飞书配置
|
||||
FEISHU_BASE_URL = os.getenv('FEISHU_BASE_URL', 'https://open.feishu.cn/open-apis/sheets/v3')
|
||||
FEISHU_TOKEN = os.getenv('FEISHU_TOKEN')
|
||||
FEISHU_SPREADSHEET_TOKEN = os.getenv('FEISHU_SPREADSHEET_TOKEN')
|
||||
FEISHU_APP_ID = os.getenv('FEISHU_APP_ID')
|
||||
FEISHU_APP_SECRET = os.getenv('FEISHU_APP_SECRET')
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_PATH = os.getenv('DATABASE_PATH', 'data/daily_logs.db')
|
||||
SCHEDULE_DATABASE_PATH = os.getenv('SCHEDULE_DATABASE_PATH', 'data/daily_logs.db')
|
||||
|
||||
# 业务配置
|
||||
DAILY_TARGET_TEU = int(os.getenv('DAILY_TARGET_TEU', '300'))
|
||||
DUTY_PHONE = os.getenv('DUTY_PHONE', '13107662315')
|
||||
|
||||
# 缓存配置
|
||||
CACHE_TTL = int(os.getenv('CACHE_TTL', '3600')) # 1小时
|
||||
SCHEDULE_CACHE_FILE = os.getenv('SCHEDULE_CACHE_FILE', 'data/schedule_cache.json')
|
||||
|
||||
# 调试目录配置
|
||||
DEBUG_DIR = os.getenv('DEBUG_DIR', 'debug')
|
||||
|
||||
# 飞书表格配置
|
||||
SHEET_RANGE = os.getenv('SHEET_RANGE', 'A:AF')
|
||||
REQUEST_TIMEOUT = int(os.getenv('REQUEST_TIMEOUT', '30'))
|
||||
|
||||
# GUI 配置
|
||||
GUI_FONT_FAMILY = os.getenv('GUI_FONT_FAMILY', 'SimHei')
|
||||
GUI_FONT_SIZE = int(os.getenv('GUI_FONT_SIZE', '10'))
|
||||
GUI_WINDOW_SIZE = os.getenv('GUI_WINDOW_SIZE', '1600x900')
|
||||
|
||||
# 排班刷新配置
|
||||
SCHEDULE_REFRESH_DAYS = int(os.getenv('SCHEDULE_REFRESH_DAYS', '30'))
|
||||
|
||||
# 特殊常量
|
||||
FIRST_DAY_OF_MONTH_SPECIAL = 1
|
||||
SEPARATOR_CHAR = '─'
|
||||
SEPARATOR_LENGTH = 50
|
||||
|
||||
@classmethod
|
||||
def validate(cls) -> bool:
|
||||
"""验证必要配置是否完整"""
|
||||
errors = []
|
||||
|
||||
# 检查 Confluence 配置
|
||||
if not cls.CONFLUENCE_BASE_URL:
|
||||
errors.append("CONFLUENCE_BASE_URL 未配置")
|
||||
if not cls.CONFLUENCE_TOKEN:
|
||||
errors.append("CONFLUENCE_TOKEN 未配置")
|
||||
if not cls.CONFLUENCE_CONTENT_ID:
|
||||
errors.append("CONFLUENCE_CONTENT_ID 未配置")
|
||||
|
||||
# 检查飞书配置(可选,但建议配置)
|
||||
has_feishu_token = bool(cls.FEISHU_TOKEN)
|
||||
has_app_credentials = bool(cls.FEISHU_APP_ID and cls.FEISHU_APP_SECRET)
|
||||
|
||||
if not has_feishu_token and not has_app_credentials:
|
||||
print("警告: 飞书认证未配置,排班功能将不可用")
|
||||
print(" 请配置 FEISHU_TOKEN 或 FEISHU_APP_ID + FEISHU_APP_SECRET")
|
||||
elif has_app_credentials:
|
||||
print("信息: 使用飞书应用凭证自动获取token")
|
||||
elif has_feishu_token:
|
||||
print("信息: 使用手动配置的FEISHU_TOKEN")
|
||||
|
||||
if not cls.FEISHU_SPREADSHEET_TOKEN:
|
||||
print("警告: FEISHU_SPREADSHEET_TOKEN 未配置,排班功能将不可用")
|
||||
|
||||
if errors:
|
||||
print("配置验证失败:")
|
||||
for error in errors:
|
||||
print(f" - {error}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def print_summary(cls):
|
||||
"""打印配置摘要"""
|
||||
print("配置摘要:")
|
||||
print(f" Confluence: {'已配置' if cls.CONFLUENCE_BASE_URL else '未配置'}")
|
||||
|
||||
# 飞书配置详情
|
||||
has_feishu_token = bool(cls.FEISHU_TOKEN)
|
||||
has_app_credentials = bool(cls.FEISHU_APP_ID and cls.FEISHU_APP_SECRET)
|
||||
has_spreadsheet_token = bool(cls.FEISHU_SPREADSHEET_TOKEN)
|
||||
|
||||
if has_app_credentials:
|
||||
feishu_status = f"应用凭证 (ID: {cls.FEISHU_APP_ID[:8]}...)"
|
||||
elif has_feishu_token:
|
||||
feishu_status = "手动token"
|
||||
else:
|
||||
feishu_status = "未配置"
|
||||
|
||||
print(f" 飞书认证: {feishu_status}")
|
||||
print(f" 飞书表格: {'已配置' if has_spreadsheet_token else '未配置'}")
|
||||
print(f" 数据库路径: {cls.DATABASE_PATH}")
|
||||
print(f" 每日目标TEU: {cls.DAILY_TARGET_TEU}")
|
||||
print(f" 排班刷新天数: {cls.SCHEDULE_REFRESH_DAYS}")
|
||||
|
||||
|
||||
# 全局配置实例
|
||||
config = Config()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试配置
|
||||
config.print_summary()
|
||||
if config.validate():
|
||||
print("配置验证通过")
|
||||
else:
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
统一配置模块
|
||||
集中管理所有配置项,避免硬编码
|
||||
"""
|
||||
import os
|
||||
from typing import Optional
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Config:
|
||||
"""应用配置类"""
|
||||
|
||||
# Confluence 配置
|
||||
CONFLUENCE_BASE_URL = os.getenv('CONFLUENCE_BASE_URL')
|
||||
CONFLUENCE_TOKEN = os.getenv('CONFLUENCE_TOKEN')
|
||||
CONFLUENCE_CONTENT_ID = os.getenv('CONFLUENCE_CONTENT_ID')
|
||||
|
||||
# 飞书配置
|
||||
FEISHU_BASE_URL = os.getenv('FEISHU_BASE_URL', 'https://open.feishu.cn/open-apis/sheets/v3')
|
||||
FEISHU_TOKEN = os.getenv('FEISHU_TOKEN')
|
||||
FEISHU_SPREADSHEET_TOKEN = os.getenv('FEISHU_SPREADSHEET_TOKEN')
|
||||
FEISHU_APP_ID = os.getenv('FEISHU_APP_ID')
|
||||
FEISHU_APP_SECRET = os.getenv('FEISHU_APP_SECRET')
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_PATH = os.getenv('DATABASE_PATH', 'data/daily_logs.db')
|
||||
SCHEDULE_DATABASE_PATH = os.getenv('SCHEDULE_DATABASE_PATH', 'data/daily_logs.db')
|
||||
|
||||
# 业务配置
|
||||
DAILY_TARGET_TEU = int(os.getenv('DAILY_TARGET_TEU', '300'))
|
||||
DUTY_PHONE = os.getenv('DUTY_PHONE', '13107662315')
|
||||
|
||||
# 缓存配置
|
||||
CACHE_TTL = int(os.getenv('CACHE_TTL', '3600')) # 1小时
|
||||
SCHEDULE_CACHE_FILE = os.getenv('SCHEDULE_CACHE_FILE', 'data/schedule_cache.json')
|
||||
|
||||
# 调试目录配置
|
||||
DEBUG_DIR = os.getenv('DEBUG_DIR', 'debug')
|
||||
|
||||
# 飞书表格配置
|
||||
SHEET_RANGE = os.getenv('SHEET_RANGE', 'A:AF')
|
||||
REQUEST_TIMEOUT = int(os.getenv('REQUEST_TIMEOUT', '30'))
|
||||
|
||||
# GUI 配置
|
||||
GUI_FONT_FAMILY = os.getenv('GUI_FONT_FAMILY', 'SimHei')
|
||||
GUI_FONT_SIZE = int(os.getenv('GUI_FONT_SIZE', '10'))
|
||||
GUI_WINDOW_SIZE = os.getenv('GUI_WINDOW_SIZE', '1600x900')
|
||||
|
||||
# 排班刷新配置
|
||||
SCHEDULE_REFRESH_DAYS = int(os.getenv('SCHEDULE_REFRESH_DAYS', '30'))
|
||||
|
||||
# 特殊常量
|
||||
FIRST_DAY_OF_MONTH_SPECIAL = 1
|
||||
SEPARATOR_CHAR = '─'
|
||||
SEPARATOR_LENGTH = 50
|
||||
|
||||
@classmethod
|
||||
def validate(cls) -> bool:
|
||||
"""验证必要配置是否完整"""
|
||||
errors = []
|
||||
|
||||
# 检查 Confluence 配置
|
||||
if not cls.CONFLUENCE_BASE_URL:
|
||||
errors.append("CONFLUENCE_BASE_URL 未配置")
|
||||
if not cls.CONFLUENCE_TOKEN:
|
||||
errors.append("CONFLUENCE_TOKEN 未配置")
|
||||
if not cls.CONFLUENCE_CONTENT_ID:
|
||||
errors.append("CONFLUENCE_CONTENT_ID 未配置")
|
||||
|
||||
# 检查飞书配置(可选,但建议配置)
|
||||
has_feishu_token = bool(cls.FEISHU_TOKEN)
|
||||
has_app_credentials = bool(cls.FEISHU_APP_ID and cls.FEISHU_APP_SECRET)
|
||||
|
||||
if not has_feishu_token and not has_app_credentials:
|
||||
print("警告: 飞书认证未配置,排班功能将不可用")
|
||||
print(" 请配置 FEISHU_TOKEN 或 FEISHU_APP_ID + FEISHU_APP_SECRET")
|
||||
elif has_app_credentials:
|
||||
print("信息: 使用飞书应用凭证自动获取token")
|
||||
elif has_feishu_token:
|
||||
print("信息: 使用手动配置的FEISHU_TOKEN")
|
||||
|
||||
if not cls.FEISHU_SPREADSHEET_TOKEN:
|
||||
print("警告: FEISHU_SPREADSHEET_TOKEN 未配置,排班功能将不可用")
|
||||
|
||||
if errors:
|
||||
print("配置验证失败:")
|
||||
for error in errors:
|
||||
print(f" - {error}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def print_summary(cls):
|
||||
"""打印配置摘要"""
|
||||
print("配置摘要:")
|
||||
print(f" Confluence: {'已配置' if cls.CONFLUENCE_BASE_URL else '未配置'}")
|
||||
|
||||
# 飞书配置详情
|
||||
has_feishu_token = bool(cls.FEISHU_TOKEN)
|
||||
has_app_credentials = bool(cls.FEISHU_APP_ID and cls.FEISHU_APP_SECRET)
|
||||
has_spreadsheet_token = bool(cls.FEISHU_SPREADSHEET_TOKEN)
|
||||
|
||||
if has_app_credentials:
|
||||
feishu_status = f"应用凭证 (ID: {cls.FEISHU_APP_ID[:8]}...)"
|
||||
elif has_feishu_token:
|
||||
feishu_status = "手动token"
|
||||
else:
|
||||
feishu_status = "未配置"
|
||||
|
||||
print(f" 飞书认证: {feishu_status}")
|
||||
print(f" 飞书表格: {'已配置' if has_spreadsheet_token else '未配置'}")
|
||||
print(f" 数据库路径: {cls.DATABASE_PATH}")
|
||||
print(f" 每日目标TEU: {cls.DAILY_TARGET_TEU}")
|
||||
print(f" 排班刷新天数: {cls.SCHEDULE_REFRESH_DAYS}")
|
||||
|
||||
|
||||
# 全局配置实例
|
||||
config = Config()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试配置
|
||||
config.print_summary()
|
||||
if config.validate():
|
||||
print("配置验证通过")
|
||||
else:
|
||||
print("配置验证失败")
|
||||
@@ -1,22 +1,22 @@
|
||||
"""
|
||||
Confluence API 模块
|
||||
提供Confluence页面内容获取和解析功能
|
||||
"""
|
||||
|
||||
from .client import ConfluenceClient, ConfluenceClientError
|
||||
from .parser import HTMLContentParser
|
||||
from .manager import ConfluenceContentManager
|
||||
from .text import HTMLTextExtractor, HTMLTextExtractorError
|
||||
from .log_parser import HandoverLogParser, ShipLog, LogParserError
|
||||
|
||||
__all__ = [
|
||||
'ConfluenceClient',
|
||||
'ConfluenceClientError',
|
||||
'HTMLContentParser',
|
||||
'ConfluenceContentManager',
|
||||
'HTMLTextExtractor',
|
||||
'HTMLTextExtractorError',
|
||||
'HandoverLogParser',
|
||||
'ShipLog',
|
||||
'LogParserError'
|
||||
]
|
||||
"""
|
||||
Confluence API 模块
|
||||
提供Confluence页面内容获取和解析功能
|
||||
"""
|
||||
|
||||
from .client import ConfluenceClient, ConfluenceClientError
|
||||
from .parser import HTMLContentParser
|
||||
from .manager import ConfluenceContentManager
|
||||
from .text import HTMLTextExtractor, HTMLTextExtractorError
|
||||
from .log_parser import HandoverLogParser, ShipLog, LogParserError
|
||||
|
||||
__all__ = [
|
||||
'ConfluenceClient',
|
||||
'ConfluenceClientError',
|
||||
'HTMLContentParser',
|
||||
'ConfluenceContentManager',
|
||||
'HTMLTextExtractor',
|
||||
'HTMLTextExtractorError',
|
||||
'HandoverLogParser',
|
||||
'ShipLog',
|
||||
'LogParserError'
|
||||
]
|
||||
|
||||
@@ -1,212 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Confluence API 客户端
|
||||
提供Confluence页面内容获取功能
|
||||
"""
|
||||
import requests
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ConfluenceClientError(Exception):
|
||||
"""Confluence API 错误"""
|
||||
pass
|
||||
|
||||
|
||||
class ConfluenceClient:
|
||||
"""Confluence REST API 客户端"""
|
||||
|
||||
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None):
|
||||
"""
|
||||
初始化客户端
|
||||
|
||||
参数:
|
||||
base_url: Confluence API 基础URL (不包含 /content),如果为None则使用配置
|
||||
token: Bearer 认证令牌,如果为None则使用配置
|
||||
"""
|
||||
self.base_url = (base_url or config.CONFLUENCE_BASE_URL).rstrip('/')
|
||||
self.token = token or config.CONFLUENCE_TOKEN
|
||||
|
||||
if not self.base_url or not self.token:
|
||||
raise ConfluenceClientError("Confluence配置不完整,请检查环境变量")
|
||||
|
||||
self.headers = {
|
||||
'Authorization': f'Bearer {self.token}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
# 使用 Session 重用连接
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.headers)
|
||||
self.session.timeout = config.REQUEST_TIMEOUT
|
||||
|
||||
logger.debug(f"Confluence客户端初始化完成,基础URL: {self.base_url}")
|
||||
|
||||
def fetch_content(self, content_id: str, expand: str = 'body.storage') -> Dict[str, Any]:
|
||||
"""
|
||||
获取页面内容
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
expand: 展开字段
|
||||
|
||||
返回:
|
||||
API 响应数据
|
||||
|
||||
异常:
|
||||
ConfluenceClientError: API调用失败
|
||||
requests.exceptions.RequestException: 网络请求失败
|
||||
"""
|
||||
url = f'{self.base_url}/content/{content_id}'
|
||||
params = {'expand': expand}
|
||||
|
||||
try:
|
||||
logger.debug(f"获取Confluence内容: {content_id}")
|
||||
response = self.session.get(url, params=params, timeout=config.REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
logger.info(f"成功获取Confluence内容: {content_id}")
|
||||
return data
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
status_code = e.response.status_code if e.response else '未知'
|
||||
error_msg = f"Confluence API HTTP错误: {status_code}, URL: {url}"
|
||||
logger.error(error_msg)
|
||||
raise ConfluenceClientError(error_msg) from e
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f"Confluence API 网络错误: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ConfluenceClientError(error_msg) from e
|
||||
|
||||
except ValueError as e:
|
||||
error_msg = f"Confluence API 响应解析失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ConfluenceClientError(error_msg) from e
|
||||
|
||||
def get_html(self, content_id: str) -> str:
|
||||
"""
|
||||
获取页面HTML内容
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
|
||||
返回:
|
||||
HTML 字符串
|
||||
|
||||
异常:
|
||||
ConfluenceClientError: API调用失败或HTML内容为空
|
||||
"""
|
||||
try:
|
||||
data = self.fetch_content(content_id)
|
||||
html = data.get('body', {}).get('storage', {}).get('value', '')
|
||||
|
||||
if not html:
|
||||
error_msg = f"Confluence页面HTML内容为空: {content_id}"
|
||||
logger.error(error_msg)
|
||||
raise ConfluenceClientError(error_msg)
|
||||
|
||||
logger.info(f"获取到Confluence HTML内容,长度: {len(html)} 字符")
|
||||
return html
|
||||
|
||||
except KeyError as e:
|
||||
error_msg = f"Confluence响应格式错误,缺少字段: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ConfluenceClientError(error_msg) from e
|
||||
|
||||
def test_connection(self, content_id: Optional[str] = None) -> bool:
|
||||
"""
|
||||
测试Confluence连接是否正常
|
||||
|
||||
参数:
|
||||
content_id: 测试页面ID,如果为None则使用配置
|
||||
|
||||
返回:
|
||||
连接是否正常
|
||||
"""
|
||||
test_content_id = content_id or config.CONFLUENCE_CONTENT_ID
|
||||
|
||||
try:
|
||||
data = self.fetch_content(test_content_id)
|
||||
title = data.get('title', '未知标题')
|
||||
logger.info(f"Confluence连接测试成功,页面: {title}")
|
||||
return True
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
logger.error(f"Confluence连接测试失败: {e}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Confluence连接测试异常: {e}")
|
||||
return False
|
||||
|
||||
def get_page_info(self, content_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取页面基本信息
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
|
||||
返回:
|
||||
页面信息字典
|
||||
"""
|
||||
try:
|
||||
data = self.fetch_content(content_id)
|
||||
return {
|
||||
'id': data.get('id'),
|
||||
'title': data.get('title'),
|
||||
'version': data.get('version', {}).get('number'),
|
||||
'created': data.get('history', {}).get('createdDate'),
|
||||
'last_updated': data.get('version', {}).get('when'),
|
||||
'space': data.get('space', {}).get('key'),
|
||||
'url': f"{self.base_url.replace('/rest/api', '')}/pages/{content_id}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"获取页面信息失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ConfluenceClientError(error_msg) from e
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
try:
|
||||
# 测试连接
|
||||
client = ConfluenceClient()
|
||||
|
||||
if client.test_connection():
|
||||
print("Confluence连接测试成功")
|
||||
|
||||
# 获取HTML内容
|
||||
content_id = config.CONFLUENCE_CONTENT_ID
|
||||
if content_id:
|
||||
html = client.get_html(content_id)
|
||||
print(f"获取到HTML内容,长度: {len(html)} 字符")
|
||||
|
||||
# 获取页面信息
|
||||
page_info = client.get_page_info(content_id)
|
||||
print(f"页面标题: {page_info.get('title')}")
|
||||
print(f"页面URL: {page_info.get('url')}")
|
||||
else:
|
||||
print("未配置CONFLUENCE_CONTENT_ID,跳过HTML获取")
|
||||
else:
|
||||
print("Confluence连接测试失败")
|
||||
sys.exit(1)
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
print(f"Confluence客户端错误: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"未知错误: {e}")
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Confluence API 客户端
|
||||
提供Confluence页面内容获取功能
|
||||
"""
|
||||
import requests
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
from src.retry import retry, retry_on_exception
|
||||
from src.error_handler import NetworkError, ConfigurationError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ConfluenceClientError(Exception):
|
||||
"""Confluence API 错误"""
|
||||
pass
|
||||
|
||||
|
||||
class ConfluenceClient:
|
||||
"""Confluence REST API 客户端"""
|
||||
|
||||
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None):
|
||||
"""
|
||||
初始化客户端
|
||||
|
||||
参数:
|
||||
base_url: Confluence API 基础URL (不包含 /content),如果为None则使用配置
|
||||
token: Bearer 认证令牌,如果为None则使用配置
|
||||
"""
|
||||
self.base_url = (base_url or config.CONFLUENCE_BASE_URL).rstrip('/')
|
||||
self.token = token or config.CONFLUENCE_TOKEN
|
||||
|
||||
if not self.base_url or not self.token:
|
||||
raise ConfigurationError("Confluence配置不完整,请检查环境变量")
|
||||
|
||||
self.headers = {
|
||||
'Authorization': f'Bearer {self.token}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
# 使用 Session 重用连接
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.headers)
|
||||
self.session.timeout = config.REQUEST_TIMEOUT
|
||||
|
||||
logger.debug(f"Confluence客户端初始化完成,基础URL: {self.base_url}")
|
||||
|
||||
@retry_on_exception(
|
||||
exception_type=requests.exceptions.RequestException,
|
||||
max_attempts=3,
|
||||
delay=2.0
|
||||
)
|
||||
def fetch_content(self, content_id: str, expand: str = 'body.storage') -> Dict[str, Any]:
|
||||
"""
|
||||
获取页面内容
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
expand: 展开字段
|
||||
|
||||
返回:
|
||||
API 响应数据
|
||||
|
||||
异常:
|
||||
ConfluenceClientError: API调用失败
|
||||
NetworkError: 网络请求失败
|
||||
"""
|
||||
url = f'{self.base_url}/content/{content_id}'
|
||||
params = {'expand': expand}
|
||||
|
||||
try:
|
||||
logger.debug(f"获取Confluence内容: {content_id}")
|
||||
response = self.session.get(url, params=params, timeout=config.REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
logger.info(f"成功获取Confluence内容: {content_id}")
|
||||
return data
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
status_code = e.response.status_code if e.response else '未知'
|
||||
error_msg = f"Confluence API HTTP错误: {status_code}, URL: {url}"
|
||||
logger.error(error_msg)
|
||||
raise NetworkError(error_msg) from e
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f"Confluence API 网络错误: {e}"
|
||||
logger.error(error_msg)
|
||||
raise NetworkError(error_msg) from e
|
||||
|
||||
except ValueError as e:
|
||||
error_msg = f"Confluence API 响应解析失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ConfluenceClientError(error_msg) from e
|
||||
|
||||
def get_html(self, content_id: str) -> str:
|
||||
"""
|
||||
获取页面HTML内容
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
|
||||
返回:
|
||||
HTML 字符串
|
||||
|
||||
异常:
|
||||
ConfluenceClientError: API调用失败或HTML内容为空
|
||||
"""
|
||||
try:
|
||||
data = self.fetch_content(content_id)
|
||||
html = data.get('body', {}).get('storage', {}).get('value', '')
|
||||
|
||||
if not html:
|
||||
error_msg = f"Confluence页面HTML内容为空: {content_id}"
|
||||
logger.error(error_msg)
|
||||
raise ConfluenceClientError(error_msg)
|
||||
|
||||
logger.info(f"获取到Confluence HTML内容,长度: {len(html)} 字符")
|
||||
return html
|
||||
|
||||
except KeyError as e:
|
||||
error_msg = f"Confluence响应格式错误,缺少字段: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ConfluenceClientError(error_msg) from e
|
||||
|
||||
def test_connection(self, content_id: Optional[str] = None) -> bool:
|
||||
"""
|
||||
测试Confluence连接是否正常
|
||||
|
||||
参数:
|
||||
content_id: 测试页面ID,如果为None则使用配置
|
||||
|
||||
返回:
|
||||
连接是否正常
|
||||
"""
|
||||
test_content_id = content_id or config.CONFLUENCE_CONTENT_ID
|
||||
|
||||
try:
|
||||
data = self.fetch_content(test_content_id)
|
||||
title = data.get('title', '未知标题')
|
||||
logger.info(f"Confluence连接测试成功,页面: {title}")
|
||||
return True
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
logger.error(f"Confluence连接测试失败: {e}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Confluence连接测试异常: {e}")
|
||||
return False
|
||||
|
||||
def get_page_info(self, content_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取页面基本信息
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
|
||||
返回:
|
||||
页面信息字典
|
||||
"""
|
||||
try:
|
||||
data = self.fetch_content(content_id)
|
||||
return {
|
||||
'id': data.get('id'),
|
||||
'title': data.get('title'),
|
||||
'version': data.get('version', {}).get('number'),
|
||||
'created': data.get('history', {}).get('createdDate'),
|
||||
'last_updated': data.get('version', {}).get('when'),
|
||||
'space': data.get('space', {}).get('key'),
|
||||
'url': f"{self.base_url.replace('/rest/api', '')}/pages/{content_id}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"获取页面信息失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ConfluenceClientError(error_msg) from e
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
try:
|
||||
# 测试连接
|
||||
client = ConfluenceClient()
|
||||
|
||||
if client.test_connection():
|
||||
print("Confluence连接测试成功")
|
||||
|
||||
# 获取HTML内容
|
||||
content_id = config.CONFLUENCE_CONTENT_ID
|
||||
if content_id:
|
||||
html = client.get_html(content_id)
|
||||
print(f"获取到HTML内容,长度: {len(html)} 字符")
|
||||
|
||||
# 获取页面信息
|
||||
page_info = client.get_page_info(content_id)
|
||||
print(f"页面标题: {page_info.get('title')}")
|
||||
print(f"页面URL: {page_info.get('url')}")
|
||||
else:
|
||||
print("未配置CONFLUENCE_CONTENT_ID,跳过HTML获取")
|
||||
else:
|
||||
print("Confluence连接测试失败")
|
||||
sys.exit(1)
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
print(f"Confluence客户端错误: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"未知错误: {e}")
|
||||
sys.exit(1)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,354 +1,354 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Confluence 内容管理器
|
||||
提供高级的Confluence内容管理功能
|
||||
"""
|
||||
from typing import Dict, List, Optional, Any
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from src.logging_config import get_logger
|
||||
from .client import ConfluenceClient, ConfluenceClientError
|
||||
from .parser import HTMLContentParser
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ConfluenceContentManager:
|
||||
"""Confluence 内容管理器"""
|
||||
|
||||
def __init__(self, client: Optional[ConfluenceClient] = None):
|
||||
"""
|
||||
初始化内容管理器
|
||||
|
||||
参数:
|
||||
client: Confluence客户端实例,如果为None则创建新实例
|
||||
"""
|
||||
self.client = client or ConfluenceClient()
|
||||
self.parser = HTMLContentParser()
|
||||
logger.debug("Confluence内容管理器初始化完成")
|
||||
|
||||
def get_content_with_analysis(self, content_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取内容并进行分析
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
|
||||
返回:
|
||||
包含内容和分析结果的字典
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取并分析Confluence内容: {content_id}")
|
||||
|
||||
# 获取页面信息
|
||||
page_info = self.client.get_page_info(content_id)
|
||||
|
||||
# 获取HTML内容
|
||||
html = self.client.get_html(content_id)
|
||||
|
||||
# 分析内容
|
||||
analysis = self.parser.analyze_content(html)
|
||||
|
||||
# 提取纯文本(前500字符)
|
||||
plain_text = self.parser.extract_plain_text(html)
|
||||
preview_text = plain_text[:500] + "..." if len(plain_text) > 500 else plain_text
|
||||
|
||||
result = {
|
||||
'page_info': page_info,
|
||||
'html_length': len(html),
|
||||
'analysis': analysis,
|
||||
'preview_text': preview_text,
|
||||
'has_content': len(html) > 0,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"内容分析完成: {content_id}")
|
||||
return result
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
logger.error(f"获取内容失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"内容分析失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def check_content_health(self, content_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
检查内容健康状况
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
|
||||
返回:
|
||||
健康检查结果
|
||||
"""
|
||||
try:
|
||||
logger.info(f"检查内容健康状况: {content_id}")
|
||||
|
||||
# 获取页面信息
|
||||
page_info = self.client.get_page_info(content_id)
|
||||
|
||||
# 获取HTML内容
|
||||
html = self.client.get_html(content_id)
|
||||
|
||||
# 分析内容
|
||||
analysis = self.parser.analyze_content(html)
|
||||
|
||||
# 检查健康状况
|
||||
health_checks = {
|
||||
'has_content': len(html) > 0,
|
||||
'has_text': analysis['plain_text_length'] > 0,
|
||||
'has_structure': analysis['has_tables'] or analysis['has_links'] or analysis['has_images'],
|
||||
'content_size_ok': 100 <= len(html) <= 1000000, # 100字节到1MB
|
||||
'text_ratio_ok': analysis['plain_text_length'] / max(len(html), 1) > 0.1, # 文本占比至少10%
|
||||
'word_count_ok': analysis['word_count'] >= 10, # 至少10个单词
|
||||
'has_links': analysis['has_links'],
|
||||
'has_images': analysis['has_images'],
|
||||
'has_tables': analysis['has_tables']
|
||||
}
|
||||
|
||||
# 计算健康分数
|
||||
passed_checks = sum(1 for check in health_checks.values() if check)
|
||||
total_checks = len(health_checks)
|
||||
health_score = passed_checks / total_checks
|
||||
|
||||
# 生成建议
|
||||
suggestions = []
|
||||
if not health_checks['has_content']:
|
||||
suggestions.append("页面内容为空")
|
||||
if not health_checks['has_text']:
|
||||
suggestions.append("页面缺少文本内容")
|
||||
if not health_checks['content_size_ok']:
|
||||
suggestions.append("页面内容大小异常")
|
||||
if not health_checks['text_ratio_ok']:
|
||||
suggestions.append("文本占比过低")
|
||||
if not health_checks['word_count_ok']:
|
||||
suggestions.append("单词数量不足")
|
||||
|
||||
result = {
|
||||
'page_info': page_info,
|
||||
'health_score': health_score,
|
||||
'health_status': '健康' if health_score >= 0.8 else '警告' if health_score >= 0.5 else '异常',
|
||||
'health_checks': health_checks,
|
||||
'analysis': analysis,
|
||||
'suggestions': suggestions,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"健康检查完成: {content_id}, 分数: {health_score:.2f}")
|
||||
return result
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
logger.error(f"健康检查失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"健康检查失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def extract_content_summary(self, content_id: str, max_length: int = 200) -> Dict[str, Any]:
|
||||
"""
|
||||
提取内容摘要
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
max_length: 摘要最大长度
|
||||
|
||||
返回:
|
||||
内容摘要
|
||||
"""
|
||||
try:
|
||||
logger.info(f"提取内容摘要: {content_id}")
|
||||
|
||||
# 获取页面信息
|
||||
page_info = self.client.get_page_info(content_id)
|
||||
|
||||
# 获取HTML内容
|
||||
html = self.client.get_html(content_id)
|
||||
|
||||
# 提取纯文本
|
||||
plain_text = self.parser.extract_plain_text(html)
|
||||
|
||||
# 生成摘要
|
||||
if len(plain_text) <= max_length:
|
||||
summary = plain_text
|
||||
else:
|
||||
# 尝试在句子边界处截断
|
||||
sentences = plain_text.split('. ')
|
||||
summary_parts = []
|
||||
current_length = 0
|
||||
|
||||
for sentence in sentences:
|
||||
if current_length + len(sentence) + 2 <= max_length: # +2 for ". "
|
||||
summary_parts.append(sentence)
|
||||
current_length += len(sentence) + 2
|
||||
else:
|
||||
break
|
||||
|
||||
summary = '. '.join(summary_parts)
|
||||
if summary and not summary.endswith('.'):
|
||||
summary += '...'
|
||||
|
||||
# 提取关键信息
|
||||
links = self.parser.extract_links(html)
|
||||
images = self.parser.extract_images(html)
|
||||
tables = self.parser.extract_tables(html)
|
||||
|
||||
result = {
|
||||
'page_info': page_info,
|
||||
'summary': summary,
|
||||
'summary_length': len(summary),
|
||||
'total_length': len(plain_text),
|
||||
'key_elements': {
|
||||
'link_count': len(links),
|
||||
'image_count': len(images),
|
||||
'table_count': len(tables)
|
||||
},
|
||||
'has_rich_content': len(links) > 0 or len(images) > 0 or len(tables) > 0,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"内容摘要提取完成: {content_id}")
|
||||
return result
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
logger.error(f"提取摘要失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"提取摘要失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def batch_analyze_pages(self, content_ids: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
批量分析多个页面
|
||||
|
||||
参数:
|
||||
content_ids: 页面ID列表
|
||||
|
||||
返回:
|
||||
批量分析结果
|
||||
"""
|
||||
try:
|
||||
logger.info(f"批量分析 {len(content_ids)} 个页面")
|
||||
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
for content_id in content_ids:
|
||||
try:
|
||||
result = self.get_content_with_analysis(content_id)
|
||||
results.append(result)
|
||||
logger.debug(f"页面分析完成: {content_id}")
|
||||
|
||||
except Exception as e:
|
||||
errors.append({
|
||||
'content_id': content_id,
|
||||
'error': str(e)
|
||||
})
|
||||
logger.warning(f"页面分析失败: {content_id}, 错误: {e}")
|
||||
|
||||
# 计算统计信息
|
||||
if results:
|
||||
total_pages = len(results)
|
||||
successful_pages = len(results)
|
||||
failed_pages = len(errors)
|
||||
|
||||
total_html_length = sum(r['html_length'] for r in results)
|
||||
avg_html_length = total_html_length / successful_pages if successful_pages > 0 else 0
|
||||
|
||||
stats = {
|
||||
'total_pages': total_pages,
|
||||
'successful_pages': successful_pages,
|
||||
'failed_pages': failed_pages,
|
||||
'success_rate': successful_pages / total_pages if total_pages > 0 else 0,
|
||||
'total_html_length': total_html_length,
|
||||
'avg_html_length': avg_html_length,
|
||||
'has_content_pages': sum(1 for r in results if r['has_content']),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
else:
|
||||
stats = {
|
||||
'total_pages': 0,
|
||||
'successful_pages': 0,
|
||||
'failed_pages': len(errors),
|
||||
'success_rate': 0,
|
||||
'total_html_length': 0,
|
||||
'avg_html_length': 0,
|
||||
'has_content_pages': 0,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
batch_result = {
|
||||
'stats': stats,
|
||||
'results': results,
|
||||
'errors': errors,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"批量分析完成: 成功 {len(results)} 个,失败 {len(errors)} 个")
|
||||
return batch_result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"批量分析失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
try:
|
||||
# 创建管理器
|
||||
manager = ConfluenceContentManager()
|
||||
|
||||
# 测试连接
|
||||
from src.config import config
|
||||
content_id = config.CONFLUENCE_CONTENT_ID
|
||||
|
||||
if not content_id:
|
||||
print("未配置CONFLUENCE_CONTENT_ID,跳过测试")
|
||||
sys.exit(0)
|
||||
|
||||
if manager.client.test_connection(content_id):
|
||||
print("Confluence连接测试成功")
|
||||
|
||||
# 测试内容分析
|
||||
print("\n1. 测试内容分析:")
|
||||
analysis = manager.get_content_with_analysis(content_id)
|
||||
print(f" 页面标题: {analysis['page_info'].get('title')}")
|
||||
print(f" 内容长度: {analysis['html_length']} 字符")
|
||||
print(f" 文本预览: {analysis['preview_text'][:100]}...")
|
||||
|
||||
# 测试健康检查
|
||||
print("\n2. 测试健康检查:")
|
||||
health = manager.check_content_health(content_id)
|
||||
print(f" 健康分数: {health['health_score']:.2f}")
|
||||
print(f" 健康状态: {health['health_status']}")
|
||||
print(f" 建议: {health['suggestions']}")
|
||||
|
||||
# 测试内容摘要
|
||||
print("\n3. 测试内容摘要:")
|
||||
summary = manager.extract_content_summary(content_id)
|
||||
print(f" 摘要: {summary['summary']}")
|
||||
print(f" 摘要长度: {summary['summary_length']} 字符")
|
||||
print(f" 总长度: {summary['total_length']} 字符")
|
||||
|
||||
print("\n所有测试通过")
|
||||
|
||||
else:
|
||||
print("Confluence连接测试失败")
|
||||
sys.exit(1)
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
print(f"Confluence客户端错误: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"未知错误: {e}")
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Confluence 内容管理器
|
||||
提供高级的Confluence内容管理功能
|
||||
"""
|
||||
from typing import Dict, List, Optional, Any
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from src.logging_config import get_logger
|
||||
from .client import ConfluenceClient, ConfluenceClientError
|
||||
from .parser import HTMLContentParser
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ConfluenceContentManager:
|
||||
"""Confluence 内容管理器"""
|
||||
|
||||
def __init__(self, client: Optional[ConfluenceClient] = None):
|
||||
"""
|
||||
初始化内容管理器
|
||||
|
||||
参数:
|
||||
client: Confluence客户端实例,如果为None则创建新实例
|
||||
"""
|
||||
self.client = client or ConfluenceClient()
|
||||
self.parser = HTMLContentParser()
|
||||
logger.debug("Confluence内容管理器初始化完成")
|
||||
|
||||
def get_content_with_analysis(self, content_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取内容并进行分析
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
|
||||
返回:
|
||||
包含内容和分析结果的字典
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取并分析Confluence内容: {content_id}")
|
||||
|
||||
# 获取页面信息
|
||||
page_info = self.client.get_page_info(content_id)
|
||||
|
||||
# 获取HTML内容
|
||||
html = self.client.get_html(content_id)
|
||||
|
||||
# 分析内容
|
||||
analysis = self.parser.analyze_content(html)
|
||||
|
||||
# 提取纯文本(前500字符)
|
||||
plain_text = self.parser.extract_plain_text(html)
|
||||
preview_text = plain_text[:500] + "..." if len(plain_text) > 500 else plain_text
|
||||
|
||||
result = {
|
||||
'page_info': page_info,
|
||||
'html_length': len(html),
|
||||
'analysis': analysis,
|
||||
'preview_text': preview_text,
|
||||
'has_content': len(html) > 0,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"内容分析完成: {content_id}")
|
||||
return result
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
logger.error(f"获取内容失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"内容分析失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def check_content_health(self, content_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
检查内容健康状况
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
|
||||
返回:
|
||||
健康检查结果
|
||||
"""
|
||||
try:
|
||||
logger.info(f"检查内容健康状况: {content_id}")
|
||||
|
||||
# 获取页面信息
|
||||
page_info = self.client.get_page_info(content_id)
|
||||
|
||||
# 获取HTML内容
|
||||
html = self.client.get_html(content_id)
|
||||
|
||||
# 分析内容
|
||||
analysis = self.parser.analyze_content(html)
|
||||
|
||||
# 检查健康状况
|
||||
health_checks = {
|
||||
'has_content': len(html) > 0,
|
||||
'has_text': analysis['plain_text_length'] > 0,
|
||||
'has_structure': analysis['has_tables'] or analysis['has_links'] or analysis['has_images'],
|
||||
'content_size_ok': 100 <= len(html) <= 1000000, # 100字节到1MB
|
||||
'text_ratio_ok': analysis['plain_text_length'] / max(len(html), 1) > 0.1, # 文本占比至少10%
|
||||
'word_count_ok': analysis['word_count'] >= 10, # 至少10个单词
|
||||
'has_links': analysis['has_links'],
|
||||
'has_images': analysis['has_images'],
|
||||
'has_tables': analysis['has_tables']
|
||||
}
|
||||
|
||||
# 计算健康分数
|
||||
passed_checks = sum(1 for check in health_checks.values() if check)
|
||||
total_checks = len(health_checks)
|
||||
health_score = passed_checks / total_checks
|
||||
|
||||
# 生成建议
|
||||
suggestions = []
|
||||
if not health_checks['has_content']:
|
||||
suggestions.append("页面内容为空")
|
||||
if not health_checks['has_text']:
|
||||
suggestions.append("页面缺少文本内容")
|
||||
if not health_checks['content_size_ok']:
|
||||
suggestions.append("页面内容大小异常")
|
||||
if not health_checks['text_ratio_ok']:
|
||||
suggestions.append("文本占比过低")
|
||||
if not health_checks['word_count_ok']:
|
||||
suggestions.append("单词数量不足")
|
||||
|
||||
result = {
|
||||
'page_info': page_info,
|
||||
'health_score': health_score,
|
||||
'health_status': '健康' if health_score >= 0.8 else '警告' if health_score >= 0.5 else '异常',
|
||||
'health_checks': health_checks,
|
||||
'analysis': analysis,
|
||||
'suggestions': suggestions,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"健康检查完成: {content_id}, 分数: {health_score:.2f}")
|
||||
return result
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
logger.error(f"健康检查失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"健康检查失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def extract_content_summary(self, content_id: str, max_length: int = 200) -> Dict[str, Any]:
|
||||
"""
|
||||
提取内容摘要
|
||||
|
||||
参数:
|
||||
content_id: 页面ID
|
||||
max_length: 摘要最大长度
|
||||
|
||||
返回:
|
||||
内容摘要
|
||||
"""
|
||||
try:
|
||||
logger.info(f"提取内容摘要: {content_id}")
|
||||
|
||||
# 获取页面信息
|
||||
page_info = self.client.get_page_info(content_id)
|
||||
|
||||
# 获取HTML内容
|
||||
html = self.client.get_html(content_id)
|
||||
|
||||
# 提取纯文本
|
||||
plain_text = self.parser.extract_plain_text(html)
|
||||
|
||||
# 生成摘要
|
||||
if len(plain_text) <= max_length:
|
||||
summary = plain_text
|
||||
else:
|
||||
# 尝试在句子边界处截断
|
||||
sentences = plain_text.split('. ')
|
||||
summary_parts = []
|
||||
current_length = 0
|
||||
|
||||
for sentence in sentences:
|
||||
if current_length + len(sentence) + 2 <= max_length: # +2 for ". "
|
||||
summary_parts.append(sentence)
|
||||
current_length += len(sentence) + 2
|
||||
else:
|
||||
break
|
||||
|
||||
summary = '. '.join(summary_parts)
|
||||
if summary and not summary.endswith('.'):
|
||||
summary += '...'
|
||||
|
||||
# 提取关键信息
|
||||
links = self.parser.extract_links(html)
|
||||
images = self.parser.extract_images(html)
|
||||
tables = self.parser.extract_tables(html)
|
||||
|
||||
result = {
|
||||
'page_info': page_info,
|
||||
'summary': summary,
|
||||
'summary_length': len(summary),
|
||||
'total_length': len(plain_text),
|
||||
'key_elements': {
|
||||
'link_count': len(links),
|
||||
'image_count': len(images),
|
||||
'table_count': len(tables)
|
||||
},
|
||||
'has_rich_content': len(links) > 0 or len(images) > 0 or len(tables) > 0,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"内容摘要提取完成: {content_id}")
|
||||
return result
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
logger.error(f"提取摘要失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"提取摘要失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def batch_analyze_pages(self, content_ids: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
批量分析多个页面
|
||||
|
||||
参数:
|
||||
content_ids: 页面ID列表
|
||||
|
||||
返回:
|
||||
批量分析结果
|
||||
"""
|
||||
try:
|
||||
logger.info(f"批量分析 {len(content_ids)} 个页面")
|
||||
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
for content_id in content_ids:
|
||||
try:
|
||||
result = self.get_content_with_analysis(content_id)
|
||||
results.append(result)
|
||||
logger.debug(f"页面分析完成: {content_id}")
|
||||
|
||||
except Exception as e:
|
||||
errors.append({
|
||||
'content_id': content_id,
|
||||
'error': str(e)
|
||||
})
|
||||
logger.warning(f"页面分析失败: {content_id}, 错误: {e}")
|
||||
|
||||
# 计算统计信息
|
||||
if results:
|
||||
total_pages = len(results)
|
||||
successful_pages = len(results)
|
||||
failed_pages = len(errors)
|
||||
|
||||
total_html_length = sum(r['html_length'] for r in results)
|
||||
avg_html_length = total_html_length / successful_pages if successful_pages > 0 else 0
|
||||
|
||||
stats = {
|
||||
'total_pages': total_pages,
|
||||
'successful_pages': successful_pages,
|
||||
'failed_pages': failed_pages,
|
||||
'success_rate': successful_pages / total_pages if total_pages > 0 else 0,
|
||||
'total_html_length': total_html_length,
|
||||
'avg_html_length': avg_html_length,
|
||||
'has_content_pages': sum(1 for r in results if r['has_content']),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
else:
|
||||
stats = {
|
||||
'total_pages': 0,
|
||||
'successful_pages': 0,
|
||||
'failed_pages': len(errors),
|
||||
'success_rate': 0,
|
||||
'total_html_length': 0,
|
||||
'avg_html_length': 0,
|
||||
'has_content_pages': 0,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
batch_result = {
|
||||
'stats': stats,
|
||||
'results': results,
|
||||
'errors': errors,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"批量分析完成: 成功 {len(results)} 个,失败 {len(errors)} 个")
|
||||
return batch_result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"批量分析失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
try:
|
||||
# 创建管理器
|
||||
manager = ConfluenceContentManager()
|
||||
|
||||
# 测试连接
|
||||
from src.config import config
|
||||
content_id = config.CONFLUENCE_CONTENT_ID
|
||||
|
||||
if not content_id:
|
||||
print("未配置CONFLUENCE_CONTENT_ID,跳过测试")
|
||||
sys.exit(0)
|
||||
|
||||
if manager.client.test_connection(content_id):
|
||||
print("Confluence连接测试成功")
|
||||
|
||||
# 测试内容分析
|
||||
print("\n1. 测试内容分析:")
|
||||
analysis = manager.get_content_with_analysis(content_id)
|
||||
print(f" 页面标题: {analysis['page_info'].get('title')}")
|
||||
print(f" 内容长度: {analysis['html_length']} 字符")
|
||||
print(f" 文本预览: {analysis['preview_text'][:100]}...")
|
||||
|
||||
# 测试健康检查
|
||||
print("\n2. 测试健康检查:")
|
||||
health = manager.check_content_health(content_id)
|
||||
print(f" 健康分数: {health['health_score']:.2f}")
|
||||
print(f" 健康状态: {health['health_status']}")
|
||||
print(f" 建议: {health['suggestions']}")
|
||||
|
||||
# 测试内容摘要
|
||||
print("\n3. 测试内容摘要:")
|
||||
summary = manager.extract_content_summary(content_id)
|
||||
print(f" 摘要: {summary['summary']}")
|
||||
print(f" 摘要长度: {summary['summary_length']} 字符")
|
||||
print(f" 总长度: {summary['total_length']} 字符")
|
||||
|
||||
print("\n所有测试通过")
|
||||
|
||||
else:
|
||||
print("Confluence连接测试失败")
|
||||
sys.exit(1)
|
||||
|
||||
except ConfluenceClientError as e:
|
||||
print(f"Confluence客户端错误: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"未知错误: {e}")
|
||||
sys.exit(1)
|
||||
@@ -1,244 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Confluence HTML 内容解析器
|
||||
提供Confluence HTML内容的解析和格式化功能
|
||||
"""
|
||||
import re
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HTMLContentParser:
|
||||
"""Confluence HTML 内容解析器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化解析器"""
|
||||
logger.debug("HTML内容解析器初始化完成")
|
||||
|
||||
def extract_plain_text(self, html: str) -> str:
|
||||
"""
|
||||
从HTML中提取纯文本(简单版本)
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
纯文本字符串
|
||||
"""
|
||||
try:
|
||||
# 移除HTML标签
|
||||
text = re.sub(r'<[^>]+>', ' ', html)
|
||||
# 合并多个空格
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
# 解码HTML实体(简单版本)
|
||||
text = text.replace(' ', ' ').replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
# 去除首尾空格
|
||||
text = text.strip()
|
||||
|
||||
logger.debug(f"提取纯文本完成,长度: {len(text)} 字符")
|
||||
return text
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"提取纯文本失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def extract_links(self, html: str) -> List[Dict[str, str]]:
|
||||
"""
|
||||
从HTML中提取链接
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
链接列表,每个链接包含 'text' 和 'url'
|
||||
"""
|
||||
links = []
|
||||
try:
|
||||
# 简单的正则表达式匹配链接
|
||||
link_pattern = r'<a\s+[^>]*href=["\']([^"\']+)["\'][^>]*>([^<]+)</a>'
|
||||
matches = re.findall(link_pattern, html, re.IGNORECASE)
|
||||
|
||||
for url, text in matches:
|
||||
links.append({
|
||||
'text': text.strip(),
|
||||
'url': url.strip()
|
||||
})
|
||||
|
||||
logger.debug(f"提取到 {len(links)} 个链接")
|
||||
return links
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"提取链接失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def extract_images(self, html: str) -> List[Dict[str, str]]:
|
||||
"""
|
||||
从HTML中提取图片
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
图片列表,每个图片包含 'src' 和 'alt'
|
||||
"""
|
||||
images = []
|
||||
try:
|
||||
# 简单的正则表达式匹配图片
|
||||
img_pattern = r'<img\s+[^>]*src=["\']([^"\']+)["\'][^>]*alt=["\']([^"\']*)["\'][^>]*>'
|
||||
matches = re.findall(img_pattern, html, re.IGNORECASE)
|
||||
|
||||
for src, alt in matches:
|
||||
images.append({
|
||||
'src': src.strip(),
|
||||
'alt': alt.strip()
|
||||
})
|
||||
|
||||
logger.debug(f"提取到 {len(images)} 张图片")
|
||||
return images
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"提取图片失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def extract_tables(self, html: str) -> List[List[List[str]]]:
|
||||
"""
|
||||
从HTML中提取表格数据
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
表格列表,每个表格是二维列表
|
||||
"""
|
||||
tables = []
|
||||
try:
|
||||
# 简单的表格提取(仅支持简单表格)
|
||||
table_pattern = r'<table[^>]*>(.*?)</table>'
|
||||
table_matches = re.findall(table_pattern, html, re.IGNORECASE | re.DOTALL)
|
||||
|
||||
for table_html in table_matches:
|
||||
rows = []
|
||||
# 提取行
|
||||
row_pattern = r'<tr[^>]*>(.*?)</tr>'
|
||||
row_matches = re.findall(row_pattern, table_html, re.IGNORECASE | re.DOTALL)
|
||||
|
||||
for row_html in row_matches:
|
||||
cells = []
|
||||
# 提取单元格
|
||||
cell_pattern = r'<t[dh][^>]*>(.*?)</t[dh]>'
|
||||
cell_matches = re.findall(cell_pattern, row_html, re.IGNORECASE | re.DOTALL)
|
||||
|
||||
for cell_html in cell_matches:
|
||||
# 清理单元格内容
|
||||
cell_text = re.sub(r'<[^>]+>', '', cell_html)
|
||||
cell_text = re.sub(r'\s+', ' ', cell_text).strip()
|
||||
cells.append(cell_text)
|
||||
|
||||
rows.append(cells)
|
||||
|
||||
if rows: # 只添加非空表格
|
||||
tables.append(rows)
|
||||
|
||||
logger.debug(f"提取到 {len(tables)} 个表格")
|
||||
return tables
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"提取表格失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def analyze_content(self, html: str) -> Dict[str, any]:
|
||||
"""
|
||||
分析HTML内容
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
内容分析结果
|
||||
"""
|
||||
try:
|
||||
plain_text = self.extract_plain_text(html)
|
||||
links = self.extract_links(html)
|
||||
images = self.extract_images(html)
|
||||
tables = self.extract_tables(html)
|
||||
|
||||
analysis = {
|
||||
'total_length': len(html),
|
||||
'plain_text_length': len(plain_text),
|
||||
'link_count': len(links),
|
||||
'image_count': len(images),
|
||||
'table_count': len(tables),
|
||||
'word_count': len(plain_text.split()),
|
||||
'line_count': plain_text.count('\n') + 1,
|
||||
'has_tables': len(tables) > 0,
|
||||
'has_images': len(images) > 0,
|
||||
'has_links': len(links) > 0
|
||||
}
|
||||
|
||||
logger.info(f"内容分析完成: {analysis}")
|
||||
return analysis
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"内容分析失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# 测试HTML
|
||||
test_html = """
|
||||
<html>
|
||||
<body>
|
||||
<h1>测试页面</h1>
|
||||
<p>这是一个测试页面,包含<a href="https://example.com">链接</a>和图片。</p>
|
||||
<img src="test.jpg" alt="测试图片">
|
||||
<table>
|
||||
<tr><th>标题1</th><th>标题2</th></tr>
|
||||
<tr><td>数据1</td><td>数据2</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
try:
|
||||
parser = HTMLContentParser()
|
||||
|
||||
# 测试纯文本提取
|
||||
text = parser.extract_plain_text(test_html)
|
||||
print(f"纯文本: {text[:100]}...")
|
||||
|
||||
# 测试链接提取
|
||||
links = parser.extract_links(test_html)
|
||||
print(f"链接: {links}")
|
||||
|
||||
# 测试图片提取
|
||||
images = parser.extract_images(test_html)
|
||||
print(f"图片: {images}")
|
||||
|
||||
# 测试表格提取
|
||||
tables = parser.extract_tables(test_html)
|
||||
print(f"表格: {tables}")
|
||||
|
||||
# 测试内容分析
|
||||
analysis = parser.analyze_content(test_html)
|
||||
print(f"内容分析: {analysis}")
|
||||
|
||||
print("所有测试通过")
|
||||
|
||||
except Exception as e:
|
||||
print(f"测试失败: {e}")
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Confluence HTML 内容解析器
|
||||
提供Confluence HTML内容的解析和格式化功能
|
||||
"""
|
||||
import re
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HTMLContentParser:
|
||||
"""Confluence HTML 内容解析器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化解析器"""
|
||||
logger.debug("HTML内容解析器初始化完成")
|
||||
|
||||
def extract_plain_text(self, html: str) -> str:
|
||||
"""
|
||||
从HTML中提取纯文本(简单版本)
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
纯文本字符串
|
||||
"""
|
||||
try:
|
||||
# 移除HTML标签
|
||||
text = re.sub(r'<[^>]+>', ' ', html)
|
||||
# 合并多个空格
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
# 解码HTML实体(简单版本)
|
||||
text = text.replace(' ', ' ').replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
# 去除首尾空格
|
||||
text = text.strip()
|
||||
|
||||
logger.debug(f"提取纯文本完成,长度: {len(text)} 字符")
|
||||
return text
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"提取纯文本失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def extract_links(self, html: str) -> List[Dict[str, str]]:
|
||||
"""
|
||||
从HTML中提取链接
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
链接列表,每个链接包含 'text' 和 'url'
|
||||
"""
|
||||
links = []
|
||||
try:
|
||||
# 简单的正则表达式匹配链接
|
||||
link_pattern = r'<a\s+[^>]*href=["\']([^"\']+)["\'][^>]*>([^<]+)</a>'
|
||||
matches = re.findall(link_pattern, html, re.IGNORECASE)
|
||||
|
||||
for url, text in matches:
|
||||
links.append({
|
||||
'text': text.strip(),
|
||||
'url': url.strip()
|
||||
})
|
||||
|
||||
logger.debug(f"提取到 {len(links)} 个链接")
|
||||
return links
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"提取链接失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def extract_images(self, html: str) -> List[Dict[str, str]]:
|
||||
"""
|
||||
从HTML中提取图片
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
图片列表,每个图片包含 'src' 和 'alt'
|
||||
"""
|
||||
images = []
|
||||
try:
|
||||
# 简单的正则表达式匹配图片
|
||||
img_pattern = r'<img\s+[^>]*src=["\']([^"\']+)["\'][^>]*alt=["\']([^"\']*)["\'][^>]*>'
|
||||
matches = re.findall(img_pattern, html, re.IGNORECASE)
|
||||
|
||||
for src, alt in matches:
|
||||
images.append({
|
||||
'src': src.strip(),
|
||||
'alt': alt.strip()
|
||||
})
|
||||
|
||||
logger.debug(f"提取到 {len(images)} 张图片")
|
||||
return images
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"提取图片失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def extract_tables(self, html: str) -> List[List[List[str]]]:
|
||||
"""
|
||||
从HTML中提取表格数据
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
表格列表,每个表格是二维列表
|
||||
"""
|
||||
tables = []
|
||||
try:
|
||||
# 简单的表格提取(仅支持简单表格)
|
||||
table_pattern = r'<table[^>]*>(.*?)</table>'
|
||||
table_matches = re.findall(table_pattern, html, re.IGNORECASE | re.DOTALL)
|
||||
|
||||
for table_html in table_matches:
|
||||
rows = []
|
||||
# 提取行
|
||||
row_pattern = r'<tr[^>]*>(.*?)</tr>'
|
||||
row_matches = re.findall(row_pattern, table_html, re.IGNORECASE | re.DOTALL)
|
||||
|
||||
for row_html in row_matches:
|
||||
cells = []
|
||||
# 提取单元格
|
||||
cell_pattern = r'<t[dh][^>]*>(.*?)</t[dh]>'
|
||||
cell_matches = re.findall(cell_pattern, row_html, re.IGNORECASE | re.DOTALL)
|
||||
|
||||
for cell_html in cell_matches:
|
||||
# 清理单元格内容
|
||||
cell_text = re.sub(r'<[^>]+>', '', cell_html)
|
||||
cell_text = re.sub(r'\s+', ' ', cell_text).strip()
|
||||
cells.append(cell_text)
|
||||
|
||||
rows.append(cells)
|
||||
|
||||
if rows: # 只添加非空表格
|
||||
tables.append(rows)
|
||||
|
||||
logger.debug(f"提取到 {len(tables)} 个表格")
|
||||
return tables
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"提取表格失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
def analyze_content(self, html: str) -> Dict[str, any]:
|
||||
"""
|
||||
分析HTML内容
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
内容分析结果
|
||||
"""
|
||||
try:
|
||||
plain_text = self.extract_plain_text(html)
|
||||
links = self.extract_links(html)
|
||||
images = self.extract_images(html)
|
||||
tables = self.extract_tables(html)
|
||||
|
||||
analysis = {
|
||||
'total_length': len(html),
|
||||
'plain_text_length': len(plain_text),
|
||||
'link_count': len(links),
|
||||
'image_count': len(images),
|
||||
'table_count': len(tables),
|
||||
'word_count': len(plain_text.split()),
|
||||
'line_count': plain_text.count('\n') + 1,
|
||||
'has_tables': len(tables) > 0,
|
||||
'has_images': len(images) > 0,
|
||||
'has_links': len(links) > 0
|
||||
}
|
||||
|
||||
logger.info(f"内容分析完成: {analysis}")
|
||||
return analysis
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"内容分析失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# 测试HTML
|
||||
test_html = """
|
||||
<html>
|
||||
<body>
|
||||
<h1>测试页面</h1>
|
||||
<p>这是一个测试页面,包含<a href="https://example.com">链接</a>和图片。</p>
|
||||
<img src="test.jpg" alt="测试图片">
|
||||
<table>
|
||||
<tr><th>标题1</th><th>标题2</th></tr>
|
||||
<tr><td>数据1</td><td>数据2</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
try:
|
||||
parser = HTMLContentParser()
|
||||
|
||||
# 测试纯文本提取
|
||||
text = parser.extract_plain_text(test_html)
|
||||
print(f"纯文本: {text[:100]}...")
|
||||
|
||||
# 测试链接提取
|
||||
links = parser.extract_links(test_html)
|
||||
print(f"链接: {links}")
|
||||
|
||||
# 测试图片提取
|
||||
images = parser.extract_images(test_html)
|
||||
print(f"图片: {images}")
|
||||
|
||||
# 测试表格提取
|
||||
tables = parser.extract_tables(test_html)
|
||||
print(f"表格: {tables}")
|
||||
|
||||
# 测试内容分析
|
||||
analysis = parser.analyze_content(test_html)
|
||||
print(f"内容分析: {analysis}")
|
||||
|
||||
print("所有测试通过")
|
||||
|
||||
except Exception as e:
|
||||
print(f"测试失败: {e}")
|
||||
sys.exit(1)
|
||||
@@ -1,309 +1,309 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTML 文本提取模块
|
||||
改进异常处理和类型提示
|
||||
"""
|
||||
import re
|
||||
from bs4 import BeautifulSoup, Tag, NavigableString
|
||||
from typing import List, Optional, Any, Union
|
||||
import logging
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HTMLTextExtractorError(Exception):
|
||||
"""HTML文本提取错误"""
|
||||
pass
|
||||
|
||||
|
||||
class HTMLTextExtractor:
|
||||
"""HTML 文本提取器 - 保留布局结构"""
|
||||
|
||||
# 块级元素列表
|
||||
BLOCK_TAGS = {
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'section',
|
||||
'table', 'tr', 'td', 'th', 'li', 'ul', 'ol', 'blockquote',
|
||||
'pre', 'hr', 'br', 'tbody', 'thead', 'tfoot'
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""初始化提取器"""
|
||||
self.output_lines: List[str] = []
|
||||
|
||||
def extract(self, html: str) -> str:
|
||||
"""
|
||||
从HTML中提取保留布局的文本
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
格式化的纯文本
|
||||
|
||||
异常:
|
||||
HTMLTextExtractorError: HTML解析失败
|
||||
ValueError: 输入参数无效
|
||||
"""
|
||||
if not html:
|
||||
logger.warning("HTML内容为空")
|
||||
return ''
|
||||
|
||||
if not isinstance(html, str):
|
||||
error_msg = f"HTML参数类型错误,应为字符串,实际为: {type(html)}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
try:
|
||||
logger.debug(f"开始解析HTML,长度: {len(html)} 字符")
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
|
||||
# 移除不需要的元素
|
||||
for tag in soup(["script", "style", "noscript"]):
|
||||
tag.decompose()
|
||||
|
||||
# 移除 Confluence 宏
|
||||
for macro in soup.find_all(attrs={"ac:name": True}):
|
||||
macro.decompose()
|
||||
|
||||
self.output_lines = []
|
||||
|
||||
# 处理 body 或整个文档
|
||||
body = soup.body if soup.body else soup
|
||||
for child in body.children:
|
||||
self._process_node(child)
|
||||
|
||||
# 清理结果
|
||||
result = ''.join(self.output_lines)
|
||||
result = re.sub(r'\n\s*\n\s*\n', '\n\n', result)
|
||||
result = '\n'.join(line.rstrip() for line in result.split('\n'))
|
||||
|
||||
logger.info(f"HTML提取完成,输出长度: {len(result)} 字符")
|
||||
return result.strip()
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"HTML解析失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise HTMLTextExtractorError(error_msg) from e
|
||||
|
||||
def _process_node(self, node: Union[Tag, NavigableString], indent: int = 0,
|
||||
list_context: Optional[tuple] = None) -> None:
|
||||
"""递归处理节点"""
|
||||
if isinstance(node, NavigableString):
|
||||
text = str(node).strip()
|
||||
if text:
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
if self.output_lines and not self.output_lines[-1].endswith('\n'):
|
||||
self.output_lines[-1] += text
|
||||
else:
|
||||
self.output_lines.append(' ' * indent + text)
|
||||
return
|
||||
|
||||
if not isinstance(node, Tag):
|
||||
return
|
||||
|
||||
tag_name = node.name.lower()
|
||||
is_block = tag_name in self.BLOCK_TAGS
|
||||
|
||||
# 块级元素前添加换行
|
||||
if is_block and self.output_lines and not self.output_lines[-1].endswith('\n'):
|
||||
self.output_lines.append('\n')
|
||||
|
||||
# 处理特定标签
|
||||
if tag_name in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
|
||||
try:
|
||||
level = int(tag_name[1])
|
||||
prefix = '#' * level + ' '
|
||||
text = node.get_text().strip()
|
||||
if text:
|
||||
self.output_lines.append(' ' * indent + prefix + text + '\n')
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.warning(f"解析标题标签失败: {tag_name}, 错误: {e}")
|
||||
return
|
||||
|
||||
elif tag_name == 'p':
|
||||
text = node.get_text().strip()
|
||||
if text:
|
||||
self.output_lines.append(' ' * indent + text + '\n')
|
||||
return
|
||||
|
||||
elif tag_name == 'hr':
|
||||
self.output_lines.append(' ' * indent + config.SEPARATOR_CHAR * config.SEPARATOR_LENGTH + '\n')
|
||||
return
|
||||
|
||||
elif tag_name == 'br':
|
||||
self.output_lines.append('\n')
|
||||
return
|
||||
|
||||
elif tag_name == 'table':
|
||||
self._process_table(node, indent)
|
||||
return
|
||||
|
||||
elif tag_name in ('ul', 'ol'):
|
||||
self._process_list(node, indent, tag_name)
|
||||
return
|
||||
|
||||
elif tag_name == 'li':
|
||||
self._process_list_item(node, indent, list_context)
|
||||
return
|
||||
|
||||
elif tag_name == 'a':
|
||||
try:
|
||||
href = node.get('href', '')
|
||||
text = node.get_text().strip()
|
||||
if href and text:
|
||||
self.output_lines.append(f'{text} ({href})')
|
||||
elif text:
|
||||
self.output_lines.append(text)
|
||||
except Exception as e:
|
||||
logger.warning(f"解析链接标签失败: {e}")
|
||||
return
|
||||
|
||||
elif tag_name in ('strong', 'b'):
|
||||
text = node.get_text().strip()
|
||||
if text:
|
||||
self.output_lines.append(f'**{text}**')
|
||||
return
|
||||
|
||||
elif tag_name in ('em', 'i'):
|
||||
text = node.get_text().strip()
|
||||
if text:
|
||||
self.output_lines.append(f'*{text}*')
|
||||
return
|
||||
|
||||
else:
|
||||
# 默认递归处理子元素
|
||||
for child in node.children:
|
||||
self._process_node(child, indent, list_context)
|
||||
|
||||
if is_block and self.output_lines and not self.output_lines[-1].endswith('\n'):
|
||||
self.output_lines.append('\n')
|
||||
|
||||
def _process_table(self, table: Tag, indent: int) -> None:
|
||||
"""处理表格"""
|
||||
try:
|
||||
rows = []
|
||||
for tr in table.find_all('tr'):
|
||||
row = []
|
||||
for td in tr.find_all(['td', 'th']):
|
||||
row.append(td.get_text().strip())
|
||||
if row:
|
||||
rows.append(row)
|
||||
|
||||
if rows:
|
||||
# 计算列宽
|
||||
col_widths = []
|
||||
max_cols = max(len(r) for r in rows)
|
||||
for i in range(max_cols):
|
||||
col_width = max((len(r[i]) if i < len(r) else 0) for r in rows)
|
||||
col_widths.append(col_width)
|
||||
|
||||
for row in rows:
|
||||
line = ' ' * indent
|
||||
for i, cell in enumerate(row):
|
||||
width = col_widths[i] if i < len(col_widths) else 0
|
||||
line += cell.ljust(width) + ' '
|
||||
self.output_lines.append(line.rstrip() + '\n')
|
||||
self.output_lines.append('\n')
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"处理表格失败: {e}")
|
||||
# 降级处理:简单提取表格文本
|
||||
table_text = table.get_text().strip()
|
||||
if table_text:
|
||||
self.output_lines.append(' ' * indent + table_text + '\n')
|
||||
|
||||
def _process_list(self, ul: Tag, indent: int, list_type: str) -> None:
|
||||
"""处理列表"""
|
||||
try:
|
||||
counter = 1 if list_type == 'ol' else None
|
||||
for child in ul.children:
|
||||
if isinstance(child, Tag) and child.name == 'li':
|
||||
ctx = (list_type, counter) if counter else (list_type, 1)
|
||||
self._process_list_item(child, indent, ctx)
|
||||
if counter:
|
||||
counter += 1
|
||||
else:
|
||||
self._process_node(child, indent, (list_type, 1) if not counter else None)
|
||||
except Exception as e:
|
||||
logger.warning(f"处理列表失败: {e}")
|
||||
|
||||
def _process_list_item(self, li: Tag, indent: int, list_context: Optional[tuple]) -> None:
|
||||
"""处理列表项"""
|
||||
try:
|
||||
prefix = ''
|
||||
if list_context:
|
||||
list_type, num = list_context
|
||||
prefix = '• ' if list_type == 'ul' else f'{num}. '
|
||||
|
||||
# 收集直接文本
|
||||
direct_parts = []
|
||||
for child in li.children:
|
||||
if isinstance(child, NavigableString):
|
||||
text = str(child).strip()
|
||||
if text:
|
||||
direct_parts.append(text)
|
||||
elif isinstance(child, Tag) and child.name == 'a':
|
||||
href = child.get('href', '')
|
||||
link_text = child.get_text().strip()
|
||||
if href and link_text:
|
||||
direct_parts.append(f'{link_text} ({href})')
|
||||
|
||||
if direct_parts:
|
||||
self.output_lines.append(' ' * indent + prefix + ' '.join(direct_parts) + '\n')
|
||||
|
||||
# 处理子元素
|
||||
for child in li.children:
|
||||
if isinstance(child, Tag) and child.name != 'a':
|
||||
self._process_node(child, indent + 2, None)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"处理列表项失败: {e}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
extractor = HTMLTextExtractor()
|
||||
|
||||
# 测试正常HTML
|
||||
html = "<h1>标题</h1><p>段落</p><ul><li>项目1</li><li>项目2</li></ul>"
|
||||
try:
|
||||
result = extractor.extract(html)
|
||||
print(f"测试1 - 正常HTML提取结果:\n{result}")
|
||||
except Exception as e:
|
||||
print(f"测试1失败: {e}")
|
||||
|
||||
# 测试空HTML
|
||||
try:
|
||||
result = extractor.extract("")
|
||||
print(f"测试2 - 空HTML提取结果: '{result}'")
|
||||
except Exception as e:
|
||||
print(f"测试2失败: {e}")
|
||||
|
||||
# 测试无效HTML
|
||||
try:
|
||||
result = extractor.extract("<invalid>html")
|
||||
print(f"测试3 - 无效HTML提取结果:\n{result}")
|
||||
except Exception as e:
|
||||
print(f"测试3失败: {e}")
|
||||
|
||||
# 测试表格
|
||||
table_html = """
|
||||
<table>
|
||||
<tr><th>姓名</th><th>年龄</th></tr>
|
||||
<tr><td>张三</td><td>25</td></tr>
|
||||
<tr><td>李四</td><td>30</td></tr>
|
||||
</table>
|
||||
"""
|
||||
try:
|
||||
result = extractor.extract(table_html)
|
||||
print(f"测试4 - 表格提取结果:\n{result}")
|
||||
except Exception as e:
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTML 文本提取模块
|
||||
改进异常处理和类型提示
|
||||
"""
|
||||
import re
|
||||
from bs4 import BeautifulSoup, Tag, NavigableString
|
||||
from typing import List, Optional, Any, Union
|
||||
import logging
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HTMLTextExtractorError(Exception):
|
||||
"""HTML文本提取错误"""
|
||||
pass
|
||||
|
||||
|
||||
class HTMLTextExtractor:
|
||||
"""HTML 文本提取器 - 保留布局结构"""
|
||||
|
||||
# 块级元素列表
|
||||
BLOCK_TAGS = {
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'section',
|
||||
'table', 'tr', 'td', 'th', 'li', 'ul', 'ol', 'blockquote',
|
||||
'pre', 'hr', 'br', 'tbody', 'thead', 'tfoot'
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""初始化提取器"""
|
||||
self.output_lines: List[str] = []
|
||||
|
||||
def extract(self, html: str) -> str:
|
||||
"""
|
||||
从HTML中提取保留布局的文本
|
||||
|
||||
参数:
|
||||
html: HTML字符串
|
||||
|
||||
返回:
|
||||
格式化的纯文本
|
||||
|
||||
异常:
|
||||
HTMLTextExtractorError: HTML解析失败
|
||||
ValueError: 输入参数无效
|
||||
"""
|
||||
if not html:
|
||||
logger.warning("HTML内容为空")
|
||||
return ''
|
||||
|
||||
if not isinstance(html, str):
|
||||
error_msg = f"HTML参数类型错误,应为字符串,实际为: {type(html)}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
try:
|
||||
logger.debug(f"开始解析HTML,长度: {len(html)} 字符")
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
|
||||
# 移除不需要的元素
|
||||
for tag in soup(["script", "style", "noscript"]):
|
||||
tag.decompose()
|
||||
|
||||
# 移除 Confluence 宏
|
||||
for macro in soup.find_all(attrs={"ac:name": True}):
|
||||
macro.decompose()
|
||||
|
||||
self.output_lines = []
|
||||
|
||||
# 处理 body 或整个文档
|
||||
body = soup.body if soup.body else soup
|
||||
for child in body.children:
|
||||
self._process_node(child)
|
||||
|
||||
# 清理结果
|
||||
result = ''.join(self.output_lines)
|
||||
result = re.sub(r'\n\s*\n\s*\n', '\n\n', result)
|
||||
result = '\n'.join(line.rstrip() for line in result.split('\n'))
|
||||
|
||||
logger.info(f"HTML提取完成,输出长度: {len(result)} 字符")
|
||||
return result.strip()
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"HTML解析失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise HTMLTextExtractorError(error_msg) from e
|
||||
|
||||
def _process_node(self, node: Union[Tag, NavigableString], indent: int = 0,
|
||||
list_context: Optional[tuple] = None) -> None:
|
||||
"""递归处理节点"""
|
||||
if isinstance(node, NavigableString):
|
||||
text = str(node).strip()
|
||||
if text:
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
if self.output_lines and not self.output_lines[-1].endswith('\n'):
|
||||
self.output_lines[-1] += text
|
||||
else:
|
||||
self.output_lines.append(' ' * indent + text)
|
||||
return
|
||||
|
||||
if not isinstance(node, Tag):
|
||||
return
|
||||
|
||||
tag_name = node.name.lower()
|
||||
is_block = tag_name in self.BLOCK_TAGS
|
||||
|
||||
# 块级元素前添加换行
|
||||
if is_block and self.output_lines and not self.output_lines[-1].endswith('\n'):
|
||||
self.output_lines.append('\n')
|
||||
|
||||
# 处理特定标签
|
||||
if tag_name in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
|
||||
try:
|
||||
level = int(tag_name[1])
|
||||
prefix = '#' * level + ' '
|
||||
text = node.get_text().strip()
|
||||
if text:
|
||||
self.output_lines.append(' ' * indent + prefix + text + '\n')
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.warning(f"解析标题标签失败: {tag_name}, 错误: {e}")
|
||||
return
|
||||
|
||||
elif tag_name == 'p':
|
||||
text = node.get_text().strip()
|
||||
if text:
|
||||
self.output_lines.append(' ' * indent + text + '\n')
|
||||
return
|
||||
|
||||
elif tag_name == 'hr':
|
||||
self.output_lines.append(' ' * indent + config.SEPARATOR_CHAR * config.SEPARATOR_LENGTH + '\n')
|
||||
return
|
||||
|
||||
elif tag_name == 'br':
|
||||
self.output_lines.append('\n')
|
||||
return
|
||||
|
||||
elif tag_name == 'table':
|
||||
self._process_table(node, indent)
|
||||
return
|
||||
|
||||
elif tag_name in ('ul', 'ol'):
|
||||
self._process_list(node, indent, tag_name)
|
||||
return
|
||||
|
||||
elif tag_name == 'li':
|
||||
self._process_list_item(node, indent, list_context)
|
||||
return
|
||||
|
||||
elif tag_name == 'a':
|
||||
try:
|
||||
href = node.get('href', '')
|
||||
text = node.get_text().strip()
|
||||
if href and text:
|
||||
self.output_lines.append(f'{text} ({href})')
|
||||
elif text:
|
||||
self.output_lines.append(text)
|
||||
except Exception as e:
|
||||
logger.warning(f"解析链接标签失败: {e}")
|
||||
return
|
||||
|
||||
elif tag_name in ('strong', 'b'):
|
||||
text = node.get_text().strip()
|
||||
if text:
|
||||
self.output_lines.append(f'**{text}**')
|
||||
return
|
||||
|
||||
elif tag_name in ('em', 'i'):
|
||||
text = node.get_text().strip()
|
||||
if text:
|
||||
self.output_lines.append(f'*{text}*')
|
||||
return
|
||||
|
||||
else:
|
||||
# 默认递归处理子元素
|
||||
for child in node.children:
|
||||
self._process_node(child, indent, list_context)
|
||||
|
||||
if is_block and self.output_lines and not self.output_lines[-1].endswith('\n'):
|
||||
self.output_lines.append('\n')
|
||||
|
||||
def _process_table(self, table: Tag, indent: int) -> None:
|
||||
"""处理表格"""
|
||||
try:
|
||||
rows = []
|
||||
for tr in table.find_all('tr'):
|
||||
row = []
|
||||
for td in tr.find_all(['td', 'th']):
|
||||
row.append(td.get_text().strip())
|
||||
if row:
|
||||
rows.append(row)
|
||||
|
||||
if rows:
|
||||
# 计算列宽
|
||||
col_widths = []
|
||||
max_cols = max(len(r) for r in rows)
|
||||
for i in range(max_cols):
|
||||
col_width = max((len(r[i]) if i < len(r) else 0) for r in rows)
|
||||
col_widths.append(col_width)
|
||||
|
||||
for row in rows:
|
||||
line = ' ' * indent
|
||||
for i, cell in enumerate(row):
|
||||
width = col_widths[i] if i < len(col_widths) else 0
|
||||
line += cell.ljust(width) + ' '
|
||||
self.output_lines.append(line.rstrip() + '\n')
|
||||
self.output_lines.append('\n')
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"处理表格失败: {e}")
|
||||
# 降级处理:简单提取表格文本
|
||||
table_text = table.get_text().strip()
|
||||
if table_text:
|
||||
self.output_lines.append(' ' * indent + table_text + '\n')
|
||||
|
||||
def _process_list(self, ul: Tag, indent: int, list_type: str) -> None:
|
||||
"""处理列表"""
|
||||
try:
|
||||
counter = 1 if list_type == 'ol' else None
|
||||
for child in ul.children:
|
||||
if isinstance(child, Tag) and child.name == 'li':
|
||||
ctx = (list_type, counter) if counter else (list_type, 1)
|
||||
self._process_list_item(child, indent, ctx)
|
||||
if counter:
|
||||
counter += 1
|
||||
else:
|
||||
self._process_node(child, indent, (list_type, 1) if not counter else None)
|
||||
except Exception as e:
|
||||
logger.warning(f"处理列表失败: {e}")
|
||||
|
||||
def _process_list_item(self, li: Tag, indent: int, list_context: Optional[tuple]) -> None:
|
||||
"""处理列表项"""
|
||||
try:
|
||||
prefix = ''
|
||||
if list_context:
|
||||
list_type, num = list_context
|
||||
prefix = '• ' if list_type == 'ul' else f'{num}. '
|
||||
|
||||
# 收集直接文本
|
||||
direct_parts = []
|
||||
for child in li.children:
|
||||
if isinstance(child, NavigableString):
|
||||
text = str(child).strip()
|
||||
if text:
|
||||
direct_parts.append(text)
|
||||
elif isinstance(child, Tag) and child.name == 'a':
|
||||
href = child.get('href', '')
|
||||
link_text = child.get_text().strip()
|
||||
if href and link_text:
|
||||
direct_parts.append(f'{link_text} ({href})')
|
||||
|
||||
if direct_parts:
|
||||
self.output_lines.append(' ' * indent + prefix + ' '.join(direct_parts) + '\n')
|
||||
|
||||
# 处理子元素
|
||||
for child in li.children:
|
||||
if isinstance(child, Tag) and child.name != 'a':
|
||||
self._process_node(child, indent + 2, None)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"处理列表项失败: {e}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
extractor = HTMLTextExtractor()
|
||||
|
||||
# 测试正常HTML
|
||||
html = "<h1>标题</h1><p>段落</p><ul><li>项目1</li><li>项目2</li></ul>"
|
||||
try:
|
||||
result = extractor.extract(html)
|
||||
print(f"测试1 - 正常HTML提取结果:\n{result}")
|
||||
except Exception as e:
|
||||
print(f"测试1失败: {e}")
|
||||
|
||||
# 测试空HTML
|
||||
try:
|
||||
result = extractor.extract("")
|
||||
print(f"测试2 - 空HTML提取结果: '{result}'")
|
||||
except Exception as e:
|
||||
print(f"测试2失败: {e}")
|
||||
|
||||
# 测试无效HTML
|
||||
try:
|
||||
result = extractor.extract("<invalid>html")
|
||||
print(f"测试3 - 无效HTML提取结果:\n{result}")
|
||||
except Exception as e:
|
||||
print(f"测试3失败: {e}")
|
||||
|
||||
# 测试表格
|
||||
table_html = """
|
||||
<table>
|
||||
<tr><th>姓名</th><th>年龄</th></tr>
|
||||
<tr><td>张三</td><td>25</td></tr>
|
||||
<tr><td>李四</td><td>30</td></tr>
|
||||
</table>
|
||||
"""
|
||||
try:
|
||||
result = extractor.extract(table_html)
|
||||
print(f"测试4 - 表格提取结果:\n{result}")
|
||||
except Exception as e:
|
||||
print(f"测试4失败: {e}")
|
||||
BIN
src/data/daily_logs.db
Normal file
BIN
src/data/daily_logs.db
Normal file
Binary file not shown.
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据库模块包
|
||||
提供统一的数据库接口
|
||||
"""
|
||||
from src.database.base import DatabaseBase, DatabaseConnectionError
|
||||
from src.database.daily_logs import DailyLogsDatabase
|
||||
from src.database.schedules import ScheduleDatabase
|
||||
|
||||
__all__ = [
|
||||
'DatabaseBase',
|
||||
'DatabaseConnectionError',
|
||||
'DailyLogsDatabase',
|
||||
'ScheduleDatabase'
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据库模块包
|
||||
提供统一的数据库接口
|
||||
"""
|
||||
from src.database.base import DatabaseBase, DatabaseConnectionError
|
||||
from src.database.daily_logs import DailyLogsDatabase
|
||||
from src.database.schedules import ScheduleDatabase
|
||||
|
||||
__all__ = [
|
||||
'DatabaseBase',
|
||||
'DatabaseConnectionError',
|
||||
'DailyLogsDatabase',
|
||||
'ScheduleDatabase'
|
||||
]
|
||||
@@ -1,257 +1,257 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据库基类模块
|
||||
提供统一的数据库连接管理和上下文管理器
|
||||
"""
|
||||
import os
|
||||
import sqlite3
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator, Optional, Any
|
||||
from pathlib import Path
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class DatabaseConnectionError(Exception):
|
||||
"""数据库连接错误"""
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseBase:
|
||||
"""数据库基类,提供统一的连接管理"""
|
||||
|
||||
def __init__(self, db_path: Optional[str] = None):
|
||||
"""
|
||||
初始化数据库基类
|
||||
|
||||
参数:
|
||||
db_path: 数据库文件路径,如果为None则使用默认配置
|
||||
"""
|
||||
self.db_path = db_path or config.DATABASE_PATH
|
||||
self._connection: Optional[sqlite3.Connection] = None
|
||||
self._ensure_directory()
|
||||
|
||||
def _ensure_directory(self):
|
||||
"""确保数据库目录存在"""
|
||||
data_dir = os.path.dirname(self.db_path)
|
||||
if data_dir and not os.path.exists(data_dir):
|
||||
os.makedirs(data_dir)
|
||||
logger.info(f"创建数据库目录: {data_dir}")
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
"""
|
||||
创建数据库连接
|
||||
|
||||
返回:
|
||||
sqlite3.Connection 对象
|
||||
|
||||
异常:
|
||||
DatabaseConnectionError: 连接失败时抛出
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
logger.debug(f"数据库连接已建立: {self.db_path}")
|
||||
return conn
|
||||
except sqlite3.Error as e:
|
||||
error_msg = f"数据库连接失败: {self.db_path}, 错误: {e}"
|
||||
logger.error(error_msg)
|
||||
raise DatabaseConnectionError(error_msg) from e
|
||||
|
||||
@contextmanager
|
||||
def get_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
||||
"""
|
||||
获取数据库连接的上下文管理器
|
||||
|
||||
使用示例:
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(...)
|
||||
|
||||
返回:
|
||||
数据库连接对象
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
conn = self._connect()
|
||||
yield conn
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"数据库操作失败: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
logger.debug("数据库连接已关闭")
|
||||
|
||||
def execute_query(self, query: str, params: tuple = ()) -> list:
|
||||
"""
|
||||
执行查询并返回结果
|
||||
|
||||
参数:
|
||||
query: SQL查询语句
|
||||
params: 查询参数
|
||||
|
||||
返回:
|
||||
查询结果列表
|
||||
"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, params)
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def execute_update(self, query: str, params: tuple = ()) -> int:
|
||||
"""
|
||||
执行更新操作
|
||||
|
||||
参数:
|
||||
query: SQL更新语句
|
||||
params: 更新参数
|
||||
|
||||
返回:
|
||||
受影响的行数
|
||||
"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, params)
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
def execute_many(self, query: str, params_list: list) -> int:
|
||||
"""
|
||||
批量执行操作
|
||||
|
||||
参数:
|
||||
query: SQL语句
|
||||
params_list: 参数列表
|
||||
|
||||
返回:
|
||||
受影响的总行数
|
||||
"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.executemany(query, params_list)
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
def table_exists(self, table_name: str) -> bool:
|
||||
"""
|
||||
检查表是否存在
|
||||
|
||||
参数:
|
||||
table_name: 表名
|
||||
|
||||
返回:
|
||||
表是否存在
|
||||
"""
|
||||
query = """
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name=?
|
||||
"""
|
||||
result = self.execute_query(query, (table_name,))
|
||||
return len(result) > 0
|
||||
|
||||
def get_table_info(self, table_name: str) -> list:
|
||||
"""
|
||||
获取表结构信息
|
||||
|
||||
参数:
|
||||
table_name: 表名
|
||||
|
||||
返回:
|
||||
表结构信息列表
|
||||
"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def vacuum(self):
|
||||
"""执行数据库整理"""
|
||||
with self.get_connection() as conn:
|
||||
conn.execute("VACUUM")
|
||||
logger.info("数据库整理完成")
|
||||
|
||||
def backup(self, backup_path: Optional[str] = None):
|
||||
"""
|
||||
备份数据库
|
||||
|
||||
参数:
|
||||
backup_path: 备份文件路径,如果为None则使用默认路径
|
||||
"""
|
||||
if backup_path is None:
|
||||
backup_dir = "backups"
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
timestamp = os.path.getmtime(self.db_path)
|
||||
from datetime import datetime
|
||||
dt = datetime.fromtimestamp(timestamp)
|
||||
backup_path = os.path.join(
|
||||
backup_dir,
|
||||
f"backup_{dt.strftime('%Y%m%d_%H%M%S')}.db"
|
||||
)
|
||||
|
||||
try:
|
||||
with self.get_connection() as src_conn:
|
||||
dest_conn = sqlite3.connect(backup_path)
|
||||
src_conn.backup(dest_conn)
|
||||
dest_conn.close()
|
||||
logger.info(f"数据库备份完成: {backup_path}")
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"数据库备份失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
# 全局数据库连接池(可选,用于高性能场景)
|
||||
class ConnectionPool:
|
||||
"""简单的数据库连接池"""
|
||||
|
||||
def __init__(self, db_path: str, max_connections: int = 5):
|
||||
self.db_path = db_path
|
||||
self.max_connections = max_connections
|
||||
self._connections: list[sqlite3.Connection] = []
|
||||
self._in_use: set[sqlite3.Connection] = set()
|
||||
|
||||
@contextmanager
|
||||
def get_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
||||
"""从连接池获取连接"""
|
||||
conn = None
|
||||
try:
|
||||
if self._connections:
|
||||
conn = self._connections.pop()
|
||||
elif len(self._in_use) < self.max_connections:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
else:
|
||||
raise DatabaseConnectionError("连接池已满")
|
||||
|
||||
self._in_use.add(conn)
|
||||
yield conn
|
||||
finally:
|
||||
if conn:
|
||||
self._in_use.remove(conn)
|
||||
self._connections.append(conn)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试数据库基类
|
||||
db = DatabaseBase()
|
||||
|
||||
# 测试连接
|
||||
with db.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT sqlite_version()")
|
||||
version = cursor.fetchone()[0]
|
||||
print(f"SQLite版本: {version}")
|
||||
|
||||
# 测试查询
|
||||
if db.table_exists("sqlite_master"):
|
||||
print("sqlite_master表存在")
|
||||
|
||||
# 测试备份
|
||||
try:
|
||||
db.backup("test_backup.db")
|
||||
print("备份测试完成")
|
||||
except Exception as e:
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据库基类模块
|
||||
提供统一的数据库连接管理和上下文管理器
|
||||
"""
|
||||
import os
|
||||
import sqlite3
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator, Optional, Any
|
||||
from pathlib import Path
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class DatabaseConnectionError(Exception):
|
||||
"""数据库连接错误"""
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseBase:
|
||||
"""数据库基类,提供统一的连接管理"""
|
||||
|
||||
def __init__(self, db_path: Optional[str] = None):
|
||||
"""
|
||||
初始化数据库基类
|
||||
|
||||
参数:
|
||||
db_path: 数据库文件路径,如果为None则使用默认配置
|
||||
"""
|
||||
self.db_path = db_path or config.DATABASE_PATH
|
||||
self._connection: Optional[sqlite3.Connection] = None
|
||||
self._ensure_directory()
|
||||
|
||||
def _ensure_directory(self):
|
||||
"""确保数据库目录存在"""
|
||||
data_dir = os.path.dirname(self.db_path)
|
||||
if data_dir and not os.path.exists(data_dir):
|
||||
os.makedirs(data_dir)
|
||||
logger.info(f"创建数据库目录: {data_dir}")
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
"""
|
||||
创建数据库连接
|
||||
|
||||
返回:
|
||||
sqlite3.Connection 对象
|
||||
|
||||
异常:
|
||||
DatabaseConnectionError: 连接失败时抛出
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
logger.debug(f"数据库连接已建立: {self.db_path}")
|
||||
return conn
|
||||
except sqlite3.Error as e:
|
||||
error_msg = f"数据库连接失败: {self.db_path}, 错误: {e}"
|
||||
logger.error(error_msg)
|
||||
raise DatabaseConnectionError(error_msg) from e
|
||||
|
||||
@contextmanager
|
||||
def get_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
||||
"""
|
||||
获取数据库连接的上下文管理器
|
||||
|
||||
使用示例:
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(...)
|
||||
|
||||
返回:
|
||||
数据库连接对象
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
conn = self._connect()
|
||||
yield conn
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"数据库操作失败: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
logger.debug("数据库连接已关闭")
|
||||
|
||||
def execute_query(self, query: str, params: tuple = ()) -> list:
|
||||
"""
|
||||
执行查询并返回结果
|
||||
|
||||
参数:
|
||||
query: SQL查询语句
|
||||
params: 查询参数
|
||||
|
||||
返回:
|
||||
查询结果列表
|
||||
"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, params)
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def execute_update(self, query: str, params: tuple = ()) -> int:
|
||||
"""
|
||||
执行更新操作
|
||||
|
||||
参数:
|
||||
query: SQL更新语句
|
||||
params: 更新参数
|
||||
|
||||
返回:
|
||||
受影响的行数
|
||||
"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, params)
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
def execute_many(self, query: str, params_list: list) -> int:
|
||||
"""
|
||||
批量执行操作
|
||||
|
||||
参数:
|
||||
query: SQL语句
|
||||
params_list: 参数列表
|
||||
|
||||
返回:
|
||||
受影响的总行数
|
||||
"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.executemany(query, params_list)
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
def table_exists(self, table_name: str) -> bool:
|
||||
"""
|
||||
检查表是否存在
|
||||
|
||||
参数:
|
||||
table_name: 表名
|
||||
|
||||
返回:
|
||||
表是否存在
|
||||
"""
|
||||
query = """
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name=?
|
||||
"""
|
||||
result = self.execute_query(query, (table_name,))
|
||||
return len(result) > 0
|
||||
|
||||
def get_table_info(self, table_name: str) -> list:
|
||||
"""
|
||||
获取表结构信息
|
||||
|
||||
参数:
|
||||
table_name: 表名
|
||||
|
||||
返回:
|
||||
表结构信息列表
|
||||
"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def vacuum(self):
|
||||
"""执行数据库整理"""
|
||||
with self.get_connection() as conn:
|
||||
conn.execute("VACUUM")
|
||||
logger.info("数据库整理完成")
|
||||
|
||||
def backup(self, backup_path: Optional[str] = None):
|
||||
"""
|
||||
备份数据库
|
||||
|
||||
参数:
|
||||
backup_path: 备份文件路径,如果为None则使用默认路径
|
||||
"""
|
||||
if backup_path is None:
|
||||
backup_dir = "backups"
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
timestamp = os.path.getmtime(self.db_path)
|
||||
from datetime import datetime
|
||||
dt = datetime.fromtimestamp(timestamp)
|
||||
backup_path = os.path.join(
|
||||
backup_dir,
|
||||
f"backup_{dt.strftime('%Y%m%d_%H%M%S')}.db"
|
||||
)
|
||||
|
||||
try:
|
||||
with self.get_connection() as src_conn:
|
||||
dest_conn = sqlite3.connect(backup_path)
|
||||
src_conn.backup(dest_conn)
|
||||
dest_conn.close()
|
||||
logger.info(f"数据库备份完成: {backup_path}")
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"数据库备份失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
# 全局数据库连接池(可选,用于高性能场景)
|
||||
class ConnectionPool:
|
||||
"""简单的数据库连接池"""
|
||||
|
||||
def __init__(self, db_path: str, max_connections: int = 5):
|
||||
self.db_path = db_path
|
||||
self.max_connections = max_connections
|
||||
self._connections: list[sqlite3.Connection] = []
|
||||
self._in_use: set[sqlite3.Connection] = set()
|
||||
|
||||
@contextmanager
|
||||
def get_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
||||
"""从连接池获取连接"""
|
||||
conn = None
|
||||
try:
|
||||
if self._connections:
|
||||
conn = self._connections.pop()
|
||||
elif len(self._in_use) < self.max_connections:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
else:
|
||||
raise DatabaseConnectionError("连接池已满")
|
||||
|
||||
self._in_use.add(conn)
|
||||
yield conn
|
||||
finally:
|
||||
if conn:
|
||||
self._in_use.remove(conn)
|
||||
self._connections.append(conn)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试数据库基类
|
||||
db = DatabaseBase()
|
||||
|
||||
# 测试连接
|
||||
with db.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT sqlite_version()")
|
||||
version = cursor.fetchone()[0]
|
||||
print(f"SQLite版本: {version}")
|
||||
|
||||
# 测试查询
|
||||
if db.table_exists("sqlite_master"):
|
||||
print("sqlite_master表存在")
|
||||
|
||||
# 测试备份
|
||||
try:
|
||||
db.backup("test_backup.db")
|
||||
print("备份测试完成")
|
||||
except Exception as e:
|
||||
print(f"备份测试失败: {e}")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,342 +1,342 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
排班人员数据库模块
|
||||
基于新的数据库基类重构
|
||||
"""
|
||||
import json
|
||||
import hashlib
|
||||
from typing import List, Dict, Optional, Any
|
||||
|
||||
from src.database.base import DatabaseBase
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ScheduleDatabase(DatabaseBase):
|
||||
"""排班人员数据库"""
|
||||
|
||||
def __init__(self, db_path: Optional[str] = None):
|
||||
"""
|
||||
初始化数据库
|
||||
|
||||
参数:
|
||||
db_path: 数据库文件路径,如果为None则使用默认配置
|
||||
"""
|
||||
super().__init__(db_path)
|
||||
self._init_schema()
|
||||
|
||||
def _init_schema(self):
|
||||
"""初始化表结构"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建排班人员表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS schedule_personnel (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date TEXT NOT NULL,
|
||||
day_shift TEXT,
|
||||
night_shift TEXT,
|
||||
day_shift_list TEXT, -- JSON数组
|
||||
night_shift_list TEXT, -- JSON数组
|
||||
sheet_id TEXT,
|
||||
sheet_title TEXT,
|
||||
data_hash TEXT, -- 数据哈希,用于检测更新
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(date)
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建表格版本表(用于检测表格是否有更新)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS sheet_versions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sheet_id TEXT NOT NULL,
|
||||
sheet_title TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL,
|
||||
data_hash TEXT,
|
||||
last_checked_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(sheet_id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建索引
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_schedule_date ON schedule_personnel(date)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_schedule_sheet ON schedule_personnel(sheet_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sheet_versions ON sheet_versions(sheet_id)')
|
||||
|
||||
conn.commit()
|
||||
logger.debug("排班数据库表结构初始化完成")
|
||||
|
||||
def _calculate_hash(self, data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
计算数据哈希值
|
||||
|
||||
参数:
|
||||
data: 数据字典
|
||||
|
||||
返回:
|
||||
MD5哈希值
|
||||
"""
|
||||
data_str = json.dumps(data, sort_keys=True, ensure_ascii=False)
|
||||
return hashlib.md5(data_str.encode('utf-8')).hexdigest()
|
||||
|
||||
def check_sheet_update(self, sheet_id: str, sheet_title: str, revision: int, data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
检查表格是否有更新
|
||||
|
||||
参数:
|
||||
sheet_id: 表格ID
|
||||
sheet_title: 表格标题
|
||||
revision: 表格版本号
|
||||
data: 表格数据
|
||||
|
||||
返回:
|
||||
True: 有更新,需要重新获取
|
||||
False: 无更新,可以使用缓存
|
||||
"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 查询当前版本
|
||||
cursor.execute(
|
||||
'SELECT revision, data_hash FROM sheet_versions WHERE sheet_id = ?',
|
||||
(sheet_id,)
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
|
||||
if not result:
|
||||
# 第一次获取,记录版本
|
||||
data_hash = self._calculate_hash(data)
|
||||
cursor.execute('''
|
||||
INSERT INTO sheet_versions (sheet_id, sheet_title, revision, data_hash, last_checked_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
''', (sheet_id, sheet_title, revision, data_hash))
|
||||
conn.commit()
|
||||
logger.debug(f"首次记录表格版本: {sheet_title} (ID: {sheet_id})")
|
||||
return True
|
||||
|
||||
# 检查版本号或数据是否有变化
|
||||
old_revision = result['revision']
|
||||
old_hash = result['data_hash']
|
||||
new_hash = self._calculate_hash(data)
|
||||
|
||||
if old_revision != revision or old_hash != new_hash:
|
||||
# 有更新,更新版本信息
|
||||
cursor.execute('''
|
||||
UPDATE sheet_versions
|
||||
SET revision = ?, data_hash = ?, last_checked_at = CURRENT_TIMESTAMP
|
||||
WHERE sheet_id = ?
|
||||
''', (revision, new_hash, sheet_id))
|
||||
conn.commit()
|
||||
logger.info(f"表格有更新: {sheet_title} (ID: {sheet_id})")
|
||||
return True
|
||||
|
||||
# 无更新,更新检查时间
|
||||
cursor.execute('''
|
||||
UPDATE sheet_versions
|
||||
SET last_checked_at = CURRENT_TIMESTAMP
|
||||
WHERE sheet_id = ?
|
||||
''', (sheet_id,))
|
||||
conn.commit()
|
||||
logger.debug(f"表格无更新: {sheet_title} (ID: {sheet_id})")
|
||||
return False
|
||||
|
||||
def save_schedule(self, date: str, schedule_data: Dict[str, Any],
|
||||
sheet_id: Optional[str] = None, sheet_title: Optional[str] = None) -> bool:
|
||||
"""
|
||||
保存排班信息到数据库
|
||||
|
||||
参数:
|
||||
date: 日期 (YYYY-MM-DD)
|
||||
schedule_data: 排班数据
|
||||
sheet_id: 表格ID
|
||||
sheet_title: 表格标题
|
||||
|
||||
返回:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
# 准备数据
|
||||
day_shift = schedule_data.get('day_shift', '')
|
||||
night_shift = schedule_data.get('night_shift', '')
|
||||
day_shift_list = json.dumps(schedule_data.get('day_shift_list', []), ensure_ascii=False)
|
||||
night_shift_list = json.dumps(schedule_data.get('night_shift_list', []), ensure_ascii=False)
|
||||
data_hash = self._calculate_hash(schedule_data)
|
||||
|
||||
# 使用 INSERT OR REPLACE 来更新已存在的记录
|
||||
query = '''
|
||||
INSERT OR REPLACE INTO schedule_personnel
|
||||
(date, day_shift, night_shift, day_shift_list, night_shift_list,
|
||||
sheet_id, sheet_title, data_hash, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
'''
|
||||
params = (
|
||||
date, day_shift, night_shift, day_shift_list, night_shift_list,
|
||||
sheet_id, sheet_title, data_hash
|
||||
)
|
||||
|
||||
self.execute_update(query, params)
|
||||
logger.debug(f"保存排班信息: {date}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存排班信息失败: {e}, 日期: {date}")
|
||||
return False
|
||||
|
||||
def get_schedule(self, date: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取指定日期的排班信息
|
||||
|
||||
参数:
|
||||
date: 日期 (YYYY-MM-DD)
|
||||
|
||||
返回:
|
||||
排班信息字典,未找到返回None
|
||||
"""
|
||||
query = 'SELECT * FROM schedule_personnel WHERE date = ?'
|
||||
result = self.execute_query(query, (date,))
|
||||
|
||||
if not result:
|
||||
return None
|
||||
|
||||
row = result[0]
|
||||
|
||||
# 解析JSON数组
|
||||
day_shift_list = json.loads(row['day_shift_list']) if row['day_shift_list'] else []
|
||||
night_shift_list = json.loads(row['night_shift_list']) if row['night_shift_list'] else []
|
||||
|
||||
return {
|
||||
'date': row['date'],
|
||||
'day_shift': row['day_shift'],
|
||||
'night_shift': row['night_shift'],
|
||||
'day_shift_list': day_shift_list,
|
||||
'night_shift_list': night_shift_list,
|
||||
'sheet_id': row['sheet_id'],
|
||||
'sheet_title': row['sheet_title'],
|
||||
'updated_at': row['updated_at']
|
||||
}
|
||||
|
||||
def get_schedule_by_range(self, start_date: str, end_date: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取日期范围内的排班信息
|
||||
|
||||
参数:
|
||||
start_date: 开始日期 (YYYY-MM-DD)
|
||||
end_date: 结束日期 (YYYY-MM-DD)
|
||||
|
||||
返回:
|
||||
排班信息列表
|
||||
"""
|
||||
query = '''
|
||||
SELECT * FROM schedule_personnel
|
||||
WHERE date >= ? AND date <= ?
|
||||
ORDER BY date
|
||||
'''
|
||||
results = self.execute_query(query, (start_date, end_date))
|
||||
|
||||
processed_results = []
|
||||
for row in results:
|
||||
day_shift_list = json.loads(row['day_shift_list']) if row['day_shift_list'] else []
|
||||
night_shift_list = json.loads(row['night_shift_list']) if row['night_shift_list'] else []
|
||||
|
||||
processed_results.append({
|
||||
'date': row['date'],
|
||||
'day_shift': row['day_shift'],
|
||||
'night_shift': row['night_shift'],
|
||||
'day_shift_list': day_shift_list,
|
||||
'night_shift_list': night_shift_list,
|
||||
'sheet_id': row['sheet_id'],
|
||||
'sheet_title': row['sheet_title'],
|
||||
'updated_at': row['updated_at']
|
||||
})
|
||||
|
||||
return processed_results
|
||||
|
||||
def delete_old_schedules(self, before_date: str) -> int:
|
||||
"""
|
||||
删除指定日期之前的排班记录
|
||||
|
||||
参数:
|
||||
before_date: 日期 (YYYY-MM-DD)
|
||||
|
||||
返回:
|
||||
删除的记录数
|
||||
"""
|
||||
query = 'DELETE FROM schedule_personnel WHERE date < ?'
|
||||
return self.execute_update(query, (before_date,))
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""获取统计信息"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT COUNT(*) FROM schedule_personnel')
|
||||
total = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute('SELECT MIN(date), MAX(date) FROM schedule_personnel')
|
||||
date_range = cursor.fetchone()
|
||||
|
||||
cursor.execute('SELECT COUNT(DISTINCT sheet_id) FROM schedule_personnel')
|
||||
sheet_count = cursor.fetchone()[0]
|
||||
|
||||
return {
|
||||
'total': total,
|
||||
'date_range': {'start': date_range[0], 'end': date_range[1]},
|
||||
'sheet_count': sheet_count
|
||||
}
|
||||
|
||||
def clear_all(self) -> int:
|
||||
"""
|
||||
清空所有排班数据
|
||||
|
||||
返回:
|
||||
删除的记录数
|
||||
"""
|
||||
query1 = 'DELETE FROM schedule_personnel'
|
||||
query2 = 'DELETE FROM sheet_versions'
|
||||
|
||||
count1 = self.execute_update(query1)
|
||||
count2 = self.execute_update(query2)
|
||||
|
||||
logger.info(f"清空排班数据,删除 {count1} 条排班记录和 {count2} 条版本记录")
|
||||
return count1 + count2
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
db = ScheduleDatabase()
|
||||
|
||||
# 测试保存
|
||||
test_schedule = {
|
||||
'day_shift': '张勤、杨俊豪',
|
||||
'night_shift': '刘炜彬、梁启迟',
|
||||
'day_shift_list': ['张勤', '杨俊豪'],
|
||||
'night_shift_list': ['刘炜彬', '梁启迟']
|
||||
}
|
||||
|
||||
success = db.save_schedule('2025-12-31', test_schedule, 'zcYLIk', '12月')
|
||||
print(f"保存测试: {'成功' if success else '失败'}")
|
||||
|
||||
# 测试获取
|
||||
schedule = db.get_schedule('2025-12-31')
|
||||
print(f"获取结果: {schedule}")
|
||||
|
||||
# 测试范围查询
|
||||
schedules = db.get_schedule_by_range('2025-12-01', '2025-12-31')
|
||||
print(f"范围查询: {len(schedules)} 条记录")
|
||||
|
||||
# 测试统计
|
||||
stats = db.get_stats()
|
||||
print(f"统计信息: {stats}")
|
||||
|
||||
# 测试表格版本检查
|
||||
test_data = {'values': [['姓名', '12月31日'], ['张三', '白']]}
|
||||
needs_update = db.check_sheet_update('test_sheet', '测试表格', 1, test_data)
|
||||
print(f"表格更新检查: {'需要更新' if needs_update else '无需更新'}")
|
||||
|
||||
# 清理测试数据
|
||||
db.delete_old_schedules('2026-01-01')
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
排班人员数据库模块
|
||||
基于新的数据库基类重构
|
||||
"""
|
||||
import json
|
||||
import hashlib
|
||||
from typing import List, Dict, Optional, Any
|
||||
|
||||
from src.database.base import DatabaseBase
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ScheduleDatabase(DatabaseBase):
|
||||
"""排班人员数据库"""
|
||||
|
||||
def __init__(self, db_path: Optional[str] = None):
|
||||
"""
|
||||
初始化数据库
|
||||
|
||||
参数:
|
||||
db_path: 数据库文件路径,如果为None则使用默认配置
|
||||
"""
|
||||
super().__init__(db_path)
|
||||
self._init_schema()
|
||||
|
||||
def _init_schema(self):
|
||||
"""初始化表结构"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建排班人员表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS schedule_personnel (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date TEXT NOT NULL,
|
||||
day_shift TEXT,
|
||||
night_shift TEXT,
|
||||
day_shift_list TEXT, -- JSON数组
|
||||
night_shift_list TEXT, -- JSON数组
|
||||
sheet_id TEXT,
|
||||
sheet_title TEXT,
|
||||
data_hash TEXT, -- 数据哈希,用于检测更新
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(date)
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建表格版本表(用于检测表格是否有更新)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS sheet_versions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sheet_id TEXT NOT NULL,
|
||||
sheet_title TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL,
|
||||
data_hash TEXT,
|
||||
last_checked_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(sheet_id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建索引
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_schedule_date ON schedule_personnel(date)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_schedule_sheet ON schedule_personnel(sheet_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sheet_versions ON sheet_versions(sheet_id)')
|
||||
|
||||
conn.commit()
|
||||
logger.debug("排班数据库表结构初始化完成")
|
||||
|
||||
def _calculate_hash(self, data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
计算数据哈希值
|
||||
|
||||
参数:
|
||||
data: 数据字典
|
||||
|
||||
返回:
|
||||
MD5哈希值
|
||||
"""
|
||||
data_str = json.dumps(data, sort_keys=True, ensure_ascii=False)
|
||||
return hashlib.md5(data_str.encode('utf-8')).hexdigest()
|
||||
|
||||
def check_sheet_update(self, sheet_id: str, sheet_title: str, revision: int, data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
检查表格是否有更新
|
||||
|
||||
参数:
|
||||
sheet_id: 表格ID
|
||||
sheet_title: 表格标题
|
||||
revision: 表格版本号
|
||||
data: 表格数据
|
||||
|
||||
返回:
|
||||
True: 有更新,需要重新获取
|
||||
False: 无更新,可以使用缓存
|
||||
"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 查询当前版本
|
||||
cursor.execute(
|
||||
'SELECT revision, data_hash FROM sheet_versions WHERE sheet_id = ?',
|
||||
(sheet_id,)
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
|
||||
if not result:
|
||||
# 第一次获取,记录版本
|
||||
data_hash = self._calculate_hash(data)
|
||||
cursor.execute('''
|
||||
INSERT INTO sheet_versions (sheet_id, sheet_title, revision, data_hash, last_checked_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
''', (sheet_id, sheet_title, revision, data_hash))
|
||||
conn.commit()
|
||||
logger.debug(f"首次记录表格版本: {sheet_title} (ID: {sheet_id})")
|
||||
return True
|
||||
|
||||
# 检查版本号或数据是否有变化
|
||||
old_revision = result['revision']
|
||||
old_hash = result['data_hash']
|
||||
new_hash = self._calculate_hash(data)
|
||||
|
||||
if old_revision != revision or old_hash != new_hash:
|
||||
# 有更新,更新版本信息
|
||||
cursor.execute('''
|
||||
UPDATE sheet_versions
|
||||
SET revision = ?, data_hash = ?, last_checked_at = CURRENT_TIMESTAMP
|
||||
WHERE sheet_id = ?
|
||||
''', (revision, new_hash, sheet_id))
|
||||
conn.commit()
|
||||
logger.info(f"表格有更新: {sheet_title} (ID: {sheet_id})")
|
||||
return True
|
||||
|
||||
# 无更新,更新检查时间
|
||||
cursor.execute('''
|
||||
UPDATE sheet_versions
|
||||
SET last_checked_at = CURRENT_TIMESTAMP
|
||||
WHERE sheet_id = ?
|
||||
''', (sheet_id,))
|
||||
conn.commit()
|
||||
logger.debug(f"表格无更新: {sheet_title} (ID: {sheet_id})")
|
||||
return False
|
||||
|
||||
def save_schedule(self, date: str, schedule_data: Dict[str, Any],
|
||||
sheet_id: Optional[str] = None, sheet_title: Optional[str] = None) -> bool:
|
||||
"""
|
||||
保存排班信息到数据库
|
||||
|
||||
参数:
|
||||
date: 日期 (YYYY-MM-DD)
|
||||
schedule_data: 排班数据
|
||||
sheet_id: 表格ID
|
||||
sheet_title: 表格标题
|
||||
|
||||
返回:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
# 准备数据
|
||||
day_shift = schedule_data.get('day_shift', '')
|
||||
night_shift = schedule_data.get('night_shift', '')
|
||||
day_shift_list = json.dumps(schedule_data.get('day_shift_list', []), ensure_ascii=False)
|
||||
night_shift_list = json.dumps(schedule_data.get('night_shift_list', []), ensure_ascii=False)
|
||||
data_hash = self._calculate_hash(schedule_data)
|
||||
|
||||
# 使用 INSERT OR REPLACE 来更新已存在的记录
|
||||
query = '''
|
||||
INSERT OR REPLACE INTO schedule_personnel
|
||||
(date, day_shift, night_shift, day_shift_list, night_shift_list,
|
||||
sheet_id, sheet_title, data_hash, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
'''
|
||||
params = (
|
||||
date, day_shift, night_shift, day_shift_list, night_shift_list,
|
||||
sheet_id, sheet_title, data_hash
|
||||
)
|
||||
|
||||
self.execute_update(query, params)
|
||||
logger.debug(f"保存排班信息: {date}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存排班信息失败: {e}, 日期: {date}")
|
||||
return False
|
||||
|
||||
def get_schedule(self, date: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取指定日期的排班信息
|
||||
|
||||
参数:
|
||||
date: 日期 (YYYY-MM-DD)
|
||||
|
||||
返回:
|
||||
排班信息字典,未找到返回None
|
||||
"""
|
||||
query = 'SELECT * FROM schedule_personnel WHERE date = ?'
|
||||
result = self.execute_query(query, (date,))
|
||||
|
||||
if not result:
|
||||
return None
|
||||
|
||||
row = result[0]
|
||||
|
||||
# 解析JSON数组
|
||||
day_shift_list = json.loads(row['day_shift_list']) if row['day_shift_list'] else []
|
||||
night_shift_list = json.loads(row['night_shift_list']) if row['night_shift_list'] else []
|
||||
|
||||
return {
|
||||
'date': row['date'],
|
||||
'day_shift': row['day_shift'],
|
||||
'night_shift': row['night_shift'],
|
||||
'day_shift_list': day_shift_list,
|
||||
'night_shift_list': night_shift_list,
|
||||
'sheet_id': row['sheet_id'],
|
||||
'sheet_title': row['sheet_title'],
|
||||
'updated_at': row['updated_at']
|
||||
}
|
||||
|
||||
def get_schedule_by_range(self, start_date: str, end_date: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取日期范围内的排班信息
|
||||
|
||||
参数:
|
||||
start_date: 开始日期 (YYYY-MM-DD)
|
||||
end_date: 结束日期 (YYYY-MM-DD)
|
||||
|
||||
返回:
|
||||
排班信息列表
|
||||
"""
|
||||
query = '''
|
||||
SELECT * FROM schedule_personnel
|
||||
WHERE date >= ? AND date <= ?
|
||||
ORDER BY date
|
||||
'''
|
||||
results = self.execute_query(query, (start_date, end_date))
|
||||
|
||||
processed_results = []
|
||||
for row in results:
|
||||
day_shift_list = json.loads(row['day_shift_list']) if row['day_shift_list'] else []
|
||||
night_shift_list = json.loads(row['night_shift_list']) if row['night_shift_list'] else []
|
||||
|
||||
processed_results.append({
|
||||
'date': row['date'],
|
||||
'day_shift': row['day_shift'],
|
||||
'night_shift': row['night_shift'],
|
||||
'day_shift_list': day_shift_list,
|
||||
'night_shift_list': night_shift_list,
|
||||
'sheet_id': row['sheet_id'],
|
||||
'sheet_title': row['sheet_title'],
|
||||
'updated_at': row['updated_at']
|
||||
})
|
||||
|
||||
return processed_results
|
||||
|
||||
def delete_old_schedules(self, before_date: str) -> int:
|
||||
"""
|
||||
删除指定日期之前的排班记录
|
||||
|
||||
参数:
|
||||
before_date: 日期 (YYYY-MM-DD)
|
||||
|
||||
返回:
|
||||
删除的记录数
|
||||
"""
|
||||
query = 'DELETE FROM schedule_personnel WHERE date < ?'
|
||||
return self.execute_update(query, (before_date,))
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""获取统计信息"""
|
||||
with self.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT COUNT(*) FROM schedule_personnel')
|
||||
total = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute('SELECT MIN(date), MAX(date) FROM schedule_personnel')
|
||||
date_range = cursor.fetchone()
|
||||
|
||||
cursor.execute('SELECT COUNT(DISTINCT sheet_id) FROM schedule_personnel')
|
||||
sheet_count = cursor.fetchone()[0]
|
||||
|
||||
return {
|
||||
'total': total,
|
||||
'date_range': {'start': date_range[0], 'end': date_range[1]},
|
||||
'sheet_count': sheet_count
|
||||
}
|
||||
|
||||
def clear_all(self) -> int:
|
||||
"""
|
||||
清空所有排班数据
|
||||
|
||||
返回:
|
||||
删除的记录数
|
||||
"""
|
||||
query1 = 'DELETE FROM schedule_personnel'
|
||||
query2 = 'DELETE FROM sheet_versions'
|
||||
|
||||
count1 = self.execute_update(query1)
|
||||
count2 = self.execute_update(query2)
|
||||
|
||||
logger.info(f"清空排班数据,删除 {count1} 条排班记录和 {count2} 条版本记录")
|
||||
return count1 + count2
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
db = ScheduleDatabase()
|
||||
|
||||
# 测试保存
|
||||
test_schedule = {
|
||||
'day_shift': '张勤、杨俊豪',
|
||||
'night_shift': '刘炜彬、梁启迟',
|
||||
'day_shift_list': ['张勤', '杨俊豪'],
|
||||
'night_shift_list': ['刘炜彬', '梁启迟']
|
||||
}
|
||||
|
||||
success = db.save_schedule('2025-12-31', test_schedule, 'zcYLIk', '12月')
|
||||
print(f"保存测试: {'成功' if success else '失败'}")
|
||||
|
||||
# 测试获取
|
||||
schedule = db.get_schedule('2025-12-31')
|
||||
print(f"获取结果: {schedule}")
|
||||
|
||||
# 测试范围查询
|
||||
schedules = db.get_schedule_by_range('2025-12-01', '2025-12-31')
|
||||
print(f"范围查询: {len(schedules)} 条记录")
|
||||
|
||||
# 测试统计
|
||||
stats = db.get_stats()
|
||||
print(f"统计信息: {stats}")
|
||||
|
||||
# 测试表格版本检查
|
||||
test_data = {'values': [['姓名', '12月31日'], ['张三', '白']]}
|
||||
needs_update = db.check_sheet_update('test_sheet', '测试表格', 1, test_data)
|
||||
print(f"表格更新检查: {'需要更新' if needs_update else '无需更新'}")
|
||||
|
||||
# 清理测试数据
|
||||
db.delete_old_schedules('2026-01-01')
|
||||
print("测试数据已清理")
|
||||
348
src/error_handler.py
Normal file
348
src/error_handler.py
Normal file
@@ -0,0 +1,348 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
统一错误处理模块
|
||||
提供自定义异常类和错误处理装饰器
|
||||
"""
|
||||
import logging
|
||||
from functools import wraps
|
||||
from typing import Callable, Any, Optional, Type
|
||||
from datetime import datetime
|
||||
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class OrbitInError(Exception):
|
||||
"""OrbitIn 基础异常类"""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(OrbitInError):
|
||||
"""验证错误"""
|
||||
pass
|
||||
|
||||
|
||||
class ConfigurationError(OrbitInError):
|
||||
"""配置错误"""
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseError(OrbitInError):
|
||||
"""数据库错误"""
|
||||
pass
|
||||
|
||||
|
||||
class NetworkError(OrbitInError):
|
||||
"""网络错误"""
|
||||
pass
|
||||
|
||||
|
||||
class ParsingError(OrbitInError):
|
||||
"""解析错误"""
|
||||
pass
|
||||
|
||||
|
||||
class ReportError(OrbitInError):
|
||||
"""报表生成错误"""
|
||||
pass
|
||||
|
||||
|
||||
def handle_errors(
|
||||
default_return: Any = None,
|
||||
reraise: bool = False,
|
||||
log_level: int = logging.ERROR,
|
||||
exception_types: Optional[tuple] = None
|
||||
) -> Callable:
|
||||
"""
|
||||
错误处理装饰器
|
||||
|
||||
参数:
|
||||
default_return: 发生异常时返回的默认值
|
||||
reraise: 是否重新抛出异常
|
||||
log_level: 日志级别
|
||||
exception_types: 要捕获的异常类型,None表示捕获所有异常
|
||||
|
||||
使用示例:
|
||||
@handle_errors(default_return=None, reraise=False)
|
||||
def my_function():
|
||||
# 可能抛出异常的代码
|
||||
pass
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
# 检查是否需要捕获此异常
|
||||
if exception_types and not isinstance(e, exception_types):
|
||||
raise
|
||||
|
||||
# 记录日志
|
||||
func_name = func.__name__
|
||||
module = func.__module__
|
||||
|
||||
log_message = f"Error in {module}.{func_name}: {str(e)}"
|
||||
|
||||
if log_level == logging.DEBUG:
|
||||
logger.debug(log_message, exc_info=True)
|
||||
elif log_level == logging.INFO:
|
||||
logger.info(log_message, exc_info=True)
|
||||
elif log_level == logging.WARNING:
|
||||
logger.warning(log_message, exc_info=True)
|
||||
elif log_level == logging.ERROR:
|
||||
logger.error(log_message, exc_info=True)
|
||||
elif log_level == logging.CRITICAL:
|
||||
logger.critical(log_message, exc_info=True)
|
||||
else:
|
||||
logger.error(log_message, exc_info=True)
|
||||
|
||||
# 决定是否重新抛出异常
|
||||
if reraise:
|
||||
raise
|
||||
|
||||
return default_return
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def validate_input(
|
||||
date: Optional[str] = None,
|
||||
ship_name: Optional[str] = None,
|
||||
teu: Optional[int] = None,
|
||||
twenty_feet: Optional[int] = None,
|
||||
forty_feet: Optional[int] = None
|
||||
) -> None:
|
||||
"""
|
||||
验证输入参数
|
||||
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||
ship_name: 船名
|
||||
teu: TEU数量
|
||||
twenty_feet: 20尺箱量
|
||||
forty_feet: 40尺箱量
|
||||
|
||||
异常:
|
||||
ValidationError: 验证失败
|
||||
"""
|
||||
if date is not None:
|
||||
if not isinstance(date, str):
|
||||
raise ValidationError(f"日期必须是字符串类型,实际类型: {type(date)}")
|
||||
try:
|
||||
datetime.strptime(date, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise ValidationError(f"日期格式无效: {date},应为 YYYY-MM-DD")
|
||||
|
||||
if ship_name is not None:
|
||||
if not isinstance(ship_name, str):
|
||||
raise ValidationError(f"船名必须是字符串类型,实际类型: {type(ship_name)}")
|
||||
if len(ship_name.strip()) == 0:
|
||||
raise ValidationError("船名不能为空")
|
||||
|
||||
if teu is not None:
|
||||
if not isinstance(teu, int):
|
||||
raise ValidationError(f"TEU必须是整数类型,实际类型: {type(teu)}")
|
||||
if teu < 0:
|
||||
raise ValidationError(f"TEU数量不能为负数: {teu}")
|
||||
|
||||
if twenty_feet is not None:
|
||||
if not isinstance(twenty_feet, int):
|
||||
raise ValidationError(f"20尺箱量必须是整数类型,实际类型: {type(twenty_feet)}")
|
||||
if twenty_feet < 0:
|
||||
raise ValidationError(f"20尺箱量不能为负数: {twenty_feet}")
|
||||
|
||||
if forty_feet is not None:
|
||||
if not isinstance(forty_feet, int):
|
||||
raise ValidationError(f"40尺箱量必须是整数类型,实际类型: {type(forty_feet)}")
|
||||
if forty_feet < 0:
|
||||
raise ValidationError(f"40尺箱量不能为负数: {forty_feet}")
|
||||
|
||||
|
||||
def safe_execute(
|
||||
func: Callable,
|
||||
*args,
|
||||
default_return: Any = None,
|
||||
log_error: bool = True,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
"""
|
||||
安全执行函数
|
||||
|
||||
参数:
|
||||
func: 要执行的函数
|
||||
*args: 位置参数
|
||||
default_return: 发生异常时返回的默认值
|
||||
log_error: 是否记录错误日志
|
||||
**kwargs: 关键字参数
|
||||
|
||||
返回:
|
||||
函数执行结果或默认值
|
||||
|
||||
使用示例:
|
||||
result = safe_execute(
|
||||
some_function,
|
||||
arg1, arg2,
|
||||
default_return=None,
|
||||
log_error=True
|
||||
)
|
||||
"""
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if log_error:
|
||||
func_name = getattr(func, '__name__', 'unknown_function')
|
||||
module = getattr(func, '__module__', 'unknown_module')
|
||||
logger.error(f"Error in {module}.{func_name}: {e}", exc_info=True)
|
||||
return default_return
|
||||
|
||||
|
||||
class ErrorContext:
|
||||
"""错误上下文管理器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
operation_name: str,
|
||||
reraise: bool = False,
|
||||
log_level: int = logging.ERROR
|
||||
):
|
||||
"""
|
||||
初始化错误上下文
|
||||
|
||||
参数:
|
||||
operation_name: 操作名称
|
||||
reraise: 是否重新抛出异常
|
||||
log_level: 日志级别
|
||||
"""
|
||||
self.operation_name = operation_name
|
||||
self.reraise = reraise
|
||||
self.log_level = log_level
|
||||
self.start_time = None
|
||||
|
||||
def __enter__(self):
|
||||
self.start_time = datetime.now()
|
||||
logger.debug(f"开始执行: {self.operation_name}")
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
elapsed = (datetime.now() - self.start_time).total_seconds()
|
||||
|
||||
if exc_type is None:
|
||||
logger.debug(f"成功完成: {self.operation_name} (耗时: {elapsed:.2f}s)")
|
||||
return False
|
||||
|
||||
# 发生异常
|
||||
log_message = f"执行失败: {self.operation_name} (耗时: {elapsed:.2f}s) - {exc_val}"
|
||||
|
||||
if self.log_level == logging.DEBUG:
|
||||
logger.debug(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||
elif self.log_level == logging.INFO:
|
||||
logger.info(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||
elif self.log_level == logging.WARNING:
|
||||
logger.warning(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||
elif self.log_level == logging.ERROR:
|
||||
logger.error(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||
elif self.log_level == logging.CRITICAL:
|
||||
logger.critical(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||
else:
|
||||
logger.error(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||
|
||||
# 决定是否抑制异常
|
||||
return not self.reraise
|
||||
|
||||
|
||||
def convert_exception(
|
||||
from_exception: Type[Exception],
|
||||
to_exception: Type[Exception],
|
||||
message: Optional[str] = None
|
||||
) -> Callable:
|
||||
"""
|
||||
异常转换装饰器
|
||||
|
||||
参数:
|
||||
from_exception: 源异常类型
|
||||
to_exception: 目标异常类型
|
||||
message: 转换时的消息模板,可以使用 {original} 占位符
|
||||
|
||||
使用示例:
|
||||
@convert_exception(ValueError, ValidationError, "验证失败: {original}")
|
||||
def my_function():
|
||||
# 可能抛出 ValueError 的代码
|
||||
pass
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except from_exception as e:
|
||||
if message:
|
||||
new_message = message.format(original=str(e))
|
||||
else:
|
||||
new_message = str(e)
|
||||
raise to_exception(new_message) from e
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def log_execution_time(func: Callable) -> Callable:
|
||||
"""
|
||||
记录函数执行时间的装饰器
|
||||
|
||||
使用示例:
|
||||
@log_execution_time
|
||||
def my_function():
|
||||
# 函数代码
|
||||
pass
|
||||
"""
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
start_time = datetime.now()
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
elapsed = (datetime.now() - start_time).total_seconds()
|
||||
logger.info(f"{func.__name__} 执行成功,耗时: {elapsed:.2f}s")
|
||||
return result
|
||||
except Exception as e:
|
||||
elapsed = (datetime.now() - start_time).total_seconds()
|
||||
logger.error(f"{func.__name__} 执行失败,耗时: {elapsed:.2f}s,错误: {e}")
|
||||
raise
|
||||
return wrapper
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
|
||||
# 测试错误处理装饰器
|
||||
@handle_errors(default_return="error", reraise=False)
|
||||
def test_function():
|
||||
raise ValueError("测试异常")
|
||||
|
||||
result = test_function()
|
||||
print(f"测试函数返回: {result}")
|
||||
|
||||
# 测试输入验证
|
||||
try:
|
||||
validate_input(date="2025-12-30", ship_name="测试船", teu=100)
|
||||
print("输入验证通过")
|
||||
except ValidationError as e:
|
||||
print(f"输入验证失败: {e}")
|
||||
|
||||
# 测试错误上下文管理器
|
||||
try:
|
||||
with ErrorContext("测试操作", reraise=True):
|
||||
print("执行测试操作...")
|
||||
raise RuntimeError("测试错误")
|
||||
except RuntimeError as e:
|
||||
print(f"捕获到错误: {e}")
|
||||
|
||||
# 测试异常转换
|
||||
@convert_exception(ValueError, ValidationError, "验证失败: {original}")
|
||||
def test_convert():
|
||||
raise ValueError("原始错误")
|
||||
|
||||
try:
|
||||
test_convert()
|
||||
except ValidationError as e:
|
||||
print(f"转换后的异常: {e}")
|
||||
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
飞书模块包
|
||||
提供统一的飞书API接口
|
||||
"""
|
||||
from src.feishu.client import FeishuSheetsClient, FeishuClientError
|
||||
from src.feishu.parser import ScheduleDataParser
|
||||
from src.feishu.manager import FeishuScheduleManager
|
||||
|
||||
__all__ = [
|
||||
'FeishuSheetsClient',
|
||||
'FeishuClientError',
|
||||
'ScheduleDataParser',
|
||||
'FeishuScheduleManager'
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
飞书模块包
|
||||
提供统一的飞书API接口
|
||||
"""
|
||||
from src.feishu.client import FeishuSheetsClient, FeishuClientError
|
||||
from src.feishu.parser import ScheduleDataParser
|
||||
from src.feishu.manager import FeishuScheduleManager
|
||||
|
||||
__all__ = [
|
||||
'FeishuSheetsClient',
|
||||
'FeishuClientError',
|
||||
'ScheduleDataParser',
|
||||
'FeishuScheduleManager'
|
||||
]
|
||||
@@ -1,367 +1,367 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
飞书表格 API 客户端模块
|
||||
统一版本,支持月度表格和年度表格
|
||||
支持自动获取和刷新 tenant_access_token
|
||||
"""
|
||||
import requests
|
||||
import time
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class FeishuClientError(Exception):
|
||||
"""飞书客户端异常基类"""
|
||||
pass
|
||||
|
||||
|
||||
class FeishuSheetsClient:
|
||||
"""飞书表格 API 客户端"""
|
||||
|
||||
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None,
|
||||
spreadsheet_token: Optional[str] = None, app_id: Optional[str] = None,
|
||||
app_secret: Optional[str] = None):
|
||||
"""
|
||||
初始化客户端
|
||||
|
||||
参数:
|
||||
base_url: 飞书 API 基础URL,如果为None则使用配置
|
||||
token: Bearer 认证令牌,如果为None则使用配置或自动获取
|
||||
spreadsheet_token: 表格 token,如果为None则使用配置
|
||||
app_id: 飞书应用ID,用于获取tenant_access_token
|
||||
app_secret: 飞书应用密钥,用于获取tenant_access_token
|
||||
"""
|
||||
self.base_url = (base_url or config.FEISHU_BASE_URL).rstrip('/')
|
||||
self.spreadsheet_token = spreadsheet_token or config.FEISHU_SPREADSHEET_TOKEN
|
||||
self.app_id = app_id or config.FEISHU_APP_ID
|
||||
self.app_secret = app_secret or config.FEISHU_APP_SECRET
|
||||
|
||||
# Token管理相关属性
|
||||
self._token = token or config.FEISHU_TOKEN
|
||||
self._token_expire_time = 0 # token过期时间戳
|
||||
self._token_obtained_time = 0 # token获取时间戳
|
||||
|
||||
# 使用 Session 重用连接
|
||||
self.session = requests.Session()
|
||||
self.session.timeout = config.REQUEST_TIMEOUT
|
||||
|
||||
# 初始化headers
|
||||
self._update_session_headers()
|
||||
|
||||
logger.debug(f"飞书客户端初始化完成,基础URL: {self.base_url}")
|
||||
logger.debug(f"使用应用ID: {self.app_id[:8]}... 如果配置" if self.app_id else "未配置应用ID")
|
||||
|
||||
def _update_session_headers(self):
|
||||
"""更新session的headers"""
|
||||
if self._token:
|
||||
self.session.headers.update({
|
||||
'Authorization': f'Bearer {self._token}',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
})
|
||||
else:
|
||||
# 如果没有token,移除Authorization头
|
||||
if 'Authorization' in self.session.headers:
|
||||
del self.session.headers['Authorization']
|
||||
|
||||
def _get_tenant_access_token(self) -> Tuple[str, int]:
|
||||
"""
|
||||
获取tenant_access_token
|
||||
|
||||
返回:
|
||||
(token, expire_time): token字符串和过期时间(秒)
|
||||
|
||||
异常:
|
||||
requests.exceptions.RequestException: 网络请求失败
|
||||
ValueError: API返回错误
|
||||
"""
|
||||
if not self.app_id or not self.app_secret:
|
||||
raise ValueError("未配置飞书应用ID和密钥,无法获取tenant_access_token")
|
||||
|
||||
token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
|
||||
|
||||
payload = {
|
||||
"app_id": self.app_id,
|
||||
"app_secret": self.app_secret
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
}
|
||||
|
||||
try:
|
||||
logger.info(f"正在获取tenant_access_token,应用ID: {self.app_id[:8]}...")
|
||||
response = requests.post(token_url, json=payload, headers=headers, timeout=config.REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data.get('code') != 0:
|
||||
error_msg = f"获取tenant_access_token失败: {data.get('msg')}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
token = data.get('tenant_access_token')
|
||||
expire = data.get('expire', 7200) # 默认2小时
|
||||
|
||||
if not token:
|
||||
raise ValueError("API返回的token为空")
|
||||
|
||||
logger.info(f"成功获取tenant_access_token,有效期: {expire}秒")
|
||||
return token, expire
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取tenant_access_token网络请求失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取tenant_access_token失败: {e}")
|
||||
raise
|
||||
|
||||
def _ensure_valid_token(self):
|
||||
"""
|
||||
确保当前token有效,如果无效则重新获取
|
||||
|
||||
返回:
|
||||
bool: token是否有效
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
# 如果有手动配置的token,直接使用
|
||||
if config.FEISHU_TOKEN and self._token == config.FEISHU_TOKEN:
|
||||
logger.debug("使用手动配置的FEISHU_TOKEN")
|
||||
return True
|
||||
|
||||
# 检查token是否过期(提前30分钟刷新)
|
||||
if self._token and self._token_expire_time > 0:
|
||||
time_remaining = self._token_expire_time - current_time
|
||||
if time_remaining > 1800: # 剩余时间大于30分钟
|
||||
logger.debug(f"token仍然有效,剩余时间: {int(time_remaining)}秒")
|
||||
return True
|
||||
elif time_remaining > 0: # 剩余时间小于30分钟但大于0
|
||||
logger.info(f"token即将过期,剩余时间: {int(time_remaining)}秒,重新获取")
|
||||
else: # 已过期
|
||||
logger.info("token已过期,重新获取")
|
||||
|
||||
# 需要获取新token
|
||||
try:
|
||||
token, expire = self._get_tenant_access_token()
|
||||
self._token = token
|
||||
self._token_obtained_time = current_time
|
||||
self._token_expire_time = current_time + expire
|
||||
self._update_session_headers()
|
||||
logger.info(f"token获取成功,将在 {expire} 秒后过期")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"获取token失败: {e}")
|
||||
# 如果配置了备用token,尝试使用
|
||||
if config.FEISHU_TOKEN and config.FEISHU_TOKEN != self._token:
|
||||
logger.warning("使用备用FEISHU_TOKEN")
|
||||
self._token = config.FEISHU_TOKEN
|
||||
self._update_session_headers()
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_sheets_info(self) -> List[Dict[str, str]]:
|
||||
"""
|
||||
获取所有表格信息(sheet_id 和 title)
|
||||
|
||||
返回:
|
||||
表格信息列表 [{'sheet_id': 'xxx', 'title': 'xxx'}, ...]
|
||||
|
||||
异常:
|
||||
requests.exceptions.RequestException: 网络请求失败
|
||||
ValueError: API返回错误
|
||||
"""
|
||||
# 确保token有效
|
||||
if not self._ensure_valid_token():
|
||||
raise FeishuClientError("无法获取有效的飞书token")
|
||||
|
||||
url = f'{self.base_url}/spreadsheets/{self.spreadsheet_token}/sheets/query'
|
||||
|
||||
try:
|
||||
response = self.session.get(url, timeout=config.REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data.get('code') != 0:
|
||||
error_msg = f"飞书API错误: {data.get('msg')}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
sheets = data.get('data', {}).get('sheets', [])
|
||||
result = []
|
||||
for sheet in sheets:
|
||||
result.append({
|
||||
'sheet_id': sheet.get('sheet_id'),
|
||||
'title': sheet.get('title')
|
||||
})
|
||||
|
||||
logger.info(f"获取到 {len(result)} 个表格")
|
||||
return result
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取表格信息失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"解析表格信息失败: {e}")
|
||||
raise
|
||||
|
||||
def get_sheet_data(self, sheet_id: str, range_: Optional[str] = None) -> Dict:
|
||||
"""
|
||||
获取指定表格的数据
|
||||
|
||||
参数:
|
||||
sheet_id: 表格ID
|
||||
range_: 数据范围,如果为None则使用配置
|
||||
|
||||
返回:
|
||||
飞书API返回的原始数据,包含revision版本号
|
||||
|
||||
异常:
|
||||
requests.exceptions.RequestException: 网络请求失败
|
||||
ValueError: API返回错误
|
||||
"""
|
||||
# 确保token有效
|
||||
if not self._ensure_valid_token():
|
||||
raise FeishuClientError("无法获取有效的飞书token")
|
||||
|
||||
if range_ is None:
|
||||
range_ = config.SHEET_RANGE
|
||||
|
||||
# 注意:获取表格数据使用 v2 API,而不是 v3
|
||||
url = f'{self.base_url.replace("/v3", "/v2")}/spreadsheets/{self.spreadsheet_token}/values/{sheet_id}!{range_}'
|
||||
params = {
|
||||
'valueRenderOption': 'ToString',
|
||||
'dateTimeRenderOption': 'FormattedString'
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.session.get(url, params=params, timeout=config.REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data.get('code') != 0:
|
||||
error_msg = f"飞书API错误: {data.get('msg')}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
logger.debug(f"获取表格数据成功: {sheet_id}, 范围: {range_}")
|
||||
return data.get('data', {})
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取表格数据失败: {e}, sheet_id: {sheet_id}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"解析表格数据失败: {e}, sheet_id: {sheet_id}")
|
||||
raise
|
||||
|
||||
def get_token_info(self) -> Dict[str, any]:
|
||||
"""
|
||||
获取当前token信息
|
||||
|
||||
返回:
|
||||
token信息字典
|
||||
"""
|
||||
current_time = time.time()
|
||||
time_remaining = self._token_expire_time - current_time if self._token_expire_time > 0 else 0
|
||||
|
||||
return {
|
||||
'has_token': bool(self._token),
|
||||
'token_preview': self._token[:20] + '...' if self._token and len(self._token) > 20 else self._token,
|
||||
'token_obtained_time': self._token_obtained_time,
|
||||
'token_expire_time': self._token_expire_time,
|
||||
'time_remaining': max(0, time_remaining),
|
||||
'using_app_credentials': bool(self.app_id and self.app_secret),
|
||||
'using_manual_token': bool(config.FEISHU_TOKEN and self._token == config.FEISHU_TOKEN)
|
||||
}
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""
|
||||
测试飞书连接是否正常
|
||||
|
||||
返回:
|
||||
连接是否正常
|
||||
"""
|
||||
try:
|
||||
# 首先测试token获取
|
||||
if not self._ensure_valid_token():
|
||||
logger.error("无法获取有效的飞书token")
|
||||
return False
|
||||
|
||||
# 然后测试表格访问
|
||||
sheets = self.get_sheets_info()
|
||||
if sheets:
|
||||
logger.info(f"飞书连接测试成功,找到 {len(sheets)} 个表格")
|
||||
return True
|
||||
else:
|
||||
logger.warning("飞书连接测试成功,但未找到表格")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"飞书连接测试失败: {e}")
|
||||
return False
|
||||
|
||||
def refresh_token(self) -> bool:
|
||||
"""
|
||||
强制刷新token
|
||||
|
||||
返回:
|
||||
刷新是否成功
|
||||
"""
|
||||
try:
|
||||
logger.info("强制刷新token...")
|
||||
current_time = time.time()
|
||||
token, expire = self._get_tenant_access_token()
|
||||
self._token = token
|
||||
self._token_obtained_time = current_time
|
||||
self._token_expire_time = current_time + expire
|
||||
self._update_session_headers()
|
||||
logger.info(f"token刷新成功,将在 {expire} 秒后过期")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"强制刷新token失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志级别
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# 测试连接
|
||||
client = FeishuSheetsClient()
|
||||
|
||||
# 显示token信息
|
||||
token_info = client.get_token_info()
|
||||
print("当前token信息:")
|
||||
print(f" 是否有token: {token_info['has_token']}")
|
||||
print(f" token预览: {token_info['token_preview']}")
|
||||
print(f" 剩余时间: {int(token_info['time_remaining'])}秒")
|
||||
print(f" 使用应用凭证: {token_info['using_app_credentials']}")
|
||||
print(f" 使用手动token: {token_info['using_manual_token']}")
|
||||
|
||||
if client.test_connection():
|
||||
print("\n飞书连接测试成功")
|
||||
|
||||
# 获取表格信息
|
||||
sheets = client.get_sheets_info()
|
||||
for sheet in sheets[:3]: # 只显示前3个
|
||||
print(f"表格: {sheet['title']} (ID: {sheet['sheet_id']})")
|
||||
|
||||
if sheets:
|
||||
# 获取第一个表格的数据
|
||||
sheet_id = sheets[0]['sheet_id']
|
||||
data = client.get_sheet_data(sheet_id, 'A1:C5')
|
||||
print(f"获取到表格数据,版本: {data.get('revision', '未知')}")
|
||||
|
||||
# 再次显示token信息
|
||||
token_info = client.get_token_info()
|
||||
print(f"\n测试后token剩余时间: {int(token_info['time_remaining'])}秒")
|
||||
else:
|
||||
print("\n飞书连接测试失败")
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
飞书表格 API 客户端模块
|
||||
统一版本,支持月度表格和年度表格
|
||||
支持自动获取和刷新 tenant_access_token
|
||||
"""
|
||||
import requests
|
||||
import time
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class FeishuClientError(Exception):
|
||||
"""飞书客户端异常基类"""
|
||||
pass
|
||||
|
||||
|
||||
class FeishuSheetsClient:
|
||||
"""飞书表格 API 客户端"""
|
||||
|
||||
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None,
|
||||
spreadsheet_token: Optional[str] = None, app_id: Optional[str] = None,
|
||||
app_secret: Optional[str] = None):
|
||||
"""
|
||||
初始化客户端
|
||||
|
||||
参数:
|
||||
base_url: 飞书 API 基础URL,如果为None则使用配置
|
||||
token: Bearer 认证令牌,如果为None则使用配置或自动获取
|
||||
spreadsheet_token: 表格 token,如果为None则使用配置
|
||||
app_id: 飞书应用ID,用于获取tenant_access_token
|
||||
app_secret: 飞书应用密钥,用于获取tenant_access_token
|
||||
"""
|
||||
self.base_url = (base_url or config.FEISHU_BASE_URL).rstrip('/')
|
||||
self.spreadsheet_token = spreadsheet_token or config.FEISHU_SPREADSHEET_TOKEN
|
||||
self.app_id = app_id or config.FEISHU_APP_ID
|
||||
self.app_secret = app_secret or config.FEISHU_APP_SECRET
|
||||
|
||||
# Token管理相关属性
|
||||
self._token = token or config.FEISHU_TOKEN
|
||||
self._token_expire_time = 0 # token过期时间戳
|
||||
self._token_obtained_time = 0 # token获取时间戳
|
||||
|
||||
# 使用 Session 重用连接
|
||||
self.session = requests.Session()
|
||||
self.session.timeout = config.REQUEST_TIMEOUT
|
||||
|
||||
# 初始化headers
|
||||
self._update_session_headers()
|
||||
|
||||
logger.debug(f"飞书客户端初始化完成,基础URL: {self.base_url}")
|
||||
logger.debug(f"使用应用ID: {self.app_id[:8]}... 如果配置" if self.app_id else "未配置应用ID")
|
||||
|
||||
def _update_session_headers(self):
|
||||
"""更新session的headers"""
|
||||
if self._token:
|
||||
self.session.headers.update({
|
||||
'Authorization': f'Bearer {self._token}',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
})
|
||||
else:
|
||||
# 如果没有token,移除Authorization头
|
||||
if 'Authorization' in self.session.headers:
|
||||
del self.session.headers['Authorization']
|
||||
|
||||
def _get_tenant_access_token(self) -> Tuple[str, int]:
|
||||
"""
|
||||
获取tenant_access_token
|
||||
|
||||
返回:
|
||||
(token, expire_time): token字符串和过期时间(秒)
|
||||
|
||||
异常:
|
||||
requests.exceptions.RequestException: 网络请求失败
|
||||
ValueError: API返回错误
|
||||
"""
|
||||
if not self.app_id or not self.app_secret:
|
||||
raise ValueError("未配置飞书应用ID和密钥,无法获取tenant_access_token")
|
||||
|
||||
token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
|
||||
|
||||
payload = {
|
||||
"app_id": self.app_id,
|
||||
"app_secret": self.app_secret
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
}
|
||||
|
||||
try:
|
||||
logger.info(f"正在获取tenant_access_token,应用ID: {self.app_id[:8]}...")
|
||||
response = requests.post(token_url, json=payload, headers=headers, timeout=config.REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data.get('code') != 0:
|
||||
error_msg = f"获取tenant_access_token失败: {data.get('msg')}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
token = data.get('tenant_access_token')
|
||||
expire = data.get('expire', 7200) # 默认2小时
|
||||
|
||||
if not token:
|
||||
raise ValueError("API返回的token为空")
|
||||
|
||||
logger.info(f"成功获取tenant_access_token,有效期: {expire}秒")
|
||||
return token, expire
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取tenant_access_token网络请求失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取tenant_access_token失败: {e}")
|
||||
raise
|
||||
|
||||
def _ensure_valid_token(self):
|
||||
"""
|
||||
确保当前token有效,如果无效则重新获取
|
||||
|
||||
返回:
|
||||
bool: token是否有效
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
# 如果有手动配置的token,直接使用
|
||||
if config.FEISHU_TOKEN and self._token == config.FEISHU_TOKEN:
|
||||
logger.debug("使用手动配置的FEISHU_TOKEN")
|
||||
return True
|
||||
|
||||
# 检查token是否过期(提前30分钟刷新)
|
||||
if self._token and self._token_expire_time > 0:
|
||||
time_remaining = self._token_expire_time - current_time
|
||||
if time_remaining > 1800: # 剩余时间大于30分钟
|
||||
logger.debug(f"token仍然有效,剩余时间: {int(time_remaining)}秒")
|
||||
return True
|
||||
elif time_remaining > 0: # 剩余时间小于30分钟但大于0
|
||||
logger.info(f"token即将过期,剩余时间: {int(time_remaining)}秒,重新获取")
|
||||
else: # 已过期
|
||||
logger.info("token已过期,重新获取")
|
||||
|
||||
# 需要获取新token
|
||||
try:
|
||||
token, expire = self._get_tenant_access_token()
|
||||
self._token = token
|
||||
self._token_obtained_time = current_time
|
||||
self._token_expire_time = current_time + expire
|
||||
self._update_session_headers()
|
||||
logger.info(f"token获取成功,将在 {expire} 秒后过期")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"获取token失败: {e}")
|
||||
# 如果配置了备用token,尝试使用
|
||||
if config.FEISHU_TOKEN and config.FEISHU_TOKEN != self._token:
|
||||
logger.warning("使用备用FEISHU_TOKEN")
|
||||
self._token = config.FEISHU_TOKEN
|
||||
self._update_session_headers()
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_sheets_info(self) -> List[Dict[str, str]]:
|
||||
"""
|
||||
获取所有表格信息(sheet_id 和 title)
|
||||
|
||||
返回:
|
||||
表格信息列表 [{'sheet_id': 'xxx', 'title': 'xxx'}, ...]
|
||||
|
||||
异常:
|
||||
requests.exceptions.RequestException: 网络请求失败
|
||||
ValueError: API返回错误
|
||||
"""
|
||||
# 确保token有效
|
||||
if not self._ensure_valid_token():
|
||||
raise FeishuClientError("无法获取有效的飞书token")
|
||||
|
||||
url = f'{self.base_url}/spreadsheets/{self.spreadsheet_token}/sheets/query'
|
||||
|
||||
try:
|
||||
response = self.session.get(url, timeout=config.REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data.get('code') != 0:
|
||||
error_msg = f"飞书API错误: {data.get('msg')}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
sheets = data.get('data', {}).get('sheets', [])
|
||||
result = []
|
||||
for sheet in sheets:
|
||||
result.append({
|
||||
'sheet_id': sheet.get('sheet_id'),
|
||||
'title': sheet.get('title')
|
||||
})
|
||||
|
||||
logger.info(f"获取到 {len(result)} 个表格")
|
||||
return result
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取表格信息失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"解析表格信息失败: {e}")
|
||||
raise
|
||||
|
||||
def get_sheet_data(self, sheet_id: str, range_: Optional[str] = None) -> Dict:
|
||||
"""
|
||||
获取指定表格的数据
|
||||
|
||||
参数:
|
||||
sheet_id: 表格ID
|
||||
range_: 数据范围,如果为None则使用配置
|
||||
|
||||
返回:
|
||||
飞书API返回的原始数据,包含revision版本号
|
||||
|
||||
异常:
|
||||
requests.exceptions.RequestException: 网络请求失败
|
||||
ValueError: API返回错误
|
||||
"""
|
||||
# 确保token有效
|
||||
if not self._ensure_valid_token():
|
||||
raise FeishuClientError("无法获取有效的飞书token")
|
||||
|
||||
if range_ is None:
|
||||
range_ = config.SHEET_RANGE
|
||||
|
||||
# 注意:获取表格数据使用 v2 API,而不是 v3
|
||||
url = f'{self.base_url.replace("/v3", "/v2")}/spreadsheets/{self.spreadsheet_token}/values/{sheet_id}!{range_}'
|
||||
params = {
|
||||
'valueRenderOption': 'ToString',
|
||||
'dateTimeRenderOption': 'FormattedString'
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.session.get(url, params=params, timeout=config.REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data.get('code') != 0:
|
||||
error_msg = f"飞书API错误: {data.get('msg')}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
logger.debug(f"获取表格数据成功: {sheet_id}, 范围: {range_}")
|
||||
return data.get('data', {})
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取表格数据失败: {e}, sheet_id: {sheet_id}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"解析表格数据失败: {e}, sheet_id: {sheet_id}")
|
||||
raise
|
||||
|
||||
def get_token_info(self) -> Dict[str, any]:
|
||||
"""
|
||||
获取当前token信息
|
||||
|
||||
返回:
|
||||
token信息字典
|
||||
"""
|
||||
current_time = time.time()
|
||||
time_remaining = self._token_expire_time - current_time if self._token_expire_time > 0 else 0
|
||||
|
||||
return {
|
||||
'has_token': bool(self._token),
|
||||
'token_preview': self._token[:20] + '...' if self._token and len(self._token) > 20 else self._token,
|
||||
'token_obtained_time': self._token_obtained_time,
|
||||
'token_expire_time': self._token_expire_time,
|
||||
'time_remaining': max(0, time_remaining),
|
||||
'using_app_credentials': bool(self.app_id and self.app_secret),
|
||||
'using_manual_token': bool(config.FEISHU_TOKEN and self._token == config.FEISHU_TOKEN)
|
||||
}
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""
|
||||
测试飞书连接是否正常
|
||||
|
||||
返回:
|
||||
连接是否正常
|
||||
"""
|
||||
try:
|
||||
# 首先测试token获取
|
||||
if not self._ensure_valid_token():
|
||||
logger.error("无法获取有效的飞书token")
|
||||
return False
|
||||
|
||||
# 然后测试表格访问
|
||||
sheets = self.get_sheets_info()
|
||||
if sheets:
|
||||
logger.info(f"飞书连接测试成功,找到 {len(sheets)} 个表格")
|
||||
return True
|
||||
else:
|
||||
logger.warning("飞书连接测试成功,但未找到表格")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"飞书连接测试失败: {e}")
|
||||
return False
|
||||
|
||||
def refresh_token(self) -> bool:
|
||||
"""
|
||||
强制刷新token
|
||||
|
||||
返回:
|
||||
刷新是否成功
|
||||
"""
|
||||
try:
|
||||
logger.info("强制刷新token...")
|
||||
current_time = time.time()
|
||||
token, expire = self._get_tenant_access_token()
|
||||
self._token = token
|
||||
self._token_obtained_time = current_time
|
||||
self._token_expire_time = current_time + expire
|
||||
self._update_session_headers()
|
||||
logger.info(f"token刷新成功,将在 {expire} 秒后过期")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"强制刷新token失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志级别
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# 测试连接
|
||||
client = FeishuSheetsClient()
|
||||
|
||||
# 显示token信息
|
||||
token_info = client.get_token_info()
|
||||
print("当前token信息:")
|
||||
print(f" 是否有token: {token_info['has_token']}")
|
||||
print(f" token预览: {token_info['token_preview']}")
|
||||
print(f" 剩余时间: {int(token_info['time_remaining'])}秒")
|
||||
print(f" 使用应用凭证: {token_info['using_app_credentials']}")
|
||||
print(f" 使用手动token: {token_info['using_manual_token']}")
|
||||
|
||||
if client.test_connection():
|
||||
print("\n飞书连接测试成功")
|
||||
|
||||
# 获取表格信息
|
||||
sheets = client.get_sheets_info()
|
||||
for sheet in sheets[:3]: # 只显示前3个
|
||||
print(f"表格: {sheet['title']} (ID: {sheet['sheet_id']})")
|
||||
|
||||
if sheets:
|
||||
# 获取第一个表格的数据
|
||||
sheet_id = sheets[0]['sheet_id']
|
||||
data = client.get_sheet_data(sheet_id, 'A1:C5')
|
||||
print(f"获取到表格数据,版本: {data.get('revision', '未知')}")
|
||||
|
||||
# 再次显示token信息
|
||||
token_info = client.get_token_info()
|
||||
print(f"\n测试后token剩余时间: {int(token_info['time_remaining'])}秒")
|
||||
else:
|
||||
print("\n飞书连接测试失败")
|
||||
sys.exit(1)
|
||||
@@ -1,316 +1,316 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
飞书排班管理器模块
|
||||
统一入口,使用数据库存储和缓存
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
from src.feishu.client import FeishuSheetsClient
|
||||
from src.feishu.parser import ScheduleDataParser
|
||||
from src.database.schedules import ScheduleDatabase
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class FeishuScheduleManager:
|
||||
"""飞书排班管理器(统一入口)"""
|
||||
|
||||
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None,
|
||||
spreadsheet_token: Optional[str] = None, db_path: Optional[str] = None):
|
||||
"""
|
||||
初始化管理器
|
||||
|
||||
参数:
|
||||
base_url: 飞书API基础URL,如果为None则使用配置
|
||||
token: 飞书API令牌,如果为None则使用配置
|
||||
spreadsheet_token: 表格token,如果为None则使用配置
|
||||
db_path: 数据库路径,如果为None则使用配置
|
||||
"""
|
||||
# 检查配置是否完整
|
||||
self._check_config(token, spreadsheet_token)
|
||||
|
||||
# 初始化组件
|
||||
self.client = FeishuSheetsClient(base_url, token, spreadsheet_token)
|
||||
self.parser = ScheduleDataParser()
|
||||
self.db = ScheduleDatabase(db_path)
|
||||
|
||||
logger.info("飞书排班管理器初始化完成")
|
||||
|
||||
def _check_config(self, token: Optional[str], spreadsheet_token: Optional[str]) -> None:
|
||||
"""检查必要配置"""
|
||||
# 检查是否有任何可用的认证方式
|
||||
has_token = bool(token or config.FEISHU_TOKEN)
|
||||
has_app_credentials = bool(config.FEISHU_APP_ID and config.FEISHU_APP_SECRET)
|
||||
|
||||
if not has_token and not has_app_credentials:
|
||||
logger.warning("飞书认证未配置,排班功能将不可用")
|
||||
logger.warning("请配置 FEISHU_TOKEN 或 FEISHU_APP_ID + FEISHU_APP_SECRET")
|
||||
elif has_app_credentials:
|
||||
logger.info("使用飞书应用凭证自动获取token")
|
||||
elif has_token:
|
||||
logger.info("使用手动配置的FEISHU_TOKEN")
|
||||
|
||||
if not spreadsheet_token and not config.FEISHU_SPREADSHEET_TOKEN:
|
||||
logger.warning("飞书表格令牌未配置,排班功能将不可用")
|
||||
|
||||
def _select_sheet_for_date(self, sheets: List[Dict[str, str]], target_year_month: str) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
为指定日期选择最合适的表格
|
||||
|
||||
参数:
|
||||
sheets: 表格列表
|
||||
target_year_month: 目标年月,格式 "2025-12"
|
||||
|
||||
返回:
|
||||
选中的表格信息,未找到返回None
|
||||
"""
|
||||
if not sheets:
|
||||
logger.error("表格列表为空")
|
||||
return None
|
||||
|
||||
# 提取年份和月份
|
||||
try:
|
||||
year = target_year_month[:4]
|
||||
month = target_year_month[5:7].lstrip('0')
|
||||
except (IndexError, ValueError) as e:
|
||||
logger.error(f"解析年月失败: {target_year_month}, 错误: {e}")
|
||||
return None
|
||||
|
||||
# 对于2026年,优先使用年度表格
|
||||
if year == '2026':
|
||||
# 查找年度表格,如 "2026年排班表"
|
||||
year_name = f"{year}年"
|
||||
for sheet in sheets:
|
||||
title = sheet.get('title', '')
|
||||
if year_name in title and '排班表' in title:
|
||||
logger.info(f"找到2026年年度表格: {title}")
|
||||
return sheet
|
||||
|
||||
# 优先查找月份表格,如 "12月"
|
||||
month_name = f"{int(month)}月"
|
||||
for sheet in sheets:
|
||||
title = sheet.get('title', '')
|
||||
if month_name in title:
|
||||
logger.info(f"找到月份表格: {title}")
|
||||
return sheet
|
||||
|
||||
# 查找年度表格,如 "2026年排班表"
|
||||
year_name = f"{year}年"
|
||||
for sheet in sheets:
|
||||
title = sheet.get('title', '')
|
||||
if year_name in title and '排班表' in title:
|
||||
logger.info(f"找到年度表格: {title}")
|
||||
return sheet
|
||||
|
||||
# 如果没有找到匹配的表格,使用第一个表格
|
||||
logger.warning(f"未找到 {target_year_month} 的匹配表格,使用第一个表格: {sheets[0]['title']}")
|
||||
return sheets[0]
|
||||
|
||||
def get_schedule_for_date(self, date_str: str) -> Dict[str, any]:
|
||||
"""
|
||||
获取指定日期的排班信息
|
||||
|
||||
修复:每次都从飞书获取最新数据并覆盖数据库,确保日报中显示最新排班信息
|
||||
|
||||
参数:
|
||||
date_str: 日期字符串,格式 "2025-12-30"
|
||||
|
||||
返回:
|
||||
排班信息字典
|
||||
|
||||
异常:
|
||||
ValueError: 日期格式无效
|
||||
Exception: 其他错误
|
||||
"""
|
||||
try:
|
||||
# 解析日期
|
||||
dt = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
|
||||
# 生成两种格式的日期字符串,用于匹配不同表格
|
||||
target_date_mm_dd = dt.strftime('%m/%d') # "01/01" 用于月度表格
|
||||
target_date_chinese = f"{dt.month}月{dt.day}日" # "1月1日" 用于年度表格
|
||||
target_year_month = dt.strftime('%Y-%m') # "2025-12"
|
||||
|
||||
logger.info(f"获取 {date_str} 的排班信息 (格式: {target_date_mm_dd}/{target_date_chinese})")
|
||||
|
||||
# 1. 获取表格信息
|
||||
sheets = self.client.get_sheets_info()
|
||||
if not sheets:
|
||||
logger.error("未获取到表格信息")
|
||||
return self._empty_result()
|
||||
|
||||
# 2. 选择最合适的表格
|
||||
selected_sheet = self._select_sheet_for_date(sheets, target_year_month)
|
||||
if not selected_sheet:
|
||||
logger.error("未找到合适的表格")
|
||||
return self._empty_result()
|
||||
|
||||
sheet_id = selected_sheet['sheet_id']
|
||||
sheet_title = selected_sheet['title']
|
||||
|
||||
# 3. 获取表格数据
|
||||
sheet_data = self.client.get_sheet_data(sheet_id)
|
||||
if not sheet_data:
|
||||
logger.error("未获取到表格数据")
|
||||
return self._empty_result()
|
||||
|
||||
values = sheet_data.get('valueRange', {}).get('values', [])
|
||||
|
||||
if not values:
|
||||
logger.error("表格数据为空")
|
||||
return self._empty_result()
|
||||
|
||||
# 4. 解析数据 - 根据表格类型选择合适的日期格式
|
||||
# 如果是年度表格,使用中文日期格式;否则使用mm/dd格式
|
||||
if '年' in sheet_title and '排班表' in sheet_title:
|
||||
target_date = target_date_chinese # "1月1日"
|
||||
else:
|
||||
target_date = target_date_mm_dd # "01/01"
|
||||
|
||||
logger.info(f"使用日期格式: {target_date} 解析表格: {sheet_title}")
|
||||
result = self.parser.parse(values, target_date, sheet_title)
|
||||
|
||||
# 5. 每次都保存到数据库,覆盖旧数据,确保人员变动能及时更新
|
||||
if result['day_shift'] or result['night_shift']:
|
||||
self.db.save_schedule(date_str, result, sheet_id, sheet_title)
|
||||
logger.info(f"已更新 {date_str} 的排班信息到数据库: 白班={result['day_shift']}, 夜班={result['night_shift']}")
|
||||
else:
|
||||
logger.warning(f"解析结果为空,{date_str} 未保存到数据库")
|
||||
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"日期格式无效: {date_str}, 错误: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取排班信息失败: {e}")
|
||||
# 降级处理:返回空值
|
||||
return self._empty_result()
|
||||
|
||||
def get_schedule_for_today(self) -> Dict[str, any]:
|
||||
"""获取今天的排班信息"""
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
return self.get_schedule_for_date(today)
|
||||
|
||||
def get_schedule_for_tomorrow(self) -> Dict[str, any]:
|
||||
"""获取明天的排班信息"""
|
||||
tomorrow = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')
|
||||
return self.get_schedule_for_date(tomorrow)
|
||||
|
||||
def refresh_all_schedules(self, days: Optional[int] = None):
|
||||
"""
|
||||
刷新未来指定天数的排班信息
|
||||
|
||||
参数:
|
||||
days: 刷新未来多少天的排班信息,如果为None则使用配置
|
||||
"""
|
||||
if days is None:
|
||||
days = config.SCHEDULE_REFRESH_DAYS
|
||||
|
||||
logger.info(f"开始刷新未来 {days} 天的排班信息")
|
||||
|
||||
today = datetime.now()
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for i in range(days):
|
||||
date = (today + timedelta(days=i)).strftime('%Y-%m-%d')
|
||||
try:
|
||||
logger.debug(f"刷新 {date} 的排班信息...")
|
||||
self.get_schedule_for_date(date)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"刷新 {date} 的排班信息失败: {e}")
|
||||
error_count += 1
|
||||
|
||||
logger.info(f"排班信息刷新完成,成功: {success_count}, 失败: {error_count}")
|
||||
|
||||
def get_schedule_by_range(self, start_date: str, end_date: str) -> List[Dict[str, any]]:
|
||||
"""
|
||||
获取日期范围内的排班信息
|
||||
|
||||
参数:
|
||||
start_date: 开始日期 (YYYY-MM-DD)
|
||||
end_date: 结束日期 (YYYY-MM-DD)
|
||||
|
||||
返回:
|
||||
排班信息列表
|
||||
"""
|
||||
try:
|
||||
# 验证日期格式
|
||||
datetime.strptime(start_date, '%Y-%m-%d')
|
||||
datetime.strptime(end_date, '%Y-%m-%d')
|
||||
|
||||
return self.db.get_schedule_by_range(start_date, end_date)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"日期格式无效: {e}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"获取排班范围失败: {e}")
|
||||
return []
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""测试飞书连接是否正常"""
|
||||
return self.client.test_connection()
|
||||
|
||||
def get_stats(self) -> Dict[str, any]:
|
||||
"""获取排班数据库统计信息"""
|
||||
return self.db.get_stats()
|
||||
|
||||
def _empty_result(self) -> Dict[str, any]:
|
||||
"""返回空结果"""
|
||||
return {
|
||||
'day_shift': '',
|
||||
'night_shift': '',
|
||||
'day_shift_list': [],
|
||||
'night_shift_list': []
|
||||
}
|
||||
|
||||
def _format_db_result(self, db_result: Dict[str, any]) -> Dict[str, any]:
|
||||
"""格式化数据库结果"""
|
||||
return {
|
||||
'day_shift': db_result['day_shift'],
|
||||
'night_shift': db_result['night_shift'],
|
||||
'day_shift_list': db_result['day_shift_list'],
|
||||
'night_shift_list': db_result['night_shift_list']
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# 初始化管理器
|
||||
manager = FeishuScheduleManager()
|
||||
|
||||
# 测试连接
|
||||
if not manager.test_connection():
|
||||
print("飞书连接测试失败")
|
||||
sys.exit(1)
|
||||
|
||||
print("飞书连接测试成功")
|
||||
|
||||
# 测试获取今天和明天的排班
|
||||
today_schedule = manager.get_schedule_for_today()
|
||||
print(f"今天排班: 白班={today_schedule['day_shift']}, 夜班={today_schedule['night_shift']}")
|
||||
|
||||
tomorrow_schedule = manager.get_schedule_for_tomorrow()
|
||||
print(f"明天排班: 白班={tomorrow_schedule['day_shift']}, 夜班={tomorrow_schedule['night_shift']}")
|
||||
|
||||
# 测试统计
|
||||
stats = manager.get_stats()
|
||||
print(f"排班统计: {stats}")
|
||||
|
||||
# 测试范围查询(最近7天)
|
||||
end_date = datetime.now().strftime('%Y-%m-%d')
|
||||
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
|
||||
schedules = manager.get_schedule_by_range(start_date, end_date)
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
飞书排班管理器模块
|
||||
统一入口,使用数据库存储和缓存
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
from src.feishu.client import FeishuSheetsClient
|
||||
from src.feishu.parser import ScheduleDataParser
|
||||
from src.database.schedules import ScheduleDatabase
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class FeishuScheduleManager:
|
||||
"""飞书排班管理器(统一入口)"""
|
||||
|
||||
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None,
|
||||
spreadsheet_token: Optional[str] = None, db_path: Optional[str] = None):
|
||||
"""
|
||||
初始化管理器
|
||||
|
||||
参数:
|
||||
base_url: 飞书API基础URL,如果为None则使用配置
|
||||
token: 飞书API令牌,如果为None则使用配置
|
||||
spreadsheet_token: 表格token,如果为None则使用配置
|
||||
db_path: 数据库路径,如果为None则使用配置
|
||||
"""
|
||||
# 检查配置是否完整
|
||||
self._check_config(token, spreadsheet_token)
|
||||
|
||||
# 初始化组件
|
||||
self.client = FeishuSheetsClient(base_url, token, spreadsheet_token)
|
||||
self.parser = ScheduleDataParser()
|
||||
self.db = ScheduleDatabase(db_path)
|
||||
|
||||
logger.info("飞书排班管理器初始化完成")
|
||||
|
||||
def _check_config(self, token: Optional[str], spreadsheet_token: Optional[str]) -> None:
|
||||
"""检查必要配置"""
|
||||
# 检查是否有任何可用的认证方式
|
||||
has_token = bool(token or config.FEISHU_TOKEN)
|
||||
has_app_credentials = bool(config.FEISHU_APP_ID and config.FEISHU_APP_SECRET)
|
||||
|
||||
if not has_token and not has_app_credentials:
|
||||
logger.warning("飞书认证未配置,排班功能将不可用")
|
||||
logger.warning("请配置 FEISHU_TOKEN 或 FEISHU_APP_ID + FEISHU_APP_SECRET")
|
||||
elif has_app_credentials:
|
||||
logger.info("使用飞书应用凭证自动获取token")
|
||||
elif has_token:
|
||||
logger.info("使用手动配置的FEISHU_TOKEN")
|
||||
|
||||
if not spreadsheet_token and not config.FEISHU_SPREADSHEET_TOKEN:
|
||||
logger.warning("飞书表格令牌未配置,排班功能将不可用")
|
||||
|
||||
def _select_sheet_for_date(self, sheets: List[Dict[str, str]], target_year_month: str) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
为指定日期选择最合适的表格
|
||||
|
||||
参数:
|
||||
sheets: 表格列表
|
||||
target_year_month: 目标年月,格式 "2025-12"
|
||||
|
||||
返回:
|
||||
选中的表格信息,未找到返回None
|
||||
"""
|
||||
if not sheets:
|
||||
logger.error("表格列表为空")
|
||||
return None
|
||||
|
||||
# 提取年份和月份
|
||||
try:
|
||||
year = target_year_month[:4]
|
||||
month = target_year_month[5:7].lstrip('0')
|
||||
except (IndexError, ValueError) as e:
|
||||
logger.error(f"解析年月失败: {target_year_month}, 错误: {e}")
|
||||
return None
|
||||
|
||||
# 对于2026年,优先使用年度表格
|
||||
if year == '2026':
|
||||
# 查找年度表格,如 "2026年排班表"
|
||||
year_name = f"{year}年"
|
||||
for sheet in sheets:
|
||||
title = sheet.get('title', '')
|
||||
if year_name in title and '排班表' in title:
|
||||
logger.info(f"找到2026年年度表格: {title}")
|
||||
return sheet
|
||||
|
||||
# 优先查找月份表格,如 "12月"
|
||||
month_name = f"{int(month)}月"
|
||||
for sheet in sheets:
|
||||
title = sheet.get('title', '')
|
||||
if month_name in title:
|
||||
logger.info(f"找到月份表格: {title}")
|
||||
return sheet
|
||||
|
||||
# 查找年度表格,如 "2026年排班表"
|
||||
year_name = f"{year}年"
|
||||
for sheet in sheets:
|
||||
title = sheet.get('title', '')
|
||||
if year_name in title and '排班表' in title:
|
||||
logger.info(f"找到年度表格: {title}")
|
||||
return sheet
|
||||
|
||||
# 如果没有找到匹配的表格,使用第一个表格
|
||||
logger.warning(f"未找到 {target_year_month} 的匹配表格,使用第一个表格: {sheets[0]['title']}")
|
||||
return sheets[0]
|
||||
|
||||
def get_schedule_for_date(self, date_str: str) -> Dict[str, any]:
|
||||
"""
|
||||
获取指定日期的排班信息
|
||||
|
||||
修复:每次都从飞书获取最新数据并覆盖数据库,确保日报中显示最新排班信息
|
||||
|
||||
参数:
|
||||
date_str: 日期字符串,格式 "2025-12-30"
|
||||
|
||||
返回:
|
||||
排班信息字典
|
||||
|
||||
异常:
|
||||
ValueError: 日期格式无效
|
||||
Exception: 其他错误
|
||||
"""
|
||||
try:
|
||||
# 解析日期
|
||||
dt = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
|
||||
# 生成两种格式的日期字符串,用于匹配不同表格
|
||||
target_date_mm_dd = dt.strftime('%m/%d') # "01/01" 用于月度表格
|
||||
target_date_chinese = f"{dt.month}月{dt.day}日" # "1月1日" 用于年度表格
|
||||
target_year_month = dt.strftime('%Y-%m') # "2025-12"
|
||||
|
||||
logger.info(f"获取 {date_str} 的排班信息 (格式: {target_date_mm_dd}/{target_date_chinese})")
|
||||
|
||||
# 1. 获取表格信息
|
||||
sheets = self.client.get_sheets_info()
|
||||
if not sheets:
|
||||
logger.error("未获取到表格信息")
|
||||
return self._empty_result()
|
||||
|
||||
# 2. 选择最合适的表格
|
||||
selected_sheet = self._select_sheet_for_date(sheets, target_year_month)
|
||||
if not selected_sheet:
|
||||
logger.error("未找到合适的表格")
|
||||
return self._empty_result()
|
||||
|
||||
sheet_id = selected_sheet['sheet_id']
|
||||
sheet_title = selected_sheet['title']
|
||||
|
||||
# 3. 获取表格数据
|
||||
sheet_data = self.client.get_sheet_data(sheet_id)
|
||||
if not sheet_data:
|
||||
logger.error("未获取到表格数据")
|
||||
return self._empty_result()
|
||||
|
||||
values = sheet_data.get('valueRange', {}).get('values', [])
|
||||
|
||||
if not values:
|
||||
logger.error("表格数据为空")
|
||||
return self._empty_result()
|
||||
|
||||
# 4. 解析数据 - 根据表格类型选择合适的日期格式
|
||||
# 如果是年度表格,使用中文日期格式;否则使用mm/dd格式
|
||||
if '年' in sheet_title and '排班表' in sheet_title:
|
||||
target_date = target_date_chinese # "1月1日"
|
||||
else:
|
||||
target_date = target_date_mm_dd # "01/01"
|
||||
|
||||
logger.info(f"使用日期格式: {target_date} 解析表格: {sheet_title}")
|
||||
result = self.parser.parse(values, target_date, sheet_title)
|
||||
|
||||
# 5. 每次都保存到数据库,覆盖旧数据,确保人员变动能及时更新
|
||||
if result['day_shift'] or result['night_shift']:
|
||||
self.db.save_schedule(date_str, result, sheet_id, sheet_title)
|
||||
logger.info(f"已更新 {date_str} 的排班信息到数据库: 白班={result['day_shift']}, 夜班={result['night_shift']}")
|
||||
else:
|
||||
logger.warning(f"解析结果为空,{date_str} 未保存到数据库")
|
||||
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"日期格式无效: {date_str}, 错误: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取排班信息失败: {e}")
|
||||
# 降级处理:返回空值
|
||||
return self._empty_result()
|
||||
|
||||
def get_schedule_for_today(self) -> Dict[str, any]:
|
||||
"""获取今天的排班信息"""
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
return self.get_schedule_for_date(today)
|
||||
|
||||
def get_schedule_for_tomorrow(self) -> Dict[str, any]:
|
||||
"""获取明天的排班信息"""
|
||||
tomorrow = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')
|
||||
return self.get_schedule_for_date(tomorrow)
|
||||
|
||||
def refresh_all_schedules(self, days: Optional[int] = None):
|
||||
"""
|
||||
刷新未来指定天数的排班信息
|
||||
|
||||
参数:
|
||||
days: 刷新未来多少天的排班信息,如果为None则使用配置
|
||||
"""
|
||||
if days is None:
|
||||
days = config.SCHEDULE_REFRESH_DAYS
|
||||
|
||||
logger.info(f"开始刷新未来 {days} 天的排班信息")
|
||||
|
||||
today = datetime.now()
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for i in range(days):
|
||||
date = (today + timedelta(days=i)).strftime('%Y-%m-%d')
|
||||
try:
|
||||
logger.debug(f"刷新 {date} 的排班信息...")
|
||||
self.get_schedule_for_date(date)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"刷新 {date} 的排班信息失败: {e}")
|
||||
error_count += 1
|
||||
|
||||
logger.info(f"排班信息刷新完成,成功: {success_count}, 失败: {error_count}")
|
||||
|
||||
def get_schedule_by_range(self, start_date: str, end_date: str) -> List[Dict[str, any]]:
|
||||
"""
|
||||
获取日期范围内的排班信息
|
||||
|
||||
参数:
|
||||
start_date: 开始日期 (YYYY-MM-DD)
|
||||
end_date: 结束日期 (YYYY-MM-DD)
|
||||
|
||||
返回:
|
||||
排班信息列表
|
||||
"""
|
||||
try:
|
||||
# 验证日期格式
|
||||
datetime.strptime(start_date, '%Y-%m-%d')
|
||||
datetime.strptime(end_date, '%Y-%m-%d')
|
||||
|
||||
return self.db.get_schedule_by_range(start_date, end_date)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"日期格式无效: {e}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"获取排班范围失败: {e}")
|
||||
return []
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""测试飞书连接是否正常"""
|
||||
return self.client.test_connection()
|
||||
|
||||
def get_stats(self) -> Dict[str, any]:
|
||||
"""获取排班数据库统计信息"""
|
||||
return self.db.get_stats()
|
||||
|
||||
def _empty_result(self) -> Dict[str, any]:
|
||||
"""返回空结果"""
|
||||
return {
|
||||
'day_shift': '',
|
||||
'night_shift': '',
|
||||
'day_shift_list': [],
|
||||
'night_shift_list': []
|
||||
}
|
||||
|
||||
def _format_db_result(self, db_result: Dict[str, any]) -> Dict[str, any]:
|
||||
"""格式化数据库结果"""
|
||||
return {
|
||||
'day_shift': db_result['day_shift'],
|
||||
'night_shift': db_result['night_shift'],
|
||||
'day_shift_list': db_result['day_shift_list'],
|
||||
'night_shift_list': db_result['night_shift_list']
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# 初始化管理器
|
||||
manager = FeishuScheduleManager()
|
||||
|
||||
# 测试连接
|
||||
if not manager.test_connection():
|
||||
print("飞书连接测试失败")
|
||||
sys.exit(1)
|
||||
|
||||
print("飞书连接测试成功")
|
||||
|
||||
# 测试获取今天和明天的排班
|
||||
today_schedule = manager.get_schedule_for_today()
|
||||
print(f"今天排班: 白班={today_schedule['day_shift']}, 夜班={today_schedule['night_shift']}")
|
||||
|
||||
tomorrow_schedule = manager.get_schedule_for_tomorrow()
|
||||
print(f"明天排班: 白班={tomorrow_schedule['day_shift']}, 夜班={tomorrow_schedule['night_shift']}")
|
||||
|
||||
# 测试统计
|
||||
stats = manager.get_stats()
|
||||
print(f"排班统计: {stats}")
|
||||
|
||||
# 测试范围查询(最近7天)
|
||||
end_date = datetime.now().strftime('%Y-%m-%d')
|
||||
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
|
||||
schedules = manager.get_schedule_by_range(start_date, end_date)
|
||||
print(f"最近7天排班记录: {len(schedules)} 条")
|
||||
@@ -1,339 +1,339 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
排班数据解析器模块
|
||||
支持月度表格和年度表格解析
|
||||
"""
|
||||
import re
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ScheduleDataParser:
|
||||
"""排班数据解析器(支持月度表格和年度表格)"""
|
||||
|
||||
@staticmethod
|
||||
def _parse_chinese_date(date_str: str) -> Optional[str]:
|
||||
"""
|
||||
解析中文日期格式
|
||||
|
||||
参数:
|
||||
date_str: 中文日期,如 "12月30日" 或 "12/30" 或 "12月1日" 或 "1月1日"
|
||||
|
||||
返回:
|
||||
标准化日期字符串 "M月D日" (不补零)
|
||||
|
||||
异常:
|
||||
ValueError: 日期格式无效
|
||||
"""
|
||||
if not date_str or not isinstance(date_str, str):
|
||||
return None
|
||||
|
||||
date_str = date_str.strip()
|
||||
|
||||
try:
|
||||
# 如果是 "12/30" 格式
|
||||
if '/' in date_str:
|
||||
month, day = date_str.split('/')
|
||||
# 移除可能的空格和前导零
|
||||
month = month.strip().lstrip('0')
|
||||
day = day.strip().lstrip('0')
|
||||
if not month.isdigit() or not day.isdigit():
|
||||
raise ValueError(f"日期格式无效: {date_str}")
|
||||
return f"{int(month)}月{int(day)}日"
|
||||
|
||||
# 如果是 "12月30日" 或 "1月1日" 格式
|
||||
if '月' in date_str and '日' in date_str:
|
||||
# 移除前导零,如 "01月01日" -> "1月1日"
|
||||
parts = date_str.split('月')
|
||||
if len(parts) == 2:
|
||||
month_part = parts[0].lstrip('0')
|
||||
day_part = parts[1].rstrip('日').lstrip('0')
|
||||
if not month_part or not day_part:
|
||||
raise ValueError(f"日期格式无效: {date_str}")
|
||||
return f"{month_part}月{day_part}日"
|
||||
return date_str
|
||||
|
||||
# 如果是 "12月1日" 格式(已经包含"日"字)
|
||||
if '月' in date_str:
|
||||
# 检查是否已经有"日"字
|
||||
if '日' not in date_str:
|
||||
return f"{date_str}日"
|
||||
return date_str
|
||||
|
||||
# 如果是纯数字,尝试解析
|
||||
if date_str.isdigit() and len(date_str) == 4:
|
||||
# 假设是 "1230" 格式
|
||||
month = date_str[:2].lstrip('0')
|
||||
day = date_str[2:].lstrip('0')
|
||||
return f"{month}月{day}日"
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"解析日期失败: {date_str}, 错误: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _find_date_column_index(headers: List[str], target_date: str) -> Optional[int]:
|
||||
"""
|
||||
在表头中查找目标日期对应的列索引
|
||||
|
||||
参数:
|
||||
headers: 表头行 ["姓名", "12月1日", "12月2日", ...]
|
||||
target_date: 目标日期 "12月30日"
|
||||
|
||||
返回:
|
||||
列索引(从0开始),未找到返回None
|
||||
"""
|
||||
if not headers or not target_date:
|
||||
return None
|
||||
|
||||
# 标准化目标日期
|
||||
target_std = ScheduleDataParser._parse_chinese_date(target_date)
|
||||
if not target_std:
|
||||
logger.warning(f"无法标准化目标日期: {target_date}")
|
||||
return None
|
||||
|
||||
# 遍历表头查找匹配的日期
|
||||
for i, header in enumerate(headers):
|
||||
if not header:
|
||||
continue
|
||||
|
||||
header_std = ScheduleDataParser._parse_chinese_date(header)
|
||||
if header_std == target_std:
|
||||
logger.debug(f"找到日期列: {target_date} -> {header} (索引: {i})")
|
||||
return i
|
||||
|
||||
logger.warning(f"未找到日期列: {target_date}, 表头: {headers}")
|
||||
return None
|
||||
|
||||
def parse_monthly_sheet(self, values: List[List[str]], target_date: str) -> Dict[str, any]:
|
||||
"""
|
||||
解析月度表格数据(如12月表格)
|
||||
|
||||
参数:
|
||||
values: 飞书表格返回的二维数组
|
||||
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
||||
|
||||
返回:
|
||||
排班信息字典
|
||||
"""
|
||||
if not values or len(values) < 2:
|
||||
logger.warning("表格数据为空或不足")
|
||||
return self._empty_result()
|
||||
|
||||
# 第一行是表头
|
||||
headers = values[0]
|
||||
date_column_index = self._find_date_column_index(headers, target_date)
|
||||
|
||||
if date_column_index is None:
|
||||
logger.warning(f"未找到日期列: {target_date}")
|
||||
return self._empty_result()
|
||||
|
||||
# 收集白班和夜班人员
|
||||
day_shift_names = []
|
||||
night_shift_names = []
|
||||
|
||||
# 从第二行开始是人员数据
|
||||
for row_idx, row in enumerate(values[1:], start=2):
|
||||
if len(row) <= date_column_index:
|
||||
continue
|
||||
|
||||
name = row[0] if row else ''
|
||||
shift = row[date_column_index] if date_column_index < len(row) else ''
|
||||
|
||||
if not name or not shift:
|
||||
continue
|
||||
|
||||
# 清理班次值
|
||||
shift = shift.strip()
|
||||
if shift == '白':
|
||||
day_shift_names.append(name.strip())
|
||||
elif shift == '夜':
|
||||
night_shift_names.append(name.strip())
|
||||
elif shift: # 其他班次类型
|
||||
logger.debug(f"忽略未知班次类型: {shift} (行: {row_idx})")
|
||||
|
||||
return self._format_result(day_shift_names, night_shift_names)
|
||||
|
||||
def parse_yearly_sheet(self, values: List[List[str]], target_date: str) -> Dict[str, any]:
|
||||
"""
|
||||
解析年度表格数据(如2026年排班表)
|
||||
|
||||
参数:
|
||||
values: 飞书表格返回的二维数组
|
||||
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
||||
|
||||
返回:
|
||||
排班信息字典
|
||||
"""
|
||||
if not values:
|
||||
logger.warning("年度表格数据为空")
|
||||
return self._empty_result()
|
||||
|
||||
# 查找目标月份的数据块
|
||||
target_month = target_date.split('月')[0] if '月' in target_date else ''
|
||||
if not target_month:
|
||||
logger.warning(f"无法从 {target_date} 提取月份")
|
||||
return self._empty_result()
|
||||
|
||||
# 在年度表格中查找对应的月份块
|
||||
current_block_start = -1
|
||||
current_month = ''
|
||||
|
||||
for i, row in enumerate(values):
|
||||
if not row:
|
||||
continue
|
||||
|
||||
first_cell = str(row[0]) if row else ''
|
||||
|
||||
# 检查是否是月份标题行,如 "福州港1月排班表"
|
||||
if '排班表' in first_cell and '月' in first_cell:
|
||||
# 提取月份数字
|
||||
month_match = re.search(r'(\d+)月', first_cell)
|
||||
if month_match:
|
||||
current_month = month_match.group(1).lstrip('0')
|
||||
current_block_start = i
|
||||
logger.debug(f"找到月份块: {current_month}月 (行: {i+1})")
|
||||
|
||||
# 如果找到目标月份,检查下一行是否是表头行
|
||||
if current_month == target_month and i == current_block_start + 1:
|
||||
# 当前行是表头行
|
||||
headers = row
|
||||
date_column_index = self._find_date_column_index(headers, target_date)
|
||||
|
||||
if date_column_index is None:
|
||||
logger.warning(f"在年度表格中未找到日期列: {target_date}")
|
||||
return self._empty_result()
|
||||
|
||||
# 收集人员数据(从表头行的下一行开始)
|
||||
day_shift_names = []
|
||||
night_shift_names = []
|
||||
|
||||
for j in range(i + 1, len(values)):
|
||||
person_row = values[j]
|
||||
if not person_row:
|
||||
# 遇到空行,继续检查下一行
|
||||
continue
|
||||
|
||||
# 检查是否是下一个月份块的开始
|
||||
if person_row[0] and isinstance(person_row[0], str) and '排班表' in person_row[0] and '月' in person_row[0]:
|
||||
break
|
||||
|
||||
# 跳过星期行(第一列为空的行)
|
||||
if not person_row[0]:
|
||||
continue
|
||||
|
||||
if len(person_row) <= date_column_index:
|
||||
continue
|
||||
|
||||
name = person_row[0] if person_row else ''
|
||||
shift = person_row[date_column_index] if date_column_index < len(person_row) else ''
|
||||
|
||||
if not name or not shift:
|
||||
continue
|
||||
|
||||
# 清理班次值
|
||||
shift = shift.strip()
|
||||
if shift == '白':
|
||||
day_shift_names.append(name.strip())
|
||||
elif shift == '夜':
|
||||
night_shift_names.append(name.strip())
|
||||
|
||||
return self._format_result(day_shift_names, night_shift_names)
|
||||
|
||||
logger.warning(f"在年度表格中未找到 {target_month}月 的数据块")
|
||||
return self._empty_result()
|
||||
|
||||
def parse(self, values: List[List[str]], target_date: str, sheet_title: str = '') -> Dict[str, any]:
|
||||
"""
|
||||
解析排班数据,自动判断表格类型
|
||||
|
||||
参数:
|
||||
values: 飞书表格返回的二维数组
|
||||
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
||||
sheet_title: 表格标题,用于判断表格类型
|
||||
|
||||
返回:
|
||||
排班信息字典
|
||||
"""
|
||||
# 根据表格标题判断表格类型
|
||||
if '年' in sheet_title and '排班表' in sheet_title:
|
||||
# 年度表格
|
||||
logger.info(f"使用年度表格解析器: {sheet_title}")
|
||||
return self.parse_yearly_sheet(values, target_date)
|
||||
else:
|
||||
# 月度表格
|
||||
logger.info(f"使用月度表格解析器: {sheet_title}")
|
||||
return self.parse_monthly_sheet(values, target_date)
|
||||
|
||||
def _empty_result(self) -> Dict[str, any]:
|
||||
"""返回空结果"""
|
||||
return {
|
||||
'day_shift': '',
|
||||
'night_shift': '',
|
||||
'day_shift_list': [],
|
||||
'night_shift_list': []
|
||||
}
|
||||
|
||||
def _format_result(self, day_shift_names: List[str], night_shift_names: List[str]) -> Dict[str, any]:
|
||||
"""格式化结果"""
|
||||
# 去重并排序
|
||||
day_shift_names = sorted(set(day_shift_names))
|
||||
night_shift_names = sorted(set(night_shift_names))
|
||||
|
||||
# 格式化输出
|
||||
day_shift_str = '、'.join(day_shift_names) if day_shift_names else ''
|
||||
night_shift_str = '、'.join(night_shift_names) if night_shift_names else ''
|
||||
|
||||
return {
|
||||
'day_shift': day_shift_str,
|
||||
'night_shift': night_shift_str,
|
||||
'day_shift_list': day_shift_names,
|
||||
'night_shift_list': night_shift_names
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
parser = ScheduleDataParser()
|
||||
|
||||
# 测试日期解析
|
||||
test_dates = ["12/30", "12月30日", "1月1日", "01/01", "1230", "无效日期"]
|
||||
for date in test_dates:
|
||||
parsed = parser._parse_chinese_date(date)
|
||||
print(f"解析 '{date}' -> '{parsed}'")
|
||||
|
||||
# 测试月度表格解析
|
||||
monthly_values = [
|
||||
["姓名", "12月1日", "12月2日", "12月3日"],
|
||||
["张三", "白", "夜", ""],
|
||||
["李四", "夜", "白", "白"],
|
||||
["王五", "", "白", "夜"]
|
||||
]
|
||||
|
||||
result = parser.parse_monthly_sheet(monthly_values, "12月2日")
|
||||
print(f"\n月度表格解析结果: {result}")
|
||||
|
||||
# 测试年度表格解析
|
||||
yearly_values = [
|
||||
["福州港2026年排班表"],
|
||||
["姓名", "1月1日", "1月2日", "1月3日"],
|
||||
["张三", "白", "夜", ""],
|
||||
["李四", "夜", "白", "白"],
|
||||
["福州港2月排班表"],
|
||||
["姓名", "2月1日", "2月2日"],
|
||||
["王五", "白", "夜"]
|
||||
]
|
||||
|
||||
result = parser.parse_yearly_sheet(yearly_values, "1月2日")
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
排班数据解析器模块
|
||||
支持月度表格和年度表格解析
|
||||
"""
|
||||
import re
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ScheduleDataParser:
|
||||
"""排班数据解析器(支持月度表格和年度表格)"""
|
||||
|
||||
@staticmethod
|
||||
def _parse_chinese_date(date_str: str) -> Optional[str]:
|
||||
"""
|
||||
解析中文日期格式
|
||||
|
||||
参数:
|
||||
date_str: 中文日期,如 "12月30日" 或 "12/30" 或 "12月1日" 或 "1月1日"
|
||||
|
||||
返回:
|
||||
标准化日期字符串 "M月D日" (不补零)
|
||||
|
||||
异常:
|
||||
ValueError: 日期格式无效
|
||||
"""
|
||||
if not date_str or not isinstance(date_str, str):
|
||||
return None
|
||||
|
||||
date_str = date_str.strip()
|
||||
|
||||
try:
|
||||
# 如果是 "12/30" 格式
|
||||
if '/' in date_str:
|
||||
month, day = date_str.split('/')
|
||||
# 移除可能的空格和前导零
|
||||
month = month.strip().lstrip('0')
|
||||
day = day.strip().lstrip('0')
|
||||
if not month.isdigit() or not day.isdigit():
|
||||
raise ValueError(f"日期格式无效: {date_str}")
|
||||
return f"{int(month)}月{int(day)}日"
|
||||
|
||||
# 如果是 "12月30日" 或 "1月1日" 格式
|
||||
if '月' in date_str and '日' in date_str:
|
||||
# 移除前导零,如 "01月01日" -> "1月1日"
|
||||
parts = date_str.split('月')
|
||||
if len(parts) == 2:
|
||||
month_part = parts[0].lstrip('0')
|
||||
day_part = parts[1].rstrip('日').lstrip('0')
|
||||
if not month_part or not day_part:
|
||||
raise ValueError(f"日期格式无效: {date_str}")
|
||||
return f"{month_part}月{day_part}日"
|
||||
return date_str
|
||||
|
||||
# 如果是 "12月1日" 格式(已经包含"日"字)
|
||||
if '月' in date_str:
|
||||
# 检查是否已经有"日"字
|
||||
if '日' not in date_str:
|
||||
return f"{date_str}日"
|
||||
return date_str
|
||||
|
||||
# 如果是纯数字,尝试解析
|
||||
if date_str.isdigit() and len(date_str) == 4:
|
||||
# 假设是 "1230" 格式
|
||||
month = date_str[:2].lstrip('0')
|
||||
day = date_str[2:].lstrip('0')
|
||||
return f"{month}月{day}日"
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"解析日期失败: {date_str}, 错误: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _find_date_column_index(headers: List[str], target_date: str) -> Optional[int]:
|
||||
"""
|
||||
在表头中查找目标日期对应的列索引
|
||||
|
||||
参数:
|
||||
headers: 表头行 ["姓名", "12月1日", "12月2日", ...]
|
||||
target_date: 目标日期 "12月30日"
|
||||
|
||||
返回:
|
||||
列索引(从0开始),未找到返回None
|
||||
"""
|
||||
if not headers or not target_date:
|
||||
return None
|
||||
|
||||
# 标准化目标日期
|
||||
target_std = ScheduleDataParser._parse_chinese_date(target_date)
|
||||
if not target_std:
|
||||
logger.warning(f"无法标准化目标日期: {target_date}")
|
||||
return None
|
||||
|
||||
# 遍历表头查找匹配的日期
|
||||
for i, header in enumerate(headers):
|
||||
if not header:
|
||||
continue
|
||||
|
||||
header_std = ScheduleDataParser._parse_chinese_date(header)
|
||||
if header_std == target_std:
|
||||
logger.debug(f"找到日期列: {target_date} -> {header} (索引: {i})")
|
||||
return i
|
||||
|
||||
logger.warning(f"未找到日期列: {target_date}, 表头: {headers}")
|
||||
return None
|
||||
|
||||
def parse_monthly_sheet(self, values: List[List[str]], target_date: str) -> Dict[str, any]:
|
||||
"""
|
||||
解析月度表格数据(如12月表格)
|
||||
|
||||
参数:
|
||||
values: 飞书表格返回的二维数组
|
||||
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
||||
|
||||
返回:
|
||||
排班信息字典
|
||||
"""
|
||||
if not values or len(values) < 2:
|
||||
logger.warning("表格数据为空或不足")
|
||||
return self._empty_result()
|
||||
|
||||
# 第一行是表头
|
||||
headers = values[0]
|
||||
date_column_index = self._find_date_column_index(headers, target_date)
|
||||
|
||||
if date_column_index is None:
|
||||
logger.warning(f"未找到日期列: {target_date}")
|
||||
return self._empty_result()
|
||||
|
||||
# 收集白班和夜班人员
|
||||
day_shift_names = []
|
||||
night_shift_names = []
|
||||
|
||||
# 从第二行开始是人员数据
|
||||
for row_idx, row in enumerate(values[1:], start=2):
|
||||
if len(row) <= date_column_index:
|
||||
continue
|
||||
|
||||
name = row[0] if row else ''
|
||||
shift = row[date_column_index] if date_column_index < len(row) else ''
|
||||
|
||||
if not name or not shift:
|
||||
continue
|
||||
|
||||
# 清理班次值
|
||||
shift = shift.strip()
|
||||
if shift == '白':
|
||||
day_shift_names.append(name.strip())
|
||||
elif shift == '夜':
|
||||
night_shift_names.append(name.strip())
|
||||
elif shift: # 其他班次类型
|
||||
logger.debug(f"忽略未知班次类型: {shift} (行: {row_idx})")
|
||||
|
||||
return self._format_result(day_shift_names, night_shift_names)
|
||||
|
||||
def parse_yearly_sheet(self, values: List[List[str]], target_date: str) -> Dict[str, any]:
|
||||
"""
|
||||
解析年度表格数据(如2026年排班表)
|
||||
|
||||
参数:
|
||||
values: 飞书表格返回的二维数组
|
||||
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
||||
|
||||
返回:
|
||||
排班信息字典
|
||||
"""
|
||||
if not values:
|
||||
logger.warning("年度表格数据为空")
|
||||
return self._empty_result()
|
||||
|
||||
# 查找目标月份的数据块
|
||||
target_month = target_date.split('月')[0] if '月' in target_date else ''
|
||||
if not target_month:
|
||||
logger.warning(f"无法从 {target_date} 提取月份")
|
||||
return self._empty_result()
|
||||
|
||||
# 在年度表格中查找对应的月份块
|
||||
current_block_start = -1
|
||||
current_month = ''
|
||||
|
||||
for i, row in enumerate(values):
|
||||
if not row:
|
||||
continue
|
||||
|
||||
first_cell = str(row[0]) if row else ''
|
||||
|
||||
# 检查是否是月份标题行,如 "福州港1月排班表"
|
||||
if '排班表' in first_cell and '月' in first_cell:
|
||||
# 提取月份数字
|
||||
month_match = re.search(r'(\d+)月', first_cell)
|
||||
if month_match:
|
||||
current_month = month_match.group(1).lstrip('0')
|
||||
current_block_start = i
|
||||
logger.debug(f"找到月份块: {current_month}月 (行: {i+1})")
|
||||
|
||||
# 如果找到目标月份,检查下一行是否是表头行
|
||||
if current_month == target_month and i == current_block_start + 1:
|
||||
# 当前行是表头行
|
||||
headers = row
|
||||
date_column_index = self._find_date_column_index(headers, target_date)
|
||||
|
||||
if date_column_index is None:
|
||||
logger.warning(f"在年度表格中未找到日期列: {target_date}")
|
||||
return self._empty_result()
|
||||
|
||||
# 收集人员数据(从表头行的下一行开始)
|
||||
day_shift_names = []
|
||||
night_shift_names = []
|
||||
|
||||
for j in range(i + 1, len(values)):
|
||||
person_row = values[j]
|
||||
if not person_row:
|
||||
# 遇到空行,继续检查下一行
|
||||
continue
|
||||
|
||||
# 检查是否是下一个月份块的开始
|
||||
if person_row[0] and isinstance(person_row[0], str) and '排班表' in person_row[0] and '月' in person_row[0]:
|
||||
break
|
||||
|
||||
# 跳过星期行(第一列为空的行)
|
||||
if not person_row[0]:
|
||||
continue
|
||||
|
||||
if len(person_row) <= date_column_index:
|
||||
continue
|
||||
|
||||
name = person_row[0] if person_row else ''
|
||||
shift = person_row[date_column_index] if date_column_index < len(person_row) else ''
|
||||
|
||||
if not name or not shift:
|
||||
continue
|
||||
|
||||
# 清理班次值
|
||||
shift = shift.strip()
|
||||
if shift == '白':
|
||||
day_shift_names.append(name.strip())
|
||||
elif shift == '夜':
|
||||
night_shift_names.append(name.strip())
|
||||
|
||||
return self._format_result(day_shift_names, night_shift_names)
|
||||
|
||||
logger.warning(f"在年度表格中未找到 {target_month}月 的数据块")
|
||||
return self._empty_result()
|
||||
|
||||
def parse(self, values: List[List[str]], target_date: str, sheet_title: str = '') -> Dict[str, any]:
|
||||
"""
|
||||
解析排班数据,自动判断表格类型
|
||||
|
||||
参数:
|
||||
values: 飞书表格返回的二维数组
|
||||
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
||||
sheet_title: 表格标题,用于判断表格类型
|
||||
|
||||
返回:
|
||||
排班信息字典
|
||||
"""
|
||||
# 根据表格标题判断表格类型
|
||||
if '年' in sheet_title and '排班表' in sheet_title:
|
||||
# 年度表格
|
||||
logger.info(f"使用年度表格解析器: {sheet_title}")
|
||||
return self.parse_yearly_sheet(values, target_date)
|
||||
else:
|
||||
# 月度表格
|
||||
logger.info(f"使用月度表格解析器: {sheet_title}")
|
||||
return self.parse_monthly_sheet(values, target_date)
|
||||
|
||||
def _empty_result(self) -> Dict[str, any]:
|
||||
"""返回空结果"""
|
||||
return {
|
||||
'day_shift': '',
|
||||
'night_shift': '',
|
||||
'day_shift_list': [],
|
||||
'night_shift_list': []
|
||||
}
|
||||
|
||||
def _format_result(self, day_shift_names: List[str], night_shift_names: List[str]) -> Dict[str, any]:
|
||||
"""格式化结果"""
|
||||
# 去重并排序
|
||||
day_shift_names = sorted(set(day_shift_names))
|
||||
night_shift_names = sorted(set(night_shift_names))
|
||||
|
||||
# 格式化输出
|
||||
day_shift_str = '、'.join(day_shift_names) if day_shift_names else ''
|
||||
night_shift_str = '、'.join(night_shift_names) if night_shift_names else ''
|
||||
|
||||
return {
|
||||
'day_shift': day_shift_str,
|
||||
'night_shift': night_shift_str,
|
||||
'day_shift_list': day_shift_names,
|
||||
'night_shift_list': night_shift_names
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
parser = ScheduleDataParser()
|
||||
|
||||
# 测试日期解析
|
||||
test_dates = ["12/30", "12月30日", "1月1日", "01/01", "1230", "无效日期"]
|
||||
for date in test_dates:
|
||||
parsed = parser._parse_chinese_date(date)
|
||||
print(f"解析 '{date}' -> '{parsed}'")
|
||||
|
||||
# 测试月度表格解析
|
||||
monthly_values = [
|
||||
["姓名", "12月1日", "12月2日", "12月3日"],
|
||||
["张三", "白", "夜", ""],
|
||||
["李四", "夜", "白", "白"],
|
||||
["王五", "", "白", "夜"]
|
||||
]
|
||||
|
||||
result = parser.parse_monthly_sheet(monthly_values, "12月2日")
|
||||
print(f"\n月度表格解析结果: {result}")
|
||||
|
||||
# 测试年度表格解析
|
||||
yearly_values = [
|
||||
["福州港2026年排班表"],
|
||||
["姓名", "1月1日", "1月2日", "1月3日"],
|
||||
["张三", "白", "夜", ""],
|
||||
["李四", "夜", "白", "白"],
|
||||
["福州港2月排班表"],
|
||||
["姓名", "2月1日", "2月2日"],
|
||||
["王五", "白", "夜"]
|
||||
]
|
||||
|
||||
result = parser.parse_yearly_sheet(yearly_values, "1月2日")
|
||||
print(f"年度表格解析结果: {result}")
|
||||
3548
src/gui.py
3548
src/gui.py
File diff suppressed because it is too large
Load Diff
@@ -1,172 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
统一日志配置模块
|
||||
提供统一的日志配置,避免各模块自行配置
|
||||
支持按日期分片存储日志
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
||||
from typing import Optional
|
||||
|
||||
from src.config import config
|
||||
|
||||
|
||||
def setup_logging(
|
||||
log_file: Optional[str] = None,
|
||||
console_level: int = logging.INFO,
|
||||
file_level: int = logging.DEBUG,
|
||||
use_date_split: bool = True,
|
||||
date_folder_format: str = "%Y-%m", # 按月份分文件夹
|
||||
max_bytes: int = 10 * 1024 * 1024, # 10MB
|
||||
backup_count: int = 5
|
||||
) -> logging.Logger:
|
||||
"""
|
||||
配置统一的日志系统
|
||||
|
||||
参数:
|
||||
log_file: 日志文件路径,如果为None则使用默认路径
|
||||
console_level: 控制台日志级别
|
||||
file_level: 文件日志级别
|
||||
use_date_split: 是否使用日期分片
|
||||
date_folder_format: 日期文件夹格式(默认按月份,如 logs/2025-12/)
|
||||
max_bytes: 单个日志文件最大大小
|
||||
backup_count: 备份文件数量
|
||||
|
||||
返回:
|
||||
配置好的根日志器
|
||||
"""
|
||||
# 获取当前日期用于构建路径
|
||||
now = datetime.now()
|
||||
|
||||
if log_file is None:
|
||||
log_dir = 'logs'
|
||||
if use_date_split:
|
||||
# 按日期分片:logs/2025-12/2025-12-30.log
|
||||
date_folder = now.strftime(date_folder_format)
|
||||
log_dir = os.path.join('logs', date_folder)
|
||||
log_file = os.path.join(log_dir, now.strftime('%Y-%m-%d.log'))
|
||||
else:
|
||||
log_file = os.path.join(log_dir, 'app.log')
|
||||
else:
|
||||
log_dir = os.path.dirname(log_file)
|
||||
|
||||
if log_dir and not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
# 获取根日志器
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG) # 根日志器设置为最低级别
|
||||
|
||||
# 清除现有handler,避免重复添加
|
||||
logger.handlers.clear()
|
||||
|
||||
# 控制台handler
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(console_level)
|
||||
console_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
console_handler.setFormatter(console_formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# 文件handler(日期分片或大小轮转)
|
||||
if use_date_split:
|
||||
# 使用TimedRotatingFileHandler,每天午夜轮转
|
||||
file_handler = TimedRotatingFileHandler(
|
||||
log_file,
|
||||
when='midnight',
|
||||
interval=1,
|
||||
backupCount=backup_count,
|
||||
encoding='utf-8',
|
||||
atTime=datetime.strptime('00:00:00', '%H:%M:%S')
|
||||
)
|
||||
logger.info(f"日志系统已初始化,使用日期分片: {log_file}")
|
||||
else:
|
||||
# 使用RotatingFileHandler,按大小轮转
|
||||
file_handler = RotatingFileHandler(
|
||||
log_file,
|
||||
maxBytes=max_bytes,
|
||||
backupCount=backup_count,
|
||||
encoding='utf-8'
|
||||
)
|
||||
logger.info(f"日志系统已初始化,使用大小轮转: {log_file}")
|
||||
|
||||
file_handler.setLevel(file_level)
|
||||
file_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# 设置第三方库的日志级别
|
||||
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
||||
logging.getLogger('requests').setLevel(logging.WARNING)
|
||||
|
||||
logger.info(f"控制台日志级别: {logging.getLevelName(console_level)}")
|
||||
logger.info(f"文件日志级别: {logging.getLevelName(file_level)}")
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""
|
||||
获取指定名称的日志器
|
||||
|
||||
参数:
|
||||
name: 日志器名称,通常使用 __name__
|
||||
|
||||
返回:
|
||||
配置好的日志器
|
||||
"""
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
# 自动初始化日志系统
|
||||
if not logging.getLogger().handlers:
|
||||
# 只有在没有handler时才初始化,避免重复初始化
|
||||
setup_logging()
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def info(msg: str, *args, **kwargs):
|
||||
"""记录INFO级别日志"""
|
||||
logging.info(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def warning(msg: str, *args, **kwargs):
|
||||
"""记录WARNING级别日志"""
|
||||
logging.warning(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def error(msg: str, *args, **kwargs):
|
||||
"""记录ERROR级别日志"""
|
||||
logging.error(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def debug(msg: str, *args, **kwargs):
|
||||
"""记录DEBUG级别日志"""
|
||||
logging.debug(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def exception(msg: str, *args, **kwargs):
|
||||
"""记录异常日志"""
|
||||
logging.exception(msg, *args, **kwargs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试日志配置
|
||||
logger = get_logger(__name__)
|
||||
logger.info("测试INFO日志")
|
||||
logger.warning("测试WARNING日志")
|
||||
logger.error("测试ERROR日志")
|
||||
logger.debug("测试DEBUG日志")
|
||||
|
||||
try:
|
||||
raise ValueError("测试异常")
|
||||
except ValueError as e:
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
统一日志配置模块
|
||||
提供统一的日志配置,避免各模块自行配置
|
||||
支持按日期分片存储日志
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
||||
from typing import Optional
|
||||
|
||||
from src.config import config
|
||||
|
||||
|
||||
def setup_logging(
|
||||
log_file: Optional[str] = None,
|
||||
console_level: int = logging.INFO,
|
||||
file_level: int = logging.DEBUG,
|
||||
use_date_split: bool = True,
|
||||
date_folder_format: str = "%Y-%m", # 按月份分文件夹
|
||||
max_bytes: int = 10 * 1024 * 1024, # 10MB
|
||||
backup_count: int = 5
|
||||
) -> logging.Logger:
|
||||
"""
|
||||
配置统一的日志系统
|
||||
|
||||
参数:
|
||||
log_file: 日志文件路径,如果为None则使用默认路径
|
||||
console_level: 控制台日志级别
|
||||
file_level: 文件日志级别
|
||||
use_date_split: 是否使用日期分片
|
||||
date_folder_format: 日期文件夹格式(默认按月份,如 logs/2025-12/)
|
||||
max_bytes: 单个日志文件最大大小
|
||||
backup_count: 备份文件数量
|
||||
|
||||
返回:
|
||||
配置好的根日志器
|
||||
"""
|
||||
# 获取当前日期用于构建路径
|
||||
now = datetime.now()
|
||||
|
||||
if log_file is None:
|
||||
log_dir = 'logs'
|
||||
if use_date_split:
|
||||
# 按日期分片:logs/2025-12/2025-12-30.log
|
||||
date_folder = now.strftime(date_folder_format)
|
||||
log_dir = os.path.join('logs', date_folder)
|
||||
log_file = os.path.join(log_dir, now.strftime('%Y-%m-%d.log'))
|
||||
else:
|
||||
log_file = os.path.join(log_dir, 'app.log')
|
||||
else:
|
||||
log_dir = os.path.dirname(log_file)
|
||||
|
||||
if log_dir and not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
# 获取根日志器
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG) # 根日志器设置为最低级别
|
||||
|
||||
# 清除现有handler,避免重复添加
|
||||
logger.handlers.clear()
|
||||
|
||||
# 控制台handler
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(console_level)
|
||||
console_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
console_handler.setFormatter(console_formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# 文件handler(日期分片或大小轮转)
|
||||
if use_date_split:
|
||||
# 使用TimedRotatingFileHandler,每天午夜轮转
|
||||
file_handler = TimedRotatingFileHandler(
|
||||
log_file,
|
||||
when='midnight',
|
||||
interval=1,
|
||||
backupCount=backup_count,
|
||||
encoding='utf-8',
|
||||
atTime=datetime.strptime('00:00:00', '%H:%M:%S')
|
||||
)
|
||||
logger.info(f"日志系统已初始化,使用日期分片: {log_file}")
|
||||
else:
|
||||
# 使用RotatingFileHandler,按大小轮转
|
||||
file_handler = RotatingFileHandler(
|
||||
log_file,
|
||||
maxBytes=max_bytes,
|
||||
backupCount=backup_count,
|
||||
encoding='utf-8'
|
||||
)
|
||||
logger.info(f"日志系统已初始化,使用大小轮转: {log_file}")
|
||||
|
||||
file_handler.setLevel(file_level)
|
||||
file_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# 设置第三方库的日志级别
|
||||
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
||||
logging.getLogger('requests').setLevel(logging.WARNING)
|
||||
|
||||
logger.info(f"控制台日志级别: {logging.getLevelName(console_level)}")
|
||||
logger.info(f"文件日志级别: {logging.getLevelName(file_level)}")
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""
|
||||
获取指定名称的日志器
|
||||
|
||||
参数:
|
||||
name: 日志器名称,通常使用 __name__
|
||||
|
||||
返回:
|
||||
配置好的日志器
|
||||
"""
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
# 自动初始化日志系统
|
||||
if not logging.getLogger().handlers:
|
||||
# 只有在没有handler时才初始化,避免重复初始化
|
||||
setup_logging()
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def info(msg: str, *args, **kwargs):
|
||||
"""记录INFO级别日志"""
|
||||
logging.info(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def warning(msg: str, *args, **kwargs):
|
||||
"""记录WARNING级别日志"""
|
||||
logging.warning(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def error(msg: str, *args, **kwargs):
|
||||
"""记录ERROR级别日志"""
|
||||
logging.error(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def debug(msg: str, *args, **kwargs):
|
||||
"""记录DEBUG级别日志"""
|
||||
logging.debug(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def exception(msg: str, *args, **kwargs):
|
||||
"""记录异常日志"""
|
||||
logging.exception(msg, *args, **kwargs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试日志配置
|
||||
logger = get_logger(__name__)
|
||||
logger.info("测试INFO日志")
|
||||
logger.warning("测试WARNING日志")
|
||||
logger.error("测试ERROR日志")
|
||||
logger.debug("测试DEBUG日志")
|
||||
|
||||
try:
|
||||
raise ValueError("测试异常")
|
||||
except ValueError as e:
|
||||
logger.exception("捕获到异常: %s", e)
|
||||
1980
src/logs/2026-01/2026-01-27.log
Normal file
1980
src/logs/2026-01/2026-01-27.log
Normal file
File diff suppressed because it is too large
Load Diff
265
src/logs/2026-01/2026-01-28.log
Normal file
265
src/logs/2026-01/2026-01-28.log
Normal file
@@ -0,0 +1,265 @@
|
||||
2026-01-28 08:37:33 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||
2026-01-28 08:37:33 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||
2026-01-28 08:37:33 - __main__ - INFO - gui.py:81 - 使用 iconphoto 设置图标成功: /home/admin1/文档/Orbitin/icons/container.png
|
||||
2026-01-28 08:37:33 - __main__ - INFO - gui.py:692 - GUI启动,开始自动获取新数据...
|
||||
2026-01-28 08:37:33 - __main__ - INFO - gui.py:705 - 正在刷新排班信息...
|
||||
2026-01-28 08:37:33 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-28 08:37:33 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-28 08:37:33 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-28 08:37:33 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:33 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-28 08:37:33 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:33 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-28 08:37:33 - src.feishu.manager - INFO - manager.py:214 - 开始刷新未来 7 天的排班信息
|
||||
2026-01-28 08:37:33 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-28 的排班信息...
|
||||
2026-01-28 08:37:33 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-28 的排班信息 (格式: 01/28/1月28日)
|
||||
2026-01-28 08:37:33 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-28 08:37:35 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7200秒
|
||||
2026-01-28 08:37:35 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7200 秒后过期
|
||||
2026-01-28 08:37:36 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-28 08:37:36 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-28 08:37:36 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||
2026-01-28 08:37:37 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-28 08:37:37 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月28日 解析表格: 2026年排班表
|
||||
2026-01-28 08:37:37 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-28 08:37:37 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-28 08:37:37 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月28日 -> 1月28日 (索引: 28)
|
||||
2026-01-28 08:37:37 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:37 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:37 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-28
|
||||
2026-01-28 08:37:37 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-28 的排班信息到数据库: 白班=梁启迟、汪钦良, 夜班=冯栋、刘炜彬、杨俊豪
|
||||
2026-01-28 08:37:37 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-29 的排班信息...
|
||||
2026-01-28 08:37:37 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-29 的排班信息 (格式: 01/29/1月29日)
|
||||
2026-01-28 08:37:37 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-28 08:37:37 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-28 08:37:37 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-28 08:37:37 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月29日 解析表格: 2026年排班表
|
||||
2026-01-28 08:37:38 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-28 08:37:38 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-28 08:37:38 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月29日 -> 1月29日 (索引: 29)
|
||||
2026-01-28 08:37:38 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:38 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:38 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-29
|
||||
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-29 的排班信息到数据库: 白班=汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||
2026-01-28 08:37:38 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-30 的排班信息...
|
||||
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-30 的排班信息 (格式: 01/30/1月30日)
|
||||
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||
2026-01-28 08:37:38 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月30日 解析表格: 2026年排班表
|
||||
2026-01-28 08:37:38 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-28 08:37:38 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-28 08:37:38 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月30日 -> 1月30日 (索引: 30)
|
||||
2026-01-28 08:37:38 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:38 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:38 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-30
|
||||
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-30 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||
2026-01-28 08:37:38 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-31 的排班信息...
|
||||
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||
2026-01-28 08:37:38 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||
2026-01-28 08:37:39 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||
2026-01-28 08:37:39 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-28 08:37:39 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-28 08:37:39 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||
2026-01-28 08:37:39 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:39 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:39 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||
2026-01-28 08:37:39 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-01 的排班信息...
|
||||
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-01 的排班信息 (格式: 02/01/2月1日)
|
||||
2026-01-28 08:37:39 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||
2026-01-28 08:37:39 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-28 08:37:39 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||
2026-01-28 08:37:39 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月1日 解析表格: 2026年排班表
|
||||
2026-01-28 08:37:39 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-28 08:37:39 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-28 08:37:39 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-28 08:37:39 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月1日 -> 2月1日 (索引: 1)
|
||||
2026-01-28 08:37:39 - src.feishu.manager - WARNING - manager.py:182 - 解析结果为空,2026-02-01 未保存到数据库
|
||||
2026-01-28 08:37:39 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-02 的排班信息...
|
||||
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-02 的排班信息 (格式: 02/02/2月2日)
|
||||
2026-01-28 08:37:39 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7193秒
|
||||
2026-01-28 08:37:40 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-28 08:37:40 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-28 08:37:40 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7193秒
|
||||
2026-01-28 08:37:40 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-28 08:37:40 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月2日 解析表格: 2026年排班表
|
||||
2026-01-28 08:37:40 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-28 08:37:40 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-28 08:37:40 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-28 08:37:40 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月2日 -> 2月2日 (索引: 2)
|
||||
2026-01-28 08:37:40 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:40 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:40 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-02
|
||||
2026-01-28 08:37:40 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-02 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||
2026-01-28 08:37:40 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-03 的排班信息...
|
||||
2026-01-28 08:37:40 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-03 的排班信息 (格式: 02/03/2月3日)
|
||||
2026-01-28 08:37:40 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7192秒
|
||||
2026-01-28 08:37:41 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-28 08:37:41 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-28 08:37:41 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7191秒
|
||||
2026-01-28 08:37:42 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-28 08:37:42 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月3日 解析表格: 2026年排班表
|
||||
2026-01-28 08:37:42 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-28 08:37:42 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-28 08:37:42 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-28 08:37:42 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月3日 -> 2月3日 (索引: 3)
|
||||
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:42 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-03
|
||||
2026-01-28 08:37:42 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-03 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||
2026-01-28 08:37:42 - src.feishu.manager - INFO - manager.py:230 - 排班信息刷新完成,成功: 7, 失败: 0
|
||||
2026-01-28 08:37:42 - __main__ - INFO - gui.py:710 - 排班信息刷新完成
|
||||
2026-01-28 08:37:42 - __main__ - INFO - gui.py:726 - 正在尝试获取最新作业数据...
|
||||
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:42 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:42 - __main__ - INFO - gui.py:751 - 正在从 Confluence 获取 HTML...
|
||||
2026-01-28 08:37:42 - src.confluence.client - DEBUG - client.py:50 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||
2026-01-28 08:37:42 - src.confluence.client - DEBUG - client.py:76 - 获取Confluence内容: 159049182
|
||||
2026-01-28 08:37:43 - src.confluence.client - INFO - client.py:81 - 成功获取Confluence内容: 159049182
|
||||
2026-01-28 08:37:43 - src.confluence.client - INFO - client.py:122 - 获取到Confluence HTML内容,长度: 78987 字符
|
||||
2026-01-28 08:37:43 - __main__ - INFO - gui.py:757 - 获取成功,共 78987 字符
|
||||
2026-01-28 08:37:43 - __main__ - INFO - gui.py:761 - 正在提取布局文本...
|
||||
2026-01-28 08:37:43 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 78987 字符
|
||||
2026-01-28 08:37:43 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 17658 字符
|
||||
2026-01-28 08:37:43 - __main__ - INFO - gui.py:767 - 正在解析日志数据...
|
||||
2026-01-28 08:37:43 - src.confluence.log_parser - INFO - log_parser.py:390 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||
2026-01-28 08:37:43 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 144 条记录
|
||||
2026-01-28 08:37:43 - __main__ - INFO - gui.py:774 - 正在保存到数据库...
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:43 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:43 - src.database.daily_logs - INFO - daily_logs.py:237 - 批量插入完成,成功 144/144 条记录
|
||||
2026-01-28 08:37:43 - __main__ - INFO - gui.py:778 - 已保存 144 条新记录
|
||||
2026-01-28 08:37:43 - __main__ - INFO - gui.py:806 - 正在生成今日日报...
|
||||
2026-01-28 08:37:43 - __main__ - INFO - gui.py:650 - 生成 2026-01-27 的日报...
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:43 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:43 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:43 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-28 08:37:43 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-28 08:37:43 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:43 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:43 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-28 08:37:43 - src.report - INFO - report.py:266 - 获取 2026-01-27 日报的班次人员,对应排班表日期: 2026-01-28
|
||||
2026-01-28 08:37:43 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-28 的排班信息 (格式: 01/28/1月28日)
|
||||
2026-01-28 08:37:43 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-28 08:37:44 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7191秒
|
||||
2026-01-28 08:37:44 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7191 秒后过期
|
||||
2026-01-28 08:37:46 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-28 08:37:46 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-28 08:37:46 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7188秒
|
||||
2026-01-28 08:37:47 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-28 08:37:47 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月28日 解析表格: 2026年排班表
|
||||
2026-01-28 08:37:47 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-28 08:37:47 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-28 08:37:47 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月28日 -> 1月28日 (索引: 28)
|
||||
2026-01-28 08:37:47 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:37:47 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:37:47 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-28
|
||||
2026-01-28 08:37:47 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-28 的排班信息到数据库: 白班=梁启迟、汪钦良, 夜班=冯栋、刘炜彬、杨俊豪
|
||||
2026-01-28 08:37:47 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-27
|
||||
2026-01-28 08:37:47 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-27
|
||||
2026-01-28 08:37:47 - __main__ - INFO - gui.py:811 - 自动获取完成,GUI已就绪
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:331 - 开始获取数据...
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:12 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:360 - 使用页面ID映射: 2026-01-27 -> 159049182
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:365 - 正在从 Confluence 获取 HTML...
|
||||
2026-01-28 08:39:12 - src.confluence.client - DEBUG - client.py:50 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||
2026-01-28 08:39:12 - src.confluence.client - DEBUG - client.py:76 - 获取Confluence内容: 159049182
|
||||
2026-01-28 08:39:12 - src.confluence.client - INFO - client.py:81 - 成功获取Confluence内容: 159049182
|
||||
2026-01-28 08:39:12 - src.confluence.client - INFO - client.py:122 - 获取到Confluence HTML内容,长度: 78990 字符
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:375 - 获取成功,共 78990 字符
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:379 - 正在提取布局文本...
|
||||
2026-01-28 08:39:12 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 78990 字符
|
||||
2026-01-28 08:39:12 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 17661 字符
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:383 - 提取完成,共 17660 字符
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:387 - 正在解析日志数据...
|
||||
2026-01-28 08:39:12 - src.confluence.log_parser - INFO - log_parser.py:390 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||
2026-01-28 08:39:12 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 144 条记录
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:391 - 解析到 144 条记录
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:396 - 正在保存到数据库...
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:12 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:12 - src.database.daily_logs - INFO - daily_logs.py:237 - 批量插入完成,成功 144/144 条记录
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:400 - 已保存 144 条记录
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:405 - 数据库总计: 275 条记录, 53 艘船
|
||||
2026-01-28 08:39:12 - __main__ - INFO - gui.py:650 - 生成 2026-01-27 的日报...
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:12 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:12 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:13 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-28 08:39:13 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-28 08:39:13 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:13 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:13 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-28 08:39:13 - src.report - INFO - report.py:266 - 获取 2026-01-27 日报的班次人员,对应排班表日期: 2026-01-28
|
||||
2026-01-28 08:39:13 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-28 的排班信息 (格式: 01/28/1月28日)
|
||||
2026-01-28 08:39:13 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-28 08:39:13 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7102秒
|
||||
2026-01-28 08:39:13 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7102 秒后过期
|
||||
2026-01-28 08:39:13 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-28 08:39:13 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-28 08:39:13 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7101秒
|
||||
2026-01-28 08:39:14 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-28 08:39:14 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月28日 解析表格: 2026年排班表
|
||||
2026-01-28 08:39:14 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-28 08:39:14 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-28 08:39:14 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月28日 -> 1月28日 (索引: 28)
|
||||
2026-01-28 08:39:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-28 08:39:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-28 08:39:14 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-28
|
||||
2026-01-28 08:39:14 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-28 的排班信息到数据库: 白班=梁启迟、汪钦良, 夜班=冯栋、刘炜彬、杨俊豪
|
||||
2026-01-28 08:39:14 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-27
|
||||
2026-01-28 08:39:14 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-27
|
||||
2026-01-28 08:39:14 - __main__ - INFO - gui.py:414 - 数据获取完成
|
||||
233
src/logs/2026-01/2026-01-29.log
Normal file
233
src/logs/2026-01/2026-01-29.log
Normal file
@@ -0,0 +1,233 @@
|
||||
2026-01-29 08:23:17 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||
2026-01-29 08:23:17 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||
2026-01-29 08:23:18 - __main__ - INFO - gui.py:81 - 使用 iconphoto 设置图标成功: /home/admin1/文档/Orbitin/icons/container.png
|
||||
2026-01-29 08:23:18 - __main__ - INFO - gui.py:692 - GUI启动,开始自动获取新数据...
|
||||
2026-01-29 08:23:18 - __main__ - INFO - gui.py:705 - 正在刷新排班信息...
|
||||
2026-01-29 08:23:18 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-29 08:23:18 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-29 08:23:18 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-29 08:23:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:18 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-29 08:23:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:18 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-29 08:23:18 - src.feishu.manager - INFO - manager.py:214 - 开始刷新未来 7 天的排班信息
|
||||
2026-01-29 08:23:18 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-29 的排班信息...
|
||||
2026-01-29 08:23:18 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-29 的排班信息 (格式: 01/29/1月29日)
|
||||
2026-01-29 08:23:18 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-29 08:23:18 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7200秒
|
||||
2026-01-29 08:23:18 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7200 秒后过期
|
||||
2026-01-29 08:23:19 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-29 08:23:19 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-29 08:23:19 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7199秒
|
||||
2026-01-29 08:23:19 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-29 08:23:19 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月29日 解析表格: 2026年排班表
|
||||
2026-01-29 08:23:19 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-29 08:23:19 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-29 08:23:19 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月29日 -> 1月29日 (索引: 29)
|
||||
2026-01-29 08:23:19 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:19 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:19 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-29
|
||||
2026-01-29 08:23:19 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-29 的排班信息到数据库: 白班=汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||
2026-01-29 08:23:19 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-30 的排班信息...
|
||||
2026-01-29 08:23:19 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-30 的排班信息 (格式: 01/30/1月30日)
|
||||
2026-01-29 08:23:19 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||
2026-01-29 08:23:20 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月30日 解析表格: 2026年排班表
|
||||
2026-01-29 08:23:20 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-29 08:23:20 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-29 08:23:20 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月30日 -> 1月30日 (索引: 30)
|
||||
2026-01-29 08:23:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:20 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-30
|
||||
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-30 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||
2026-01-29 08:23:20 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-31 的排班信息...
|
||||
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||
2026-01-29 08:23:20 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||
2026-01-29 08:23:20 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-29 08:23:20 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-29 08:23:20 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||
2026-01-29 08:23:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:20 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||
2026-01-29 08:23:20 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-01 的排班信息...
|
||||
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-01 的排班信息 (格式: 02/01/2月1日)
|
||||
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||
2026-01-29 08:23:21 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-29 08:23:21 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-29 08:23:21 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||
2026-01-29 08:23:21 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-29 08:23:21 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月1日 解析表格: 2026年排班表
|
||||
2026-01-29 08:23:21 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-29 08:23:21 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-29 08:23:21 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-29 08:23:21 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月1日 -> 2月1日 (索引: 1)
|
||||
2026-01-29 08:23:21 - src.feishu.manager - WARNING - manager.py:182 - 解析结果为空,2026-02-01 未保存到数据库
|
||||
2026-01-29 08:23:21 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-02 的排班信息...
|
||||
2026-01-29 08:23:21 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-02 的排班信息 (格式: 02/02/2月2日)
|
||||
2026-01-29 08:23:21 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||
2026-01-29 08:23:21 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-29 08:23:21 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-29 08:23:21 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月2日 解析表格: 2026年排班表
|
||||
2026-01-29 08:23:22 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月2日 -> 2月2日 (索引: 2)
|
||||
2026-01-29 08:23:22 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:22 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:22 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-02
|
||||
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-02 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||
2026-01-29 08:23:22 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-03 的排班信息...
|
||||
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-03 的排班信息 (格式: 02/03/2月3日)
|
||||
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-29 08:23:22 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月3日 解析表格: 2026年排班表
|
||||
2026-01-29 08:23:22 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月3日 -> 2月3日 (索引: 3)
|
||||
2026-01-29 08:23:22 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:22 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:22 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-03
|
||||
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-03 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||
2026-01-29 08:23:22 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-04 的排班信息...
|
||||
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-04 的排班信息 (格式: 02/04/2月4日)
|
||||
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||
2026-01-29 08:23:22 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||
2026-01-29 08:23:23 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月4日 解析表格: 2026年排班表
|
||||
2026-01-29 08:23:23 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-29 08:23:23 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-29 08:23:23 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-29 08:23:23 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月4日 -> 2月4日 (索引: 4)
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:23 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-04
|
||||
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-04 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:230 - 排班信息刷新完成,成功: 7, 失败: 0
|
||||
2026-01-29 08:23:23 - __main__ - INFO - gui.py:650 - 生成 2026-01-29 的日报...
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:23 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:23 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-29 08:23:23 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-29 08:23:23 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:23 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-29 08:23:23 - src.report - INFO - report.py:266 - 获取 2026-01-29 日报的班次人员,对应排班表日期: 2026-01-30
|
||||
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-30 的排班信息 (格式: 01/30/1月30日)
|
||||
2026-01-29 08:23:23 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-29 08:23:23 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7195秒
|
||||
2026-01-29 08:23:23 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7195 秒后过期
|
||||
2026-01-29 08:23:23 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-29 08:23:23 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||
2026-01-29 08:23:24 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-29 08:23:24 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月30日 解析表格: 2026年排班表
|
||||
2026-01-29 08:23:24 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-29 08:23:24 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-29 08:23:24 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月30日 -> 1月30日 (索引: 30)
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-30
|
||||
2026-01-29 08:23:24 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-30 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||
2026-01-29 08:23:24 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-29
|
||||
2026-01-29 08:23:24 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-29
|
||||
2026-01-29 08:23:24 - __main__ - INFO - gui.py:710 - 排班信息刷新完成
|
||||
2026-01-29 08:23:24 - __main__ - INFO - gui.py:726 - 正在尝试获取最新作业数据...
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - __main__ - INFO - gui.py:751 - 正在从 Confluence 获取 HTML...
|
||||
2026-01-29 08:23:24 - src.confluence.client - DEBUG - client.py:50 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||
2026-01-29 08:23:24 - src.confluence.client - DEBUG - client.py:76 - 获取Confluence内容: 159049182
|
||||
2026-01-29 08:23:24 - src.confluence.client - INFO - client.py:81 - 成功获取Confluence内容: 159049182
|
||||
2026-01-29 08:23:24 - src.confluence.client - INFO - client.py:122 - 获取到Confluence HTML内容,长度: 81156 字符
|
||||
2026-01-29 08:23:24 - __main__ - INFO - gui.py:757 - 获取成功,共 81156 字符
|
||||
2026-01-29 08:23:24 - __main__ - INFO - gui.py:761 - 正在提取布局文本...
|
||||
2026-01-29 08:23:24 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 81156 字符
|
||||
2026-01-29 08:23:24 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 18253 字符
|
||||
2026-01-29 08:23:24 - __main__ - INFO - gui.py:767 - 正在解析日志数据...
|
||||
2026-01-29 08:23:24 - src.confluence.log_parser - INFO - log_parser.py:390 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||
2026-01-29 08:23:24 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 148 条记录
|
||||
2026-01-29 08:23:24 - __main__ - INFO - gui.py:774 - 正在保存到数据库...
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - src.database.daily_logs - INFO - daily_logs.py:237 - 批量插入完成,成功 148/148 条记录
|
||||
2026-01-29 08:23:24 - __main__ - INFO - gui.py:778 - 已保存 148 条新记录
|
||||
2026-01-29 08:23:24 - __main__ - INFO - gui.py:806 - 正在生成今日日报...
|
||||
2026-01-29 08:23:24 - __main__ - INFO - gui.py:650 - 生成 2026-01-28 的日报...
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-29 08:23:24 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-29 08:23:24 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:24 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:24 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-29 08:23:24 - src.report - INFO - report.py:266 - 获取 2026-01-28 日报的班次人员,对应排班表日期: 2026-01-29
|
||||
2026-01-29 08:23:24 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-29 的排班信息 (格式: 01/29/1月29日)
|
||||
2026-01-29 08:23:24 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-29 08:23:24 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7194秒
|
||||
2026-01-29 08:23:24 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7194 秒后过期
|
||||
2026-01-29 08:23:25 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-29 08:23:25 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-29 08:23:25 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7193秒
|
||||
2026-01-29 08:23:25 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-29 08:23:25 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月29日 解析表格: 2026年排班表
|
||||
2026-01-29 08:23:25 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-29 08:23:25 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-29 08:23:25 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月29日 -> 1月29日 (索引: 29)
|
||||
2026-01-29 08:23:25 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-29 08:23:25 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-29 08:23:25 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-29
|
||||
2026-01-29 08:23:25 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-29 的排班信息到数据库: 白班=汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||
2026-01-29 08:23:25 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-28
|
||||
2026-01-29 08:23:25 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-28
|
||||
2026-01-29 08:23:25 - __main__ - INFO - gui.py:811 - 自动获取完成,GUI已就绪
|
||||
234
src/logs/2026-01/2026-01-30.log
Normal file
234
src/logs/2026-01/2026-01-30.log
Normal file
@@ -0,0 +1,234 @@
|
||||
2026-01-30 08:29:07 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||
2026-01-30 08:29:07 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||
2026-01-30 08:29:07 - __main__ - INFO - gui.py:81 - 使用 iconphoto 设置图标成功: /home/admin1/文档/Orbitin/icons/container.png
|
||||
2026-01-30 08:29:09 - __main__ - INFO - gui.py:692 - GUI启动,开始自动获取新数据...
|
||||
2026-01-30 08:29:09 - __main__ - INFO - gui.py:705 - 正在刷新排班信息...
|
||||
2026-01-30 08:29:09 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-30 08:29:09 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-30 08:29:09 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-30 08:29:09 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:09 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-30 08:29:09 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:09 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-30 08:29:09 - src.feishu.manager - INFO - manager.py:214 - 开始刷新未来 7 天的排班信息
|
||||
2026-01-30 08:29:09 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-30 的排班信息...
|
||||
2026-01-30 08:29:09 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-30 的排班信息 (格式: 01/30/1月30日)
|
||||
2026-01-30 08:29:09 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-30 08:29:09 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7200秒
|
||||
2026-01-30 08:29:09 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7200 秒后过期
|
||||
2026-01-30 08:29:10 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-30 08:29:10 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-30 08:29:10 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||
2026-01-30 08:29:10 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-30 08:29:10 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月30日 解析表格: 2026年排班表
|
||||
2026-01-30 08:29:10 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-30 08:29:10 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-30 08:29:10 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月30日 -> 1月30日 (索引: 30)
|
||||
2026-01-30 08:29:10 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:10 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:10 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-30
|
||||
2026-01-30 08:29:10 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-30 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||
2026-01-30 08:29:10 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-31 的排班信息...
|
||||
2026-01-30 08:29:10 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||
2026-01-30 08:29:10 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||
2026-01-30 08:29:10 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-30 08:29:10 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-30 08:29:10 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||
2026-01-30 08:29:11 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||
2026-01-30 08:29:11 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-30 08:29:11 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-30 08:29:11 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||
2026-01-30 08:29:11 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:11 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:11 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||
2026-01-30 08:29:11 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-01 的排班信息...
|
||||
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-01 的排班信息 (格式: 02/01/2月1日)
|
||||
2026-01-30 08:29:11 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||
2026-01-30 08:29:11 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-30 08:29:11 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||
2026-01-30 08:29:11 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月1日 解析表格: 2026年排班表
|
||||
2026-01-30 08:29:11 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-30 08:29:11 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-30 08:29:11 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-30 08:29:11 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月1日 -> 2月1日 (索引: 1)
|
||||
2026-01-30 08:29:11 - src.feishu.manager - WARNING - manager.py:182 - 解析结果为空,2026-02-01 未保存到数据库
|
||||
2026-01-30 08:29:11 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-02 的排班信息...
|
||||
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-02 的排班信息 (格式: 02/02/2月2日)
|
||||
2026-01-30 08:29:11 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||
2026-01-30 08:29:12 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月2日 解析表格: 2026年排班表
|
||||
2026-01-30 08:29:12 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月2日 -> 2月2日 (索引: 2)
|
||||
2026-01-30 08:29:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:12 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-02
|
||||
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-02 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||
2026-01-30 08:29:12 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-03 的排班信息...
|
||||
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-03 的排班信息 (格式: 02/03/2月3日)
|
||||
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-30 08:29:12 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月3日 解析表格: 2026年排班表
|
||||
2026-01-30 08:29:12 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月3日 -> 2月3日 (索引: 3)
|
||||
2026-01-30 08:29:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:12 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-03
|
||||
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-03 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||
2026-01-30 08:29:12 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-04 的排班信息...
|
||||
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-04 的排班信息 (格式: 02/04/2月4日)
|
||||
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-30 08:29:13 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-30 08:29:13 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-30 08:29:13 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||
2026-01-30 08:29:13 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-30 08:29:13 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月4日 解析表格: 2026年排班表
|
||||
2026-01-30 08:29:13 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-30 08:29:13 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-30 08:29:13 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-30 08:29:13 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月4日 -> 2月4日 (索引: 4)
|
||||
2026-01-30 08:29:13 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:13 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-04
|
||||
2026-01-30 08:29:13 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-04 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||
2026-01-30 08:29:13 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-05 的排班信息...
|
||||
2026-01-30 08:29:13 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-05 的排班信息 (格式: 02/05/2月5日)
|
||||
2026-01-30 08:29:13 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||
2026-01-30 08:29:13 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-30 08:29:13 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-30 08:29:13 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||
2026-01-30 08:29:14 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月5日 解析表格: 2026年排班表
|
||||
2026-01-30 08:29:14 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-30 08:29:14 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-30 08:29:14 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-30 08:29:14 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月5日 -> 2月5日 (索引: 5)
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:14 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-05
|
||||
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-05 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:230 - 排班信息刷新完成,成功: 7, 失败: 0
|
||||
2026-01-30 08:29:14 - __main__ - INFO - gui.py:650 - 生成 2026-01-30 的日报...
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:14 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:14 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-30 08:29:14 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-30 08:29:14 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:14 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-30 08:29:14 - src.report - INFO - report.py:266 - 获取 2026-01-30 日报的班次人员,对应排班表日期: 2026-01-31
|
||||
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||
2026-01-30 08:29:14 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-30 08:29:14 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7195秒
|
||||
2026-01-30 08:29:14 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7195 秒后过期
|
||||
2026-01-30 08:29:14 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-30 08:29:14 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||
2026-01-30 08:29:15 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-30 08:29:15 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||
2026-01-30 08:29:15 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-30 08:29:15 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-30 08:29:15 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||
2026-01-30 08:29:15 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||
2026-01-30 08:29:15 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-30
|
||||
2026-01-30 08:29:15 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-30
|
||||
2026-01-30 08:29:15 - __main__ - INFO - gui.py:710 - 排班信息刷新完成
|
||||
2026-01-30 08:29:15 - __main__ - INFO - gui.py:726 - 正在尝试获取最新作业数据...
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - __main__ - INFO - gui.py:751 - 正在从 Confluence 获取 HTML...
|
||||
2026-01-30 08:29:15 - src.confluence.client - DEBUG - client.py:50 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||
2026-01-30 08:29:15 - src.confluence.client - DEBUG - client.py:76 - 获取Confluence内容: 159049182
|
||||
2026-01-30 08:29:15 - src.confluence.client - INFO - client.py:81 - 成功获取Confluence内容: 159049182
|
||||
2026-01-30 08:29:15 - src.confluence.client - INFO - client.py:122 - 获取到Confluence HTML内容,长度: 84056 字符
|
||||
2026-01-30 08:29:15 - __main__ - INFO - gui.py:757 - 获取成功,共 84056 字符
|
||||
2026-01-30 08:29:15 - __main__ - INFO - gui.py:761 - 正在提取布局文本...
|
||||
2026-01-30 08:29:15 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 84056 字符
|
||||
2026-01-30 08:29:15 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 18944 字符
|
||||
2026-01-30 08:29:15 - __main__ - INFO - gui.py:767 - 正在解析日志数据...
|
||||
2026-01-30 08:29:15 - src.confluence.log_parser - INFO - log_parser.py:390 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||
2026-01-30 08:29:15 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 154 条记录
|
||||
2026-01-30 08:29:15 - __main__ - INFO - gui.py:774 - 正在保存到数据库...
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - src.database.daily_logs - INFO - daily_logs.py:237 - 批量插入完成,成功 154/154 条记录
|
||||
2026-01-30 08:29:15 - __main__ - INFO - gui.py:778 - 已保存 154 条新记录
|
||||
2026-01-30 08:29:15 - __main__ - INFO - gui.py:806 - 正在生成今日日报...
|
||||
2026-01-30 08:29:15 - __main__ - INFO - gui.py:650 - 生成 2026-01-29 的日报...
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-30 08:29:15 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-30 08:29:15 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:15 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:15 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-30 08:29:15 - src.report - INFO - report.py:266 - 获取 2026-01-29 日报的班次人员,对应排班表日期: 2026-01-30
|
||||
2026-01-30 08:29:15 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-30 的排班信息 (格式: 01/30/1月30日)
|
||||
2026-01-30 08:29:15 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-30 08:29:16 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7193秒
|
||||
2026-01-30 08:29:16 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7193 秒后过期
|
||||
2026-01-30 08:29:16 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-30 08:29:16 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-30 08:29:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7192秒
|
||||
2026-01-30 08:29:16 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-30 08:29:16 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月30日 解析表格: 2026年排班表
|
||||
2026-01-30 08:29:16 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-30 08:29:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-30 08:29:16 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月30日 -> 1月30日 (索引: 30)
|
||||
2026-01-30 08:29:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-30 08:29:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-30 08:29:16 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-30
|
||||
2026-01-30 08:29:16 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-30 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||
2026-01-30 08:29:16 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-29
|
||||
2026-01-30 08:29:16 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-29
|
||||
2026-01-30 08:29:16 - __main__ - INFO - gui.py:811 - 自动获取完成,GUI已就绪
|
||||
239
src/logs/2026-01/2026-01-31.log
Normal file
239
src/logs/2026-01/2026-01-31.log
Normal file
@@ -0,0 +1,239 @@
|
||||
2026-01-31 08:25:13 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||
2026-01-31 08:25:13 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||
2026-01-31 08:25:13 - __main__ - INFO - gui.py:81 - 使用 iconphoto 设置图标成功: /home/admin1/文档/Orbitin/icons/container.png
|
||||
2026-01-31 08:25:14 - __main__ - INFO - gui.py:692 - GUI启动,开始自动获取新数据...
|
||||
2026-01-31 08:25:14 - __main__ - INFO - gui.py:705 - 正在刷新排班信息...
|
||||
2026-01-31 08:25:14 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-31 08:25:14 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-31 08:25:14 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-31 08:25:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:14 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-31 08:25:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:14 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-31 08:25:14 - src.feishu.manager - INFO - manager.py:214 - 开始刷新未来 7 天的排班信息
|
||||
2026-01-31 08:25:14 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-31 的排班信息...
|
||||
2026-01-31 08:25:14 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||
2026-01-31 08:25:14 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-31 08:25:14 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7200秒
|
||||
2026-01-31 08:25:14 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7200 秒后过期
|
||||
2026-01-31 08:25:15 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-31 08:25:15 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-31 08:25:15 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7199秒
|
||||
2026-01-31 08:25:15 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-31 08:25:15 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||
2026-01-31 08:25:15 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-31 08:25:15 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-31 08:25:15 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||
2026-01-31 08:25:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:15 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||
2026-01-31 08:25:15 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||
2026-01-31 08:25:15 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-01 的排班信息...
|
||||
2026-01-31 08:25:15 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-01 的排班信息 (格式: 02/01/2月1日)
|
||||
2026-01-31 08:25:15 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||
2026-01-31 08:25:15 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-31 08:25:15 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-31 08:25:15 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||
2026-01-31 08:25:16 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月1日 解析表格: 2026年排班表
|
||||
2026-01-31 08:25:16 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月1日 -> 2月1日 (索引: 1)
|
||||
2026-01-31 08:25:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:16 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-01
|
||||
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-01 的排班信息到数据库: 白班=梁启迟、牛晨, 夜班=冯栋、刘炜彬
|
||||
2026-01-31 08:25:16 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-02 的排班信息...
|
||||
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-02 的排班信息 (格式: 02/02/2月2日)
|
||||
2026-01-31 08:25:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||
2026-01-31 08:25:16 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-31 08:25:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||
2026-01-31 08:25:16 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月2日 解析表格: 2026年排班表
|
||||
2026-01-31 08:25:16 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月2日 -> 2月2日 (索引: 2)
|
||||
2026-01-31 08:25:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:16 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-02
|
||||
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-02 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬、杨俊豪
|
||||
2026-01-31 08:25:16 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-03 的排班信息...
|
||||
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-03 的排班信息 (格式: 02/03/2月3日)
|
||||
2026-01-31 08:25:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||
2026-01-31 08:25:17 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月3日 解析表格: 2026年排班表
|
||||
2026-01-31 08:25:17 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月3日 -> 2月3日 (索引: 3)
|
||||
2026-01-31 08:25:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:17 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-03
|
||||
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-03 的排班信息到数据库: 白班=梁启迟、汪钦良, 夜班=刘炜彬、杨俊豪
|
||||
2026-01-31 08:25:17 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-04 的排班信息...
|
||||
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-04 的排班信息 (格式: 02/04/2月4日)
|
||||
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-31 08:25:17 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月4日 解析表格: 2026年排班表
|
||||
2026-01-31 08:25:17 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月4日 -> 2月4日 (索引: 4)
|
||||
2026-01-31 08:25:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:17 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-04
|
||||
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-04 的排班信息到数据库: 白班=梁启迟、汪钦良, 夜班=刘炜彬、杨俊豪
|
||||
2026-01-31 08:25:17 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-05 的排班信息...
|
||||
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-05 的排班信息 (格式: 02/05/2月5日)
|
||||
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-31 08:25:18 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月5日 解析表格: 2026年排班表
|
||||
2026-01-31 08:25:18 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月5日 -> 2月5日 (索引: 5)
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:18 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-05
|
||||
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-05 的排班信息到数据库: 白班=汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||
2026-01-31 08:25:18 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-06 的排班信息...
|
||||
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-06 的排班信息 (格式: 02/06/2月6日)
|
||||
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||
2026-01-31 08:25:18 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月6日 解析表格: 2026年排班表
|
||||
2026-01-31 08:25:18 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月6日 -> 2月6日 (索引: 6)
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:18 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-06
|
||||
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-06 的排班信息到数据库: 白班=汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:230 - 排班信息刷新完成,成功: 7, 失败: 0
|
||||
2026-01-31 08:25:18 - __main__ - INFO - gui.py:650 - 生成 2026-01-31 的日报...
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:18 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:18 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:18 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-31 08:25:18 - src.report - INFO - report.py:266 - 获取 2026-01-31 日报的班次人员,对应排班表日期: 2026-02-01
|
||||
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-01 的排班信息 (格式: 02/01/2月1日)
|
||||
2026-01-31 08:25:19 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-31 08:25:19 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7195秒
|
||||
2026-01-31 08:25:19 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7195 秒后过期
|
||||
2026-01-31 08:25:19 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-31 08:25:19 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-31 08:25:19 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||
2026-01-31 08:25:20 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-31 08:25:20 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月1日 解析表格: 2026年排班表
|
||||
2026-01-31 08:25:20 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-31 08:25:20 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-31 08:25:20 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||
2026-01-31 08:25:20 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月1日 -> 2月1日 (索引: 1)
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-01
|
||||
2026-01-31 08:25:20 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-01 的排班信息到数据库: 白班=梁启迟、牛晨, 夜班=冯栋、刘炜彬
|
||||
2026-01-31 08:25:20 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-31
|
||||
2026-01-31 08:25:20 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-31
|
||||
2026-01-31 08:25:20 - __main__ - INFO - gui.py:710 - 排班信息刷新完成
|
||||
2026-01-31 08:25:20 - __main__ - INFO - gui.py:726 - 正在尝试获取最新作业数据...
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - __main__ - INFO - gui.py:751 - 正在从 Confluence 获取 HTML...
|
||||
2026-01-31 08:25:20 - src.confluence.client - DEBUG - client.py:50 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||
2026-01-31 08:25:20 - src.confluence.client - DEBUG - client.py:76 - 获取Confluence内容: 159049182
|
||||
2026-01-31 08:25:20 - src.confluence.client - INFO - client.py:81 - 成功获取Confluence内容: 159049182
|
||||
2026-01-31 08:25:20 - src.confluence.client - INFO - client.py:122 - 获取到Confluence HTML内容,长度: 86366 字符
|
||||
2026-01-31 08:25:20 - __main__ - INFO - gui.py:757 - 获取成功,共 86366 字符
|
||||
2026-01-31 08:25:20 - __main__ - INFO - gui.py:761 - 正在提取布局文本...
|
||||
2026-01-31 08:25:20 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 86366 字符
|
||||
2026-01-31 08:25:20 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 19525 字符
|
||||
2026-01-31 08:25:20 - __main__ - INFO - gui.py:767 - 正在解析日志数据...
|
||||
2026-01-31 08:25:20 - src.confluence.log_parser - INFO - log_parser.py:390 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||
2026-01-31 08:25:20 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 159 条记录
|
||||
2026-01-31 08:25:20 - __main__ - INFO - gui.py:774 - 正在保存到数据库...
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - src.database.daily_logs - INFO - daily_logs.py:237 - 批量插入完成,成功 159/159 条记录
|
||||
2026-01-31 08:25:20 - __main__ - INFO - gui.py:778 - 已保存 159 条新记录
|
||||
2026-01-31 08:25:20 - __main__ - INFO - gui.py:806 - 正在生成今日日报...
|
||||
2026-01-31 08:25:20 - __main__ - INFO - gui.py:650 - 生成 2026-01-30 的日报...
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||
2026-01-31 08:25:20 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||
2026-01-31 08:25:20 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:20 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:20 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||
2026-01-31 08:25:20 - src.report - INFO - report.py:266 - 获取 2026-01-30 日报的班次人员,对应排班表日期: 2026-01-31
|
||||
2026-01-31 08:25:20 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||
2026-01-31 08:25:20 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||
2026-01-31 08:25:20 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7194秒
|
||||
2026-01-31 08:25:20 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7194 秒后过期
|
||||
2026-01-31 08:25:21 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||
2026-01-31 08:25:21 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||
2026-01-31 08:25:21 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7193秒
|
||||
2026-01-31 08:25:21 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||
2026-01-31 08:25:21 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||
2026-01-31 08:25:21 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||
2026-01-31 08:25:21 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||
2026-01-31 08:25:21 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||
2026-01-31 08:25:21 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||
2026-01-31 08:25:21 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||
2026-01-31 08:25:21 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||
2026-01-31 08:25:21 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||
2026-01-31 08:25:21 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-30
|
||||
2026-01-31 08:25:21 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-30
|
||||
2026-01-31 08:25:21 - __main__ - INFO - gui.py:811 - 自动获取完成,GUI已就绪
|
||||
1661
src/logs/2026-02/2026-02-01.log
Normal file
1661
src/logs/2026-02/2026-02-01.log
Normal file
File diff suppressed because it is too large
Load Diff
923
src/report.py
923
src/report.py
@@ -1,446 +1,477 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
日报生成模块
|
||||
更新依赖,使用新的配置和数据库模块
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any
|
||||
import logging
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
from src.database.daily_logs import DailyLogsDatabase
|
||||
from src.feishu.manager import FeishuScheduleManager
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ReportGeneratorError(Exception):
|
||||
"""日报生成错误"""
|
||||
pass
|
||||
|
||||
|
||||
class DailyReportGenerator:
|
||||
"""每日作业报告生成器"""
|
||||
|
||||
def __init__(self, db_path: Optional[str] = None):
|
||||
"""
|
||||
初始化日报生成器
|
||||
|
||||
参数:
|
||||
db_path: 数据库文件路径,如果为None则使用配置
|
||||
"""
|
||||
self.db = DailyLogsDatabase(db_path)
|
||||
logger.info("日报生成器初始化完成")
|
||||
|
||||
def get_latest_date(self) -> str:
|
||||
"""
|
||||
获取数据库中最新的日期
|
||||
|
||||
返回:
|
||||
最新日期字符串,格式 "YYYY-MM-DD"
|
||||
"""
|
||||
try:
|
||||
logs = self.db.query_all(limit=1)
|
||||
if logs:
|
||||
return logs[0]['date']
|
||||
return datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取最新日期失败: {e}")
|
||||
return datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
def get_daily_data(self, date: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取指定日期的数据(包含手动调整)
|
||||
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||
|
||||
返回:
|
||||
每日数据字典
|
||||
"""
|
||||
try:
|
||||
# 使用数据库的新方法获取包含调整的数据
|
||||
if hasattr(self.db, 'get_daily_data_with_adjustments'):
|
||||
return self.db.get_daily_data_with_adjustments(date)
|
||||
|
||||
# 降级处理:如果没有新方法,使用原始逻辑
|
||||
logs = self.db.query_by_date(date)
|
||||
|
||||
# 按船名汇总TEU和尺寸箱量
|
||||
ships: Dict[str, Dict[str, Any]] = {}
|
||||
for log in logs:
|
||||
ship = log['ship_name']
|
||||
if ship not in ships:
|
||||
ships[ship] = {
|
||||
'teu': 0,
|
||||
'twenty_feet': 0,
|
||||
'forty_feet': 0
|
||||
}
|
||||
if log.get('teu'):
|
||||
ships[ship]['teu'] += log['teu']
|
||||
if log.get('twenty_feet'):
|
||||
ships[ship]['twenty_feet'] += log['twenty_feet']
|
||||
if log.get('forty_feet'):
|
||||
ships[ship]['forty_feet'] += log['forty_feet']
|
||||
|
||||
total_teu = sum(ship_data['teu'] for ship_data in ships.values())
|
||||
|
||||
return {
|
||||
'date': date,
|
||||
'ships': ships,
|
||||
'total_teu': total_teu,
|
||||
'ship_count': len(ships),
|
||||
'adjustments': [],
|
||||
'total_adjustment_teu': 0
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取每日数据失败: {date}, 错误: {e}")
|
||||
return {
|
||||
'date': date,
|
||||
'ships': {},
|
||||
'total_teu': 0,
|
||||
'ship_count': 0,
|
||||
'adjustments': [],
|
||||
'total_adjustment_teu': 0
|
||||
}
|
||||
|
||||
def get_monthly_stats(self, date: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取月度统计(截止到指定日期)
|
||||
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||
|
||||
返回:
|
||||
月度统计字典
|
||||
"""
|
||||
try:
|
||||
year_month = date[:7] # YYYY-MM
|
||||
target_date = datetime.strptime(date, '%Y-%m-%d').date()
|
||||
|
||||
logs = self.db.query_all(limit=10000)
|
||||
|
||||
# 只统计当月且在指定日期之前的数据
|
||||
monthly_logs = [
|
||||
log for log in logs
|
||||
if log['date'].startswith(year_month)
|
||||
and datetime.strptime(log['date'], '%Y-%m-%d').date() <= target_date
|
||||
]
|
||||
|
||||
# 按日期汇总原始数据
|
||||
daily_totals: Dict[str, int] = {}
|
||||
for log in monthly_logs:
|
||||
d = log['date']
|
||||
if d not in daily_totals:
|
||||
daily_totals[d] = 0
|
||||
if log.get('teu'):
|
||||
daily_totals[d] += log['teu']
|
||||
|
||||
# 获取当月所有日期的调整数据
|
||||
total_adjustment_teu = 0
|
||||
adjustment_details: Dict[str, Dict[str, int]] = {}
|
||||
|
||||
# 获取当月所有日期的调整数据
|
||||
for day in range(1, target_date.day + 1):
|
||||
day_str = f"{year_month}-{day:02d}"
|
||||
if day_str <= date: # 只统计到指定日期
|
||||
# 获取该日期的调整数据
|
||||
if hasattr(self.db, 'get_daily_data_with_adjustments'):
|
||||
daily_data = self.db.get_daily_data_with_adjustments(day_str)
|
||||
adjustment_teu = daily_data.get('total_adjustment_teu', 0)
|
||||
if adjustment_teu != 0:
|
||||
total_adjustment_teu += adjustment_teu
|
||||
adjustment_details[day_str] = {
|
||||
'adjustment_teu': adjustment_teu,
|
||||
'total_teu': daily_data.get('total_teu', 0)
|
||||
}
|
||||
|
||||
# 计算当月天数(已过的天数)
|
||||
current_date = datetime.strptime(date, '%Y-%m-%d')
|
||||
if current_date.day == config.FIRST_DAY_OF_MONTH_SPECIAL:
|
||||
days_passed = 1
|
||||
else:
|
||||
days_passed = current_date.day
|
||||
|
||||
# 获取未统计数据
|
||||
unaccounted = self.db.get_unaccounted(year_month)
|
||||
|
||||
planned = days_passed * config.DAILY_TARGET_TEU
|
||||
# 实际作业量 = 原始数据总计 + 未统计数据 + 调整数据总计
|
||||
actual = sum(daily_totals.values()) + unaccounted + total_adjustment_teu
|
||||
|
||||
completion = round(actual / planned * 100, 2) if planned > 0 else 0
|
||||
|
||||
return {
|
||||
'year_month': year_month,
|
||||
'days_passed': days_passed,
|
||||
'planned': planned,
|
||||
'actual': actual,
|
||||
'unaccounted': unaccounted,
|
||||
'adjustment_total': total_adjustment_teu,
|
||||
'completion': completion,
|
||||
'daily_totals': daily_totals,
|
||||
'adjustment_details': adjustment_details
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取月度统计失败: {date}, 错误: {e}")
|
||||
return {
|
||||
'year_month': date[:7],
|
||||
'days_passed': 0,
|
||||
'planned': 0,
|
||||
'actual': 0,
|
||||
'unaccounted': 0,
|
||||
'adjustment_total': 0,
|
||||
'completion': 0,
|
||||
'daily_totals': {},
|
||||
'adjustment_details': {}
|
||||
}
|
||||
|
||||
def get_shift_personnel(self, date: str) -> Dict[str, str]:
|
||||
"""
|
||||
获取班次人员(从飞书排班表获取)
|
||||
|
||||
注意:日报中显示的是次日的班次人员,所以需要获取 date+1 的排班
|
||||
例如:生成 12/29 的日报,显示的是 12/30 的人员
|
||||
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||
|
||||
返回:
|
||||
班次人员字典
|
||||
"""
|
||||
try:
|
||||
# 检查飞书配置(支持应用凭证和手动token两种方式)
|
||||
has_feishu_config = bool(config.FEISHU_SPREADSHEET_TOKEN) and (
|
||||
bool(config.FEISHU_APP_ID and config.FEISHU_APP_SECRET) or
|
||||
bool(config.FEISHU_TOKEN)
|
||||
)
|
||||
|
||||
if not has_feishu_config:
|
||||
logger.warning("飞书配置不完整,跳过排班信息获取")
|
||||
logger.warning("需要配置 FEISHU_SPREADSHEET_TOKEN 和 (FEISHU_APP_ID+FEISHU_APP_SECRET 或 FEISHU_TOKEN)")
|
||||
return self._empty_personnel()
|
||||
|
||||
# 初始化飞书排班管理器
|
||||
manager = FeishuScheduleManager()
|
||||
|
||||
# 计算次日日期(日报中显示的是次日班次)
|
||||
parsed_date = datetime.strptime(date, '%Y-%m-%d')
|
||||
tomorrow = (parsed_date + timedelta(days=1)).strftime('%Y-%m-%d')
|
||||
|
||||
logger.info(f"获取 {date} 日报的班次人员,对应排班表日期: {tomorrow}")
|
||||
|
||||
# 获取次日的排班信息
|
||||
schedule = manager.get_schedule_for_date(tomorrow)
|
||||
|
||||
# 如果从飞书获取到数据,使用飞书数据
|
||||
if schedule.get('day_shift') or schedule.get('night_shift'):
|
||||
return {
|
||||
'day_shift': schedule.get('day_shift', ''),
|
||||
'night_shift': schedule.get('night_shift', ''),
|
||||
'duty_phone': config.DUTY_PHONE
|
||||
}
|
||||
|
||||
# 如果飞书数据为空,返回空值
|
||||
logger.warning(f"无法从飞书获取 {tomorrow} 的排班信息")
|
||||
return self._empty_personnel()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取排班信息失败: {e}")
|
||||
# 降级处理:返回空值
|
||||
return self._empty_personnel()
|
||||
|
||||
def generate_report(self, date: Optional[str] = None) -> str:
|
||||
"""
|
||||
生成日报
|
||||
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD",如果为None则使用最新日期
|
||||
|
||||
返回:
|
||||
日报文本
|
||||
|
||||
异常:
|
||||
ReportGeneratorError: 生成失败
|
||||
"""
|
||||
try:
|
||||
if not date:
|
||||
date = self.get_latest_date()
|
||||
|
||||
# 验证日期格式
|
||||
try:
|
||||
parsed = datetime.strptime(date, '%Y-%m-%d')
|
||||
display_date = parsed.strftime('%m/%d')
|
||||
query_date = parsed.strftime('%Y-%m-%d')
|
||||
except ValueError as e:
|
||||
error_msg = f"日期格式无效: {date}, 错误: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ReportGeneratorError(error_msg) from e
|
||||
|
||||
# 获取数据
|
||||
daily_data = self.get_daily_data(query_date)
|
||||
monthly_data = self.get_monthly_stats(query_date)
|
||||
personnel = self.get_shift_personnel(query_date)
|
||||
|
||||
# 生成日报
|
||||
lines: List[str] = []
|
||||
lines.append(f"日期:{display_date}")
|
||||
lines.append("")
|
||||
|
||||
# 船次信息
|
||||
if daily_data['ships']:
|
||||
ship_lines: List[str] = []
|
||||
for ship, ship_data in sorted(daily_data['ships'].items(), key=lambda x: -x[1]['teu']):
|
||||
ship_lines.append(f"船名:{ship}")
|
||||
teu = ship_data['teu']
|
||||
twenty_feet = ship_data.get('twenty_feet', 0)
|
||||
forty_feet = ship_data.get('forty_feet', 0)
|
||||
|
||||
# 构建尺寸箱量字符串
|
||||
size_parts = []
|
||||
if twenty_feet > 0:
|
||||
size_parts.append(f"20尺*{twenty_feet}")
|
||||
if forty_feet > 0:
|
||||
size_parts.append(f"40尺*{forty_feet}")
|
||||
|
||||
if size_parts:
|
||||
size_str = " ".join(size_parts)
|
||||
ship_lines.append(f"作业量:{teu}TEU({size_str})")
|
||||
else:
|
||||
ship_lines.append(f"作业量:{teu}TEU")
|
||||
|
||||
lines.extend(ship_lines)
|
||||
lines.append("")
|
||||
|
||||
# 当日实际作业量
|
||||
lines.append(f"当日实际作业量:{daily_data['total_teu']}TEU")
|
||||
|
||||
# 月度统计
|
||||
lines.append(f"当月计划作业量:{monthly_data['planned']}TEU (用天数*{config.DAILY_TARGET_TEU}TEU)")
|
||||
lines.append(f"当月实际作业量:{monthly_data['actual']}TEU")
|
||||
lines.append(f"当月完成比例:{monthly_data['completion']}%")
|
||||
lines.append("")
|
||||
|
||||
# 人员信息
|
||||
day_personnel = personnel['day_shift']
|
||||
night_personnel = personnel['night_shift']
|
||||
duty_phone = personnel['duty_phone']
|
||||
|
||||
# 班次日期使用次日
|
||||
next_day = (parsed + timedelta(days=1)).strftime('%m/%d')
|
||||
lines.append(f"{next_day} 白班人员:{day_personnel}")
|
||||
lines.append(f"{next_day} 夜班人员:{night_personnel}")
|
||||
lines.append(f"24小时值班手机:{duty_phone}")
|
||||
|
||||
report = "\n".join(lines)
|
||||
logger.info(f"日报生成完成: {date}")
|
||||
return report
|
||||
|
||||
except ReportGeneratorError:
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"生成日报失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ReportGeneratorError(error_msg) from e
|
||||
|
||||
def print_report(self, date: Optional[str] = None) -> str:
|
||||
"""
|
||||
打印日报
|
||||
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD",如果为None则使用最新日期
|
||||
|
||||
返回:
|
||||
日报文本
|
||||
"""
|
||||
try:
|
||||
report = self.generate_report(date)
|
||||
print(report)
|
||||
return report
|
||||
|
||||
except ReportGeneratorError as e:
|
||||
print(f"生成日报失败: {e}")
|
||||
return ""
|
||||
|
||||
def save_report_to_file(self, date: Optional[str] = None, filepath: Optional[str] = None) -> bool:
|
||||
"""
|
||||
保存日报到文件
|
||||
|
||||
参数:
|
||||
date: 日期字符串,如果为None则使用最新日期
|
||||
filepath: 文件路径,如果为None则使用默认路径
|
||||
|
||||
返回:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
report = self.generate_report(date)
|
||||
|
||||
if filepath is None:
|
||||
# 使用默认路径
|
||||
import os
|
||||
report_dir = "reports"
|
||||
os.makedirs(report_dir, exist_ok=True)
|
||||
|
||||
if date is None:
|
||||
date = self.get_latest_date()
|
||||
filename = f"daily_report_{date}.txt"
|
||||
filepath = os.path.join(report_dir, filename)
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(report)
|
||||
|
||||
logger.info(f"日报已保存到文件: {filepath}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存日报到文件失败: {e}")
|
||||
return False
|
||||
|
||||
def _empty_personnel(self) -> Dict[str, str]:
|
||||
"""返回空的人员信息"""
|
||||
return {
|
||||
'day_shift': '',
|
||||
'night_shift': '',
|
||||
'duty_phone': config.DUTY_PHONE
|
||||
}
|
||||
|
||||
def close(self):
|
||||
"""关闭数据库连接"""
|
||||
self.db.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
generator = DailyReportGenerator()
|
||||
|
||||
try:
|
||||
# 测试获取最新日期
|
||||
latest_date = generator.get_latest_date()
|
||||
print(f"最新日期: {latest_date}")
|
||||
|
||||
# 测试生成日报
|
||||
report = generator.generate_report(latest_date)
|
||||
print(f"\n日报内容:\n{report}")
|
||||
|
||||
# 测试保存到文件
|
||||
success = generator.save_report_to_file(latest_date)
|
||||
print(f"\n保存到文件: {'成功' if success else '失败'}")
|
||||
|
||||
except ReportGeneratorError as e:
|
||||
print(f"日报生成错误: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"未知错误: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
generator.close()
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
日报生成模块
|
||||
更新依赖,使用新的配置和数据库模块
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any
|
||||
import logging
|
||||
|
||||
from src.config import config
|
||||
from src.logging_config import get_logger
|
||||
from src.database.daily_logs import DailyLogsDatabase
|
||||
from src.feishu.manager import FeishuScheduleManager
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ReportGeneratorError(Exception):
|
||||
"""日报生成错误"""
|
||||
pass
|
||||
|
||||
|
||||
class DailyReportGenerator:
|
||||
"""每日作业报告生成器"""
|
||||
|
||||
def __init__(self, db_path: Optional[str] = None):
|
||||
"""
|
||||
初始化日报生成器
|
||||
|
||||
参数:
|
||||
db_path: 数据库文件路径,如果为None则使用配置
|
||||
"""
|
||||
self.db = DailyLogsDatabase(db_path)
|
||||
logger.info("日报生成器初始化完成")
|
||||
|
||||
def get_latest_date(self) -> str:
|
||||
"""
|
||||
获取数据库中最新的日期
|
||||
|
||||
返回:
|
||||
最新日期字符串,格式 "YYYY-MM-DD"
|
||||
"""
|
||||
try:
|
||||
logs = self.db.query_all(limit=1)
|
||||
if logs:
|
||||
return logs[0]['date']
|
||||
return datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取最新日期失败: {e}")
|
||||
return datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
def get_daily_data(self, date: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取指定日期的数据(包含手动调整)
|
||||
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||
|
||||
返回:
|
||||
每日数据字典
|
||||
"""
|
||||
try:
|
||||
# 使用数据库的新方法获取包含调整的数据
|
||||
if hasattr(self.db, 'get_daily_data_with_adjustments'):
|
||||
return self.db.get_daily_data_with_adjustments(date)
|
||||
|
||||
# 降级处理:如果没有新方法,使用原始逻辑
|
||||
logs = self.db.query_by_date(date)
|
||||
|
||||
# 按船名汇总TEU和尺寸箱量
|
||||
ships: Dict[str, Dict[str, Any]] = {}
|
||||
for log in logs:
|
||||
ship = log['ship_name']
|
||||
if ship not in ships:
|
||||
ships[ship] = {
|
||||
'teu': 0,
|
||||
'twenty_feet': 0,
|
||||
'forty_feet': 0
|
||||
}
|
||||
if log.get('teu'):
|
||||
ships[ship]['teu'] += log['teu']
|
||||
if log.get('twenty_feet'):
|
||||
ships[ship]['twenty_feet'] += log['twenty_feet']
|
||||
if log.get('forty_feet'):
|
||||
ships[ship]['forty_feet'] += log['forty_feet']
|
||||
|
||||
total_teu = sum(ship_data['teu'] for ship_data in ships.values())
|
||||
|
||||
return {
|
||||
'date': date,
|
||||
'ships': ships,
|
||||
'total_teu': total_teu,
|
||||
'ship_count': len(ships),
|
||||
'adjustments': [],
|
||||
'total_adjustment_teu': 0
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取每日数据失败: {date}, 错误: {e}")
|
||||
return {
|
||||
'date': date,
|
||||
'ships': {},
|
||||
'total_teu': 0,
|
||||
'ship_count': 0,
|
||||
'adjustments': [],
|
||||
'total_adjustment_teu': 0
|
||||
}
|
||||
|
||||
def get_monthly_stats(self, date: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取月度统计(截止到指定日期)
|
||||
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||
|
||||
返回:
|
||||
月度统计字典
|
||||
"""
|
||||
try:
|
||||
year_month = date[:7] # YYYY-MM
|
||||
target_date = datetime.strptime(date, '%Y-%m-%d').date()
|
||||
|
||||
logs = self.db.query_all(limit=10000)
|
||||
|
||||
# 只统计当月且在指定日期之前的数据
|
||||
monthly_logs = [
|
||||
log for log in logs
|
||||
if log['date'].startswith(year_month)
|
||||
and datetime.strptime(log['date'], '%Y-%m-%d').date() <= target_date
|
||||
]
|
||||
|
||||
# 按日期汇总原始数据
|
||||
daily_totals: Dict[str, int] = {}
|
||||
for log in monthly_logs:
|
||||
d = log['date']
|
||||
if d not in daily_totals:
|
||||
daily_totals[d] = 0
|
||||
if log.get('teu'):
|
||||
daily_totals[d] += log['teu']
|
||||
|
||||
# 计算当月天数(已过的天数)
|
||||
current_date = datetime.strptime(date, '%Y-%m-%d')
|
||||
if current_date.day == config.FIRST_DAY_OF_MONTH_SPECIAL:
|
||||
days_passed = 1
|
||||
else:
|
||||
days_passed = current_date.day
|
||||
|
||||
# 获取当月所有日期的调整数据
|
||||
total_adjustment_teu = 0
|
||||
adjustment_details: Dict[str, Dict[str, int]] = {}
|
||||
|
||||
# 直接获取整个月份的所有调整记录
|
||||
# 这样可以确保即使在生成日报之后执行的剔除操作也能影响当月实际作业量
|
||||
if hasattr(self.db, 'get_monthly_adjustments'):
|
||||
monthly_adjustments = self.db.get_monthly_adjustments(year_month)
|
||||
|
||||
# 按日期汇总调整数据
|
||||
date_adjustments: Dict[str, int] = {}
|
||||
for adj in monthly_adjustments:
|
||||
adj_date = adj['date']
|
||||
if adj_date not in date_adjustments:
|
||||
date_adjustments[adj_date] = 0
|
||||
if adj['adjustment_type'] == 'add':
|
||||
date_adjustments[adj_date] += adj['teu']
|
||||
elif adj['adjustment_type'] == 'exclude':
|
||||
date_adjustments[adj_date] -= adj['teu']
|
||||
|
||||
# 计算总调整量并构建调整详情
|
||||
for adj_date, adj_teu in date_adjustments.items():
|
||||
if adj_teu != 0:
|
||||
total_adjustment_teu += adj_teu
|
||||
# 获取该日期的详细数据
|
||||
if hasattr(self.db, 'get_daily_data_with_adjustments'):
|
||||
daily_data = self.db.get_daily_data_with_adjustments(adj_date)
|
||||
adjustment_details[adj_date] = {
|
||||
'adjustment_teu': adj_teu,
|
||||
'total_teu': daily_data.get('total_teu', 0)
|
||||
}
|
||||
else:
|
||||
# 降级处理:如果没有新方法,使用按天循环查询
|
||||
days_in_month = target_date.day
|
||||
if current_date.day == config.FIRST_DAY_OF_MONTH_SPECIAL:
|
||||
days_in_month = 1
|
||||
|
||||
for day in range(1, days_in_month + 1):
|
||||
day_str = f"{year_month}-{day:02d}"
|
||||
# 获取该日期的调整数据
|
||||
if hasattr(self.db, 'get_daily_data_with_adjustments'):
|
||||
daily_data = self.db.get_daily_data_with_adjustments(day_str)
|
||||
adjustment_teu = daily_data.get('total_adjustment_teu', 0)
|
||||
if adjustment_teu != 0:
|
||||
total_adjustment_teu += adjustment_teu
|
||||
adjustment_details[day_str] = {
|
||||
'adjustment_teu': adjustment_teu,
|
||||
'total_teu': daily_data.get('total_teu', 0)
|
||||
}
|
||||
|
||||
# 获取未统计数据
|
||||
unaccounted = self.db.get_unaccounted(year_month)
|
||||
|
||||
planned = days_passed * config.DAILY_TARGET_TEU
|
||||
# 实际作业量 = 原始数据总计 + 未统计数据 + 调整数据总计
|
||||
actual = sum(daily_totals.values()) + unaccounted + total_adjustment_teu
|
||||
|
||||
completion = round(actual / planned * 100, 2) if planned > 0 else 0
|
||||
|
||||
return {
|
||||
'year_month': year_month,
|
||||
'days_passed': days_passed,
|
||||
'planned': planned,
|
||||
'actual': actual,
|
||||
'unaccounted': unaccounted,
|
||||
'adjustment_total': total_adjustment_teu,
|
||||
'completion': completion,
|
||||
'daily_totals': daily_totals,
|
||||
'adjustment_details': adjustment_details
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取月度统计失败: {date}, 错误: {e}")
|
||||
return {
|
||||
'year_month': date[:7],
|
||||
'days_passed': 0,
|
||||
'planned': 0,
|
||||
'actual': 0,
|
||||
'unaccounted': 0,
|
||||
'adjustment_total': 0,
|
||||
'completion': 0,
|
||||
'daily_totals': {},
|
||||
'adjustment_details': {}
|
||||
}
|
||||
|
||||
def get_shift_personnel(self, date: str) -> Dict[str, str]:
|
||||
"""
|
||||
获取班次人员(从飞书排班表获取)
|
||||
|
||||
注意:日报中显示的是次日的班次人员,所以需要获取 date+1 的排班
|
||||
例如:生成 12/29 的日报,显示的是 12/30 的人员
|
||||
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||
|
||||
返回:
|
||||
班次人员字典
|
||||
"""
|
||||
try:
|
||||
# 检查飞书配置(支持应用凭证和手动token两种方式)
|
||||
has_feishu_config = bool(config.FEISHU_SPREADSHEET_TOKEN) and (
|
||||
bool(config.FEISHU_APP_ID and config.FEISHU_APP_SECRET) or
|
||||
bool(config.FEISHU_TOKEN)
|
||||
)
|
||||
|
||||
if not has_feishu_config:
|
||||
logger.warning("飞书配置不完整,跳过排班信息获取")
|
||||
logger.warning("需要配置 FEISHU_SPREADSHEET_TOKEN 和 (FEISHU_APP_ID+FEISHU_APP_SECRET 或 FEISHU_TOKEN)")
|
||||
return self._empty_personnel()
|
||||
|
||||
# 初始化飞书排班管理器
|
||||
manager = FeishuScheduleManager()
|
||||
|
||||
# 计算次日日期(日报中显示的是次日班次)
|
||||
parsed_date = datetime.strptime(date, '%Y-%m-%d')
|
||||
tomorrow = (parsed_date + timedelta(days=1)).strftime('%Y-%m-%d')
|
||||
|
||||
logger.info(f"获取 {date} 日报的班次人员,对应排班表日期: {tomorrow}")
|
||||
|
||||
# 获取次日的排班信息
|
||||
schedule = manager.get_schedule_for_date(tomorrow)
|
||||
|
||||
# 如果从飞书获取到数据,使用飞书数据
|
||||
if schedule.get('day_shift') or schedule.get('night_shift'):
|
||||
return {
|
||||
'day_shift': schedule.get('day_shift', ''),
|
||||
'night_shift': schedule.get('night_shift', ''),
|
||||
'duty_phone': config.DUTY_PHONE
|
||||
}
|
||||
|
||||
# 如果飞书数据为空,返回空值
|
||||
logger.warning(f"无法从飞书获取 {tomorrow} 的排班信息")
|
||||
return self._empty_personnel()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取排班信息失败: {e}")
|
||||
# 降级处理:返回空值
|
||||
return self._empty_personnel()
|
||||
|
||||
def generate_report(self, date: Optional[str] = None) -> str:
|
||||
"""
|
||||
生成日报
|
||||
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD",如果为None则使用最新日期
|
||||
|
||||
返回:
|
||||
日报文本
|
||||
|
||||
异常:
|
||||
ReportGeneratorError: 生成失败
|
||||
"""
|
||||
try:
|
||||
if not date:
|
||||
date = self.get_latest_date()
|
||||
|
||||
# 验证日期格式
|
||||
try:
|
||||
parsed = datetime.strptime(date, '%Y-%m-%d')
|
||||
display_date = parsed.strftime('%m/%d')
|
||||
query_date = parsed.strftime('%Y-%m-%d')
|
||||
except ValueError as e:
|
||||
error_msg = f"日期格式无效: {date}, 错误: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ReportGeneratorError(error_msg) from e
|
||||
|
||||
# 获取数据
|
||||
daily_data = self.get_daily_data(query_date)
|
||||
monthly_data = self.get_monthly_stats(query_date)
|
||||
personnel = self.get_shift_personnel(query_date)
|
||||
|
||||
# 生成日报
|
||||
lines: List[str] = []
|
||||
lines.append(f"日期:{display_date}")
|
||||
lines.append("")
|
||||
|
||||
# 船次信息
|
||||
if daily_data['ships']:
|
||||
ship_lines: List[str] = []
|
||||
for ship, ship_data in sorted(daily_data['ships'].items(), key=lambda x: -x[1]['teu']):
|
||||
ship_lines.append(f"船名:{ship}")
|
||||
teu = ship_data['teu']
|
||||
twenty_feet = ship_data.get('twenty_feet', 0)
|
||||
forty_feet = ship_data.get('forty_feet', 0)
|
||||
|
||||
# 构建尺寸箱量字符串
|
||||
size_parts = []
|
||||
if twenty_feet > 0:
|
||||
size_parts.append(f"20尺*{twenty_feet}")
|
||||
if forty_feet > 0:
|
||||
size_parts.append(f"40尺*{forty_feet}")
|
||||
|
||||
if size_parts:
|
||||
size_str = " ".join(size_parts)
|
||||
ship_lines.append(f"作业量:{teu}TEU({size_str})")
|
||||
else:
|
||||
ship_lines.append(f"作业量:{teu}TEU")
|
||||
|
||||
lines.extend(ship_lines)
|
||||
lines.append("")
|
||||
|
||||
# 当日实际作业量
|
||||
lines.append(f"当日实际作业量:{daily_data['total_teu']}TEU")
|
||||
|
||||
# 月度统计
|
||||
lines.append(f"当月计划作业量:{monthly_data['planned']}TEU (用天数*{config.DAILY_TARGET_TEU}TEU)")
|
||||
lines.append(f"当月实际作业量:{monthly_data['actual']}TEU")
|
||||
lines.append(f"当月完成比例:{monthly_data['completion']}%")
|
||||
lines.append("")
|
||||
|
||||
# 人员信息
|
||||
day_personnel = personnel['day_shift']
|
||||
night_personnel = personnel['night_shift']
|
||||
duty_phone = personnel['duty_phone']
|
||||
|
||||
# 班次日期使用次日
|
||||
next_day = (parsed + timedelta(days=1)).strftime('%m/%d')
|
||||
lines.append(f"{next_day} 白班人员:{day_personnel}")
|
||||
lines.append(f"{next_day} 夜班人员:{night_personnel}")
|
||||
lines.append(f"24小时值班手机:{duty_phone}")
|
||||
|
||||
report = "\n".join(lines)
|
||||
logger.info(f"日报生成完成: {date}")
|
||||
return report
|
||||
|
||||
except ReportGeneratorError:
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"生成日报失败: {e}"
|
||||
logger.error(error_msg)
|
||||
raise ReportGeneratorError(error_msg) from e
|
||||
|
||||
def print_report(self, date: Optional[str] = None) -> str:
|
||||
"""
|
||||
打印日报
|
||||
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD",如果为None则使用最新日期
|
||||
|
||||
返回:
|
||||
日报文本
|
||||
"""
|
||||
try:
|
||||
report = self.generate_report(date)
|
||||
print(report)
|
||||
return report
|
||||
|
||||
except ReportGeneratorError as e:
|
||||
print(f"生成日报失败: {e}")
|
||||
return ""
|
||||
|
||||
def save_report_to_file(self, date: Optional[str] = None, filepath: Optional[str] = None) -> bool:
|
||||
"""
|
||||
保存日报到文件
|
||||
|
||||
参数:
|
||||
date: 日期字符串,如果为None则使用最新日期
|
||||
filepath: 文件路径,如果为None则使用默认路径
|
||||
|
||||
返回:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
report = self.generate_report(date)
|
||||
|
||||
if filepath is None:
|
||||
# 使用默认路径
|
||||
import os
|
||||
report_dir = "reports"
|
||||
os.makedirs(report_dir, exist_ok=True)
|
||||
|
||||
if date is None:
|
||||
date = self.get_latest_date()
|
||||
filename = f"daily_report_{date}.txt"
|
||||
filepath = os.path.join(report_dir, filename)
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(report)
|
||||
|
||||
logger.info(f"日报已保存到文件: {filepath}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存日报到文件失败: {e}")
|
||||
return False
|
||||
|
||||
def _empty_personnel(self) -> Dict[str, str]:
|
||||
"""返回空的人员信息"""
|
||||
return {
|
||||
'day_shift': '',
|
||||
'night_shift': '',
|
||||
'duty_phone': config.DUTY_PHONE
|
||||
}
|
||||
|
||||
def close(self):
|
||||
"""关闭数据库连接"""
|
||||
self.db.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
generator = DailyReportGenerator()
|
||||
|
||||
try:
|
||||
# 测试获取最新日期
|
||||
latest_date = generator.get_latest_date()
|
||||
print(f"最新日期: {latest_date}")
|
||||
|
||||
# 测试生成日报
|
||||
report = generator.generate_report(latest_date)
|
||||
print(f"\n日报内容:\n{report}")
|
||||
|
||||
# 测试保存到文件
|
||||
success = generator.save_report_to_file(latest_date)
|
||||
print(f"\n保存到文件: {'成功' if success else '失败'}")
|
||||
|
||||
except ReportGeneratorError as e:
|
||||
print(f"日报生成错误: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"未知错误: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
generator.close()
|
||||
|
||||
356
src/retry.py
Normal file
356
src/retry.py
Normal file
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
重试机制模块
|
||||
提供重试装饰器和工具函数
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from functools import wraps
|
||||
from typing import Callable, Optional, Type, Tuple, Any
|
||||
|
||||
from src.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def retry(
|
||||
max_attempts: int = 3,
|
||||
delay: float = 1.0,
|
||||
backoff_factor: float = 2.0,
|
||||
exceptions: Optional[Tuple[Type[Exception], ...]] = None,
|
||||
on_retry: Optional[Callable[[int, Exception], None]] = None
|
||||
) -> Callable:
|
||||
"""
|
||||
重试装饰器
|
||||
|
||||
参数:
|
||||
max_attempts: 最大重试次数
|
||||
delay: 初始延迟时间(秒)
|
||||
backoff_factor: 退避因子,每次重试延迟时间乘以该因子
|
||||
exceptions: 要捕获的异常类型,None表示捕获所有异常
|
||||
on_retry: 重试时的回调函数,参数为 (attempt, exception)
|
||||
|
||||
使用示例:
|
||||
@retry(max_attempts=3, delay=2.0, backoff_factor=2.0)
|
||||
def fetch_data():
|
||||
# 可能失败的代码
|
||||
pass
|
||||
|
||||
@retry(max_attempts=5, exceptions=(ConnectionError, TimeoutError))
|
||||
def network_request():
|
||||
# 网络请求代码
|
||||
pass
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs) -> Any:
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
# 检查是否需要捕获此异常
|
||||
if exceptions and not isinstance(e, exceptions):
|
||||
raise
|
||||
|
||||
last_exception = e
|
||||
|
||||
# 如果是最后一次尝试,不再重试
|
||||
if attempt == max_attempts - 1:
|
||||
logger.error(
|
||||
f"{func.__name__} 在 {max_attempts} 次尝试后仍然失败: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
# 计算延迟时间
|
||||
current_delay = delay * (backoff_factor ** attempt)
|
||||
|
||||
logger.warning(
|
||||
f"{func.__name__} 第 {attempt + 1} 次尝试失败: {e}, "
|
||||
f"{current_delay:.2f}秒后重试..."
|
||||
)
|
||||
|
||||
# 调用重试回调
|
||||
if on_retry:
|
||||
try:
|
||||
on_retry(attempt + 1, e)
|
||||
except Exception as callback_error:
|
||||
logger.error(f"重试回调执行失败: {callback_error}")
|
||||
|
||||
# 等待
|
||||
time.sleep(current_delay)
|
||||
|
||||
# 理论上不会到达这里,但为了类型检查
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def retry_with_exponential_backoff(
|
||||
max_attempts: int = 3,
|
||||
initial_delay: float = 1.0,
|
||||
max_delay: float = 60.0
|
||||
) -> Callable:
|
||||
"""
|
||||
使用指数退避的重试装饰器
|
||||
|
||||
参数:
|
||||
max_attempts: 最大重试次数
|
||||
initial_delay: 初始延迟时间(秒)
|
||||
max_delay: 最大延迟时间(秒)
|
||||
|
||||
使用示例:
|
||||
@retry_with_exponential_backoff(max_attempts=5, initial_delay=2.0)
|
||||
def api_call():
|
||||
# API调用代码
|
||||
pass
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs) -> Any:
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
|
||||
if attempt == max_attempts - 1:
|
||||
logger.error(
|
||||
f"{func.__name__} 在 {max_attempts} 次尝试后仍然失败: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
# 计算延迟时间(指数退避,但不超过最大延迟)
|
||||
current_delay = min(initial_delay * (2 ** attempt), max_delay)
|
||||
|
||||
logger.warning(
|
||||
f"{func.__name__} 第 {attempt + 1} 次尝试失败: {e}, "
|
||||
f"{current_delay:.2f}秒后重试..."
|
||||
)
|
||||
|
||||
time.sleep(current_delay)
|
||||
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def retry_on_exception(
|
||||
exception_type: Type[Exception],
|
||||
max_attempts: int = 3,
|
||||
delay: float = 1.0
|
||||
) -> Callable:
|
||||
"""
|
||||
只在特定异常时重试的装饰器
|
||||
|
||||
参数:
|
||||
exception_type: 要捕获的异常类型
|
||||
max_attempts: 最大重试次数
|
||||
delay: 延迟时间(秒)
|
||||
|
||||
使用示例:
|
||||
@retry_on_exception(ConnectionError, max_attempts=5, delay=2.0)
|
||||
def fetch_data():
|
||||
# 可能抛出 ConnectionError 的代码
|
||||
pass
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs) -> Any:
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except exception_type as e:
|
||||
last_exception = e
|
||||
|
||||
if attempt == max_attempts - 1:
|
||||
logger.error(
|
||||
f"{func.__name__} 在 {max_attempts} 次尝试后仍然失败: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
logger.warning(
|
||||
f"{func.__name__} 第 {attempt + 1} 次尝试失败: {e}, "
|
||||
f"{delay:.2f}秒后重试..."
|
||||
)
|
||||
|
||||
time.sleep(delay)
|
||||
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class RetryContext:
|
||||
"""重试上下文管理器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
operation_name: str,
|
||||
max_attempts: int = 3,
|
||||
delay: float = 1.0,
|
||||
backoff_factor: float = 2.0,
|
||||
exceptions: Optional[Tuple[Type[Exception], ...]] = None
|
||||
):
|
||||
"""
|
||||
初始化重试上下文
|
||||
|
||||
参数:
|
||||
operation_name: 操作名称
|
||||
max_attempts: 最大重试次数
|
||||
delay: 初始延迟时间(秒)
|
||||
backoff_factor: 退避因子
|
||||
exceptions: 要捕获的异常类型
|
||||
"""
|
||||
self.operation_name = operation_name
|
||||
self.max_attempts = max_attempts
|
||||
self.delay = delay
|
||||
self.backoff_factor = backoff_factor
|
||||
self.exceptions = exceptions
|
||||
self.attempt = 0
|
||||
|
||||
def __enter__(self):
|
||||
self.attempt = 0
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type is None:
|
||||
return False
|
||||
|
||||
# 检查是否需要捕获此异常
|
||||
if self.exceptions and not isinstance(exc_val, self.exceptions):
|
||||
return False
|
||||
|
||||
self.attempt += 1
|
||||
|
||||
# 如果超过最大尝试次数,不再重试
|
||||
if self.attempt >= self.max_attempts:
|
||||
logger.error(
|
||||
f"{self.operation_name} 在 {self.max_attempts} 次尝试后仍然失败: {exc_val}"
|
||||
)
|
||||
return False
|
||||
|
||||
# 计算延迟时间
|
||||
current_delay = self.delay * (self.backoff_factor ** (self.attempt - 1))
|
||||
|
||||
logger.warning(
|
||||
f"{self.operation_name} 第 {self.attempt} 次尝试失败: {exc_val}, "
|
||||
f"{current_delay:.2f}秒后重试..."
|
||||
)
|
||||
|
||||
# 等待
|
||||
time.sleep(current_delay)
|
||||
|
||||
# 抑制异常,继续重试
|
||||
return True
|
||||
|
||||
|
||||
def async_retry(
|
||||
max_attempts: int = 3,
|
||||
delay: float = 1.0,
|
||||
backoff_factor: float = 2.0
|
||||
) -> Callable:
|
||||
"""
|
||||
异步重试装饰器(用于异步函数)
|
||||
|
||||
参数:
|
||||
max_attempts: 最大重试次数
|
||||
delay: 初始延迟时间(秒)
|
||||
backoff_factor: 退避因子
|
||||
|
||||
使用示例:
|
||||
@async_retry(max_attempts=3, delay=2.0)
|
||||
async def async_fetch_data():
|
||||
# 异步代码
|
||||
pass
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs) -> Any:
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
|
||||
if attempt == max_attempts - 1:
|
||||
logger.error(
|
||||
f"{func.__name__} 在 {max_attempts} 次尝试后仍然失败: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
# 计算延迟时间
|
||||
current_delay = delay * (backoff_factor ** attempt)
|
||||
|
||||
logger.warning(
|
||||
f"{func.__name__} 第 {attempt + 1} 次尝试失败: {e}, "
|
||||
f"{current_delay:.2f}秒后重试..."
|
||||
)
|
||||
|
||||
# 异步等待
|
||||
await asyncio.sleep(current_delay)
|
||||
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
|
||||
# 测试重试装饰器
|
||||
call_count = 0
|
||||
|
||||
@retry(max_attempts=3, delay=0.1)
|
||||
def test_retry():
|
||||
global call_count
|
||||
call_count += 1
|
||||
print(f"调用次数: {call_count}")
|
||||
if call_count < 3:
|
||||
raise ValueError("测试异常")
|
||||
return "成功"
|
||||
|
||||
result = test_retry()
|
||||
print(f"测试结果: {result}")
|
||||
|
||||
# 测试重试上下文管理器
|
||||
context_call_count = 0
|
||||
|
||||
def test_context_operation():
|
||||
global context_call_count
|
||||
context_call_count += 1
|
||||
print(f"上下文调用次数: {context_call_count}")
|
||||
if context_call_count < 3:
|
||||
raise ValueError("测试异常")
|
||||
return "成功"
|
||||
|
||||
with RetryContext("测试操作", max_attempts=3, delay=0.1):
|
||||
result = test_context_operation()
|
||||
print(f"上下文测试结果: {result}")
|
||||
|
||||
# 测试特定异常重试
|
||||
@retry_on_exception(ValueError, max_attempts=3, delay=0.1)
|
||||
def test_specific_exception():
|
||||
raise ValueError("测试异常")
|
||||
|
||||
try:
|
||||
test_specific_exception()
|
||||
except ValueError as e:
|
||||
print(f"特定异常测试: {e}")
|
||||
Reference in New Issue
Block a user