Files
Orbitin/main.py
qichi.liang 0cbc587bf3 feat: 实现月底/月初数据调整功能
1. 新增月底/月初智能数据调整功能
   - 月底最后一天自动弹出剔除数据对话框
   - 月初1号自动弹出添加数据对话框
   - 普通日期不弹出对话框

2. 实现月底剔除数据自动转移到次月1号
   - 月底剔除的数据自动添加到次月1号统计
   - 支持跨月、跨年数据转移
   - 数据备注自动记录转移信息

3. 修复自动获取数据后不弹出调整对话框的问题
   - 修改auto_fetch_data()方法,成功获取数据后调用调整处理
   - 确保第一次打开GUI也能弹出相应对话框

4. 修复月度统计不包含调整数据的问题
   - 修改get_monthly_stats()方法包含手动调整数据
   - 确保调整数据正确影响月度统计

5. 恢复日报原始模板格式
   - 移除调整数据的详细说明
   - 保持原始日报模板,只显示最终结果

6. 数据库增强
   - 新增manual_adjustments表存储手动调整数据
   - 实现调整数据的增删改查方法
   - 实现包含调整数据的每日数据获取方法

测试通过:所有功能正常工作,数据计算准确。
2026-01-02 00:08:57 +08:00

427 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
码头作业日志管理工具
从 Confluence 获取交接班日志并保存到数据库
更新依赖,使用新的模块结构
"""
import argparse
import sys
import os
from datetime import datetime
from typing import Optional, List
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
# 初始化日志
logger = get_logger(__name__)
def ensure_debug_dir():
"""确保debug目录存在"""
if not os.path.exists(config.DEBUG_DIR):
os.makedirs(config.DEBUG_DIR)
logger.info(f"创建调试目录: {config.DEBUG_DIR}")
def get_timestamp() -> str:
"""获取时间戳用于文件名"""
return datetime.now().strftime('%Y%m%d_%H%M%S')
def fetch_html() -> str:
"""
获取HTML内容
返回:
HTML字符串
异常:
SystemExit: 配置错误或获取失败
"""
# 验证配置
if not config.validate():
logger.error("配置验证失败,请检查 .env 文件")
sys.exit(1)
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}")
sys.exit(1)
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: 文件名后缀
返回:
保存的文件路径
"""
ensure_debug_dir()
filename = f'layout_output{suffix}.txt' if suffix else 'layout_output.txt'
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: 船次日志列表
返回:
保存的记录数
"""
if not logs:
logger.warning("没有记录可保存")
return 0
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 = ''):
"""
添加未统计数据
参数:
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 remove_unaccounted(year_month: str, teu_to_reduce: int = None):
"""
去除未统计数据
参数:
year_month: 年月字符串,格式 "2025-12"
teu_to_reduce: 要减少的TEU数量如果为None则删除整个记录
"""
try:
db = DailyLogsDatabase()
if teu_to_reduce is None:
# 如果没有指定减少数量,则删除整个记录
result = db.delete_unaccounted(year_month)
if result:
logger.info(f"已删除 {year_month} 月未统计数据")
else:
logger.error("删除失败")
else:
# 减少指定数量的TEU
result = db.reduce_unaccounted(year_month, teu_to_reduce)
if result:
logger.info(f"已减少 {year_month} 月未统计数据: {teu_to_reduce}TEU")
else:
logger.error("减少失败")
except Exception as e:
logger.error(f"去除未统计数据失败: {e}")
raise
def show_stats(date: str):
"""
显示指定日期的统计
参数:
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:
"""执行获取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)
def run_fetch_save_debug() -> str:
"""执行获取、提取、保存到debug目录"""
html = fetch_html()
text = extract_text(html)
suffix = f'_{get_timestamp()}'
save_debug_file(text, suffix)
return text
def run_report(date: Optional[str] = None):
"""执行:生成日报"""
if not date:
date = datetime.now().strftime('%Y-%m-%d')
show_stats(date)
def run_config_test():
"""执行:配置测试"""
logger.info("配置测试:")
config.print_summary()
# 测试Confluence连接
try:
client = ConfluenceClient()
if client.test_connection():
logger.info("Confluence连接测试: 成功")
else:
logger.warning("Confluence连接测试: 失败")
except Exception as e:
logger.error(f"Confluence连接测试失败: {e}")
# 测试数据库连接
try:
db = DailyLogsDatabase()
stats = db.get_stats()
logger.info(f"数据库连接测试: 成功,总记录: {stats['total']}")
except Exception as e:
logger.error(f"数据库连接测试失败: {e}")
# 功能映射
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')),
'config-test': run_config_test,
'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 生成日报(默认今天)
report-today 生成今日日报
config-test 配置测试
stats 显示今日统计
参数:
--unaccounted, -u TEU 添加未统计数据(需同时指定月份)
--remove-unaccounted, -r [TEU] 去除未统计数据需同时指定月份。如果指定TEU值则减少该数量如果不指定则删除整个记录
--month, -m YEAR-MONTH 指定月份(与 -u 或 -r 配合使用)
示例:
python3 main.py fetch
python3 main.py fetch-save
python3 main.py report 2025-12-28
python3 main.py config-test
python3 main.py --unaccounted 118 --month 2025-12
python3 main.py --remove-unaccounted --month 2025-12 # 删除整个记录
python3 main.py --remove-unaccounted 118 --month 2025-12 # 减少118TEU
'''
)
parser.add_argument(
'function',
nargs='?',
default='fetch-save',
choices=['fetch', 'fetch-save', 'fetch-debug', 'report', 'report-today', 'config-test', 'stats'],
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(
'--remove-unaccounted',
'-r',
metavar='TEU',
nargs='?',
const=None,
type=int,
help='去除未统计数据(需同时指定月份,如 -r 118 2025-12。如果指定TEU值则减少该数量如果不指定则删除整个记录'
)
parser.add_argument(
'--month',
'-m',
metavar='YEAR-MONTH',
help='指定月份(与 --unaccounted 或 --remove-unaccounted 配合使用)'
)
args = parser.parse_args()
# 添加未统计数据
if args.unaccounted:
year_month = args.month or datetime.now().strftime('%Y-%m')
try:
add_unaccounted(year_month, args.unaccounted)
except Exception as e:
logger.error(f"添加未统计数据失败: {e}")
sys.exit(1)
return
# 去除未统计数据
# 检查是否提供了 --remove-unaccounted 或 -r 参数
has_remove_arg = any(arg in sys.argv for arg in ['--remove-unaccounted', '-r'])
if has_remove_arg:
year_month = args.month or datetime.now().strftime('%Y-%m')
try:
# args.remove_unaccounted 可能是整数指定TEU或 None未指定
if isinstance(args.remove_unaccounted, int):
# 指定了TEU值减少指定数量
remove_unaccounted(year_month, args.remove_unaccounted)
else:
# 未指定TEU值删除整个记录
remove_unaccounted(year_month)
except Exception as e:
logger.error(f"去除未统计数据失败: {e}")
sys.exit(1)
return
# 执行功能
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)
if __name__ == '__main__':
# 初始化日志系统
setup_logging()
# 打印启动信息
logger.info("=" * 50)
logger.info("码头作业日志管理工具 - OrbitIn")
logger.info(f"启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
logger.info("=" * 50)
# 运行主程序
main()