2025-12-28 23:31:22 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
2025-12-29 01:09:59 +08:00
|
|
|
|
码头作业日志管理工具
|
2025-12-28 23:31:22 +08:00
|
|
|
|
从 Confluence 获取交接班日志并保存到数据库
|
2025-12-31 02:04:16 +08:00
|
|
|
|
更新依赖,使用新的模块结构
|
2025-12-28 23:31:22 +08:00
|
|
|
|
"""
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
import sys
|
2025-12-29 01:09:59 +08:00
|
|
|
|
import os
|
|
|
|
|
|
from datetime import datetime
|
2025-12-31 02:04:16 +08:00
|
|
|
|
from typing import Optional, List
|
2025-12-28 23:31:22 +08:00
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
from src.config import config
|
|
|
|
|
|
from src.logging_config import setup_logging, get_logger
|
|
|
|
|
|
from src.confluence import ConfluenceClient, ConfluenceClientError, HTMLTextExtractor, HTMLTextExtractorError, HandoverLogParser, ShipLog, LogParserError
|
|
|
|
|
|
from src.database.daily_logs import DailyLogsDatabase
|
|
|
|
|
|
from src.report import DailyReportGenerator, ReportGeneratorError
|
2025-12-28 23:31:22 +08:00
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
# 初始化日志
|
|
|
|
|
|
logger = get_logger(__name__)
|
2025-12-29 01:09:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_debug_dir():
|
|
|
|
|
|
"""确保debug目录存在"""
|
2025-12-31 02:04:16 +08:00
|
|
|
|
if not os.path.exists(config.DEBUG_DIR):
|
|
|
|
|
|
os.makedirs(config.DEBUG_DIR)
|
|
|
|
|
|
logger.info(f"创建调试目录: {config.DEBUG_DIR}")
|
2025-12-29 01:09:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
def get_timestamp() -> str:
|
2025-12-29 01:09:59 +08:00
|
|
|
|
"""获取时间戳用于文件名"""
|
|
|
|
|
|
return datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
def fetch_html() -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取HTML内容
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
HTML字符串
|
|
|
|
|
|
|
|
|
|
|
|
异常:
|
|
|
|
|
|
SystemExit: 配置错误或获取失败
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 验证配置
|
|
|
|
|
|
if not config.validate():
|
|
|
|
|
|
logger.error("配置验证失败,请检查 .env 文件")
|
2025-12-29 01:15:57 +08:00
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
try:
|
|
|
|
|
|
logger.info("正在从 Confluence 获取 HTML 内容...")
|
|
|
|
|
|
client = ConfluenceClient()
|
|
|
|
|
|
html = client.get_html(config.CONFLUENCE_CONTENT_ID)
|
|
|
|
|
|
logger.info(f"获取成功,共 {len(html)} 字符")
|
|
|
|
|
|
return html
|
|
|
|
|
|
|
|
|
|
|
|
except ConfluenceClientError as e:
|
|
|
|
|
|
logger.error(f"获取HTML失败: {e}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"未知错误: {e}")
|
2025-12-28 23:31:22 +08:00
|
|
|
|
sys.exit(1)
|
2025-12-29 01:09:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
def extract_text(html: str) -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
提取布局文本
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
html: HTML字符串
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
提取的文本
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
logger.info("正在提取布局文本...")
|
|
|
|
|
|
extractor = HTMLTextExtractor()
|
|
|
|
|
|
layout_text = extractor.extract(html)
|
|
|
|
|
|
logger.info(f"提取完成,共 {len(layout_text)} 字符")
|
|
|
|
|
|
return layout_text
|
|
|
|
|
|
|
|
|
|
|
|
except HTMLTextExtractorError as e:
|
|
|
|
|
|
logger.error(f"提取文本失败: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"未知错误: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_debug_file(content: str, suffix: str = '') -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
保存调试文件到debug目录
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
content: 要保存的内容
|
|
|
|
|
|
suffix: 文件名后缀
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
保存的文件路径
|
|
|
|
|
|
"""
|
2025-12-29 01:09:59 +08:00
|
|
|
|
ensure_debug_dir()
|
|
|
|
|
|
filename = f'layout_output{suffix}.txt' if suffix else 'layout_output.txt'
|
2025-12-31 02:04:16 +08:00
|
|
|
|
filepath = os.path.join(config.DEBUG_DIR, filename)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|
|
|
|
|
f.write(content)
|
|
|
|
|
|
logger.info(f"已保存到 {filepath}")
|
|
|
|
|
|
return filepath
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"保存调试文件失败: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_logs(text: str) -> List[ShipLog]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
解析日志数据
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
text: 日志文本
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
船次日志列表
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
logger.info("正在解析日志数据...")
|
|
|
|
|
|
parser = HandoverLogParser()
|
|
|
|
|
|
logs = parser.parse(text)
|
|
|
|
|
|
logger.info(f"解析到 {len(logs)} 条记录")
|
|
|
|
|
|
return logs
|
|
|
|
|
|
|
|
|
|
|
|
except LogParserError as e:
|
|
|
|
|
|
logger.error(f"解析日志失败: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"未知错误: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_to_db(logs: List[ShipLog]) -> int:
|
|
|
|
|
|
"""
|
|
|
|
|
|
保存到数据库
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
logs: 船次日志列表
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
保存的记录数
|
|
|
|
|
|
"""
|
2025-12-29 01:09:59 +08:00
|
|
|
|
if not logs:
|
2025-12-31 02:04:16 +08:00
|
|
|
|
logger.warning("没有记录可保存")
|
2025-12-29 01:09:59 +08:00
|
|
|
|
return 0
|
2025-12-28 23:31:22 +08:00
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
try:
|
|
|
|
|
|
db = DailyLogsDatabase()
|
|
|
|
|
|
count = db.insert_many([log.to_dict() for log in logs])
|
|
|
|
|
|
logger.info(f"已保存 {count} 条记录到数据库")
|
|
|
|
|
|
|
|
|
|
|
|
stats = db.get_stats()
|
|
|
|
|
|
logger.info(f"数据库统计: 总记录={stats['total']}, 船次={len(stats['ships'])}, "
|
|
|
|
|
|
f"日期范围={stats['date_range']['start']}~{stats['date_range']['end']}")
|
|
|
|
|
|
|
|
|
|
|
|
return count
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"保存到数据库失败: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_unaccounted(year_month: str, teu: int, note: str = ''):
|
|
|
|
|
|
"""
|
|
|
|
|
|
添加未统计数据
|
2025-12-28 23:31:22 +08:00
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
参数:
|
|
|
|
|
|
year_month: 年月字符串,格式 "2025-12"
|
|
|
|
|
|
teu: 未统计TEU数量
|
|
|
|
|
|
note: 备注
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
db = DailyLogsDatabase()
|
|
|
|
|
|
result = db.insert_unaccounted(year_month, teu, note)
|
|
|
|
|
|
if result:
|
|
|
|
|
|
logger.info(f"已添加 {year_month} 月未统计数据: {teu}TEU")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error("添加失败")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"添加未统计数据失败: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def show_stats(date: str):
|
|
|
|
|
|
"""
|
|
|
|
|
|
显示指定日期的统计
|
2025-12-28 23:31:22 +08:00
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
参数:
|
|
|
|
|
|
date: 日期字符串,格式 "YYYY-MM-DD"
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
generator = DailyReportGenerator()
|
|
|
|
|
|
generator.print_report(date)
|
|
|
|
|
|
except ReportGeneratorError as e:
|
|
|
|
|
|
logger.error(f"生成统计失败: {e}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"未知错误: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_fetch() -> str:
|
2025-12-29 01:09:59 +08:00
|
|
|
|
"""执行:获取HTML并提取文本"""
|
|
|
|
|
|
html = fetch_html()
|
|
|
|
|
|
text = extract_text(html)
|
|
|
|
|
|
save_debug_file(text)
|
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_fetch_and_save():
|
|
|
|
|
|
"""执行:获取、提取、解析、保存到数据库"""
|
|
|
|
|
|
text = run_fetch()
|
|
|
|
|
|
logs = parse_logs(text)
|
|
|
|
|
|
save_to_db(logs)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
def run_fetch_save_debug() -> str:
|
2025-12-29 01:09:59 +08:00
|
|
|
|
"""执行:获取、提取、保存到debug目录"""
|
|
|
|
|
|
html = fetch_html()
|
|
|
|
|
|
text = extract_text(html)
|
|
|
|
|
|
suffix = f'_{get_timestamp()}'
|
|
|
|
|
|
save_debug_file(text, suffix)
|
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
def run_report(date: Optional[str] = None):
|
2025-12-29 01:09:59 +08:00
|
|
|
|
"""执行:生成日报"""
|
|
|
|
|
|
if not date:
|
|
|
|
|
|
date = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
|
|
show_stats(date)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_config_test():
|
|
|
|
|
|
"""执行:配置测试"""
|
|
|
|
|
|
logger.info("配置测试:")
|
|
|
|
|
|
config.print_summary()
|
2025-12-29 01:09:59 +08:00
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
# 测试Confluence连接
|
|
|
|
|
|
try:
|
|
|
|
|
|
client = ConfluenceClient()
|
|
|
|
|
|
if client.test_connection():
|
|
|
|
|
|
logger.info("Confluence连接测试: 成功")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.warning("Confluence连接测试: 失败")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Confluence连接测试失败: {e}")
|
2025-12-29 01:09:59 +08:00
|
|
|
|
|
2025-12-31 02:04:16 +08:00
|
|
|
|
# 测试数据库连接
|
|
|
|
|
|
try:
|
|
|
|
|
|
db = DailyLogsDatabase()
|
|
|
|
|
|
stats = db.get_stats()
|
|
|
|
|
|
logger.info(f"数据库连接测试: 成功,总记录: {stats['total']}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"数据库连接测试失败: {e}")
|
2025-12-29 01:09:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 功能映射
|
|
|
|
|
|
FUNCTIONS = {
|
|
|
|
|
|
'fetch': run_fetch,
|
|
|
|
|
|
'fetch-save': run_fetch_and_save,
|
|
|
|
|
|
'fetch-debug': run_fetch_save_debug,
|
|
|
|
|
|
'report': lambda: run_report(),
|
|
|
|
|
|
'report-today': lambda: run_report(datetime.now().strftime('%Y-%m-%d')),
|
2025-12-31 02:04:16 +08:00
|
|
|
|
'config-test': run_config_test,
|
2025-12-29 01:09:59 +08:00
|
|
|
|
'stats': lambda: show_stats(datetime.now().strftime('%Y-%m-%d')),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
|
|
description='码头作业日志管理工具',
|
|
|
|
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
|
|
|
|
epilog='''
|
|
|
|
|
|
可选功能:
|
|
|
|
|
|
fetch 获取HTML并提取文本(保存到debug目录)
|
|
|
|
|
|
fetch-save 获取、提取、解析并保存到数据库
|
|
|
|
|
|
fetch-debug 获取、提取并保存带时间戳的debug文件
|
|
|
|
|
|
report 生成日报(默认今天)
|
2025-12-29 01:15:57 +08:00
|
|
|
|
report-today 生成今日日报
|
2025-12-31 02:04:16 +08:00
|
|
|
|
config-test 配置测试
|
2025-12-29 01:09:59 +08:00
|
|
|
|
stats 显示今日统计
|
|
|
|
|
|
|
|
|
|
|
|
示例:
|
|
|
|
|
|
python3 main.py fetch
|
|
|
|
|
|
python3 main.py fetch-save
|
|
|
|
|
|
python3 main.py report 2025-12-28
|
2025-12-31 02:04:16 +08:00
|
|
|
|
python3 main.py config-test
|
2025-12-29 01:09:59 +08:00
|
|
|
|
'''
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
'function',
|
|
|
|
|
|
nargs='?',
|
|
|
|
|
|
default='fetch-save',
|
2025-12-31 02:04:16 +08:00
|
|
|
|
choices=['fetch', 'fetch-save', 'fetch-debug', 'report', 'report-today', 'config-test', 'stats'],
|
2025-12-29 01:09:59 +08:00
|
|
|
|
help='要执行的功能 (默认: fetch-save)'
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
'date',
|
|
|
|
|
|
nargs='?',
|
|
|
|
|
|
help='日期 (格式: YYYY-MM-DD),用于 report 功能'
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
'--unaccounted',
|
|
|
|
|
|
'-u',
|
|
|
|
|
|
metavar='TEU',
|
|
|
|
|
|
type=int,
|
|
|
|
|
|
help='添加未统计数据(需同时指定月份,如 -u 118 2025-12)'
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
'--month',
|
|
|
|
|
|
'-m',
|
|
|
|
|
|
metavar='YEAR-MONTH',
|
|
|
|
|
|
help='指定月份(与 --unaccounted 配合使用)'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-28 23:31:22 +08:00
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
2025-12-29 01:09:59 +08:00
|
|
|
|
# 添加未统计数据
|
|
|
|
|
|
if args.unaccounted:
|
|
|
|
|
|
year_month = args.month or datetime.now().strftime('%Y-%m')
|
2025-12-31 02:04:16 +08:00
|
|
|
|
try:
|
|
|
|
|
|
add_unaccounted(year_month, args.unaccounted)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"添加未统计数据失败: {e}")
|
|
|
|
|
|
sys.exit(1)
|
2025-12-29 01:09:59 +08:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 执行功能
|
2025-12-31 02:04:16 +08:00
|
|
|
|
try:
|
|
|
|
|
|
if args.function == 'report' and args.date:
|
|
|
|
|
|
run_report(args.date)
|
|
|
|
|
|
else:
|
|
|
|
|
|
FUNCTIONS[args.function]()
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
|
logger.info("用户中断操作")
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"执行功能失败: {e}")
|
|
|
|
|
|
sys.exit(1)
|
2025-12-29 01:09:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2025-12-31 02:04:16 +08:00
|
|
|
|
# 初始化日志系统
|
|
|
|
|
|
setup_logging()
|
|
|
|
|
|
|
|
|
|
|
|
# 打印启动信息
|
|
|
|
|
|
logger.info("=" * 50)
|
|
|
|
|
|
logger.info("码头作业日志管理工具 - OrbitIn")
|
|
|
|
|
|
logger.info(f"启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
|
|
logger.info("=" * 50)
|
|
|
|
|
|
|
|
|
|
|
|
# 运行主程序
|
2025-12-29 01:09:59 +08:00
|
|
|
|
main()
|