Files
Orbitin/src/report.py

407 lines
14 KiB
Python
Raw Normal View History

#!/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:
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)
}
except Exception as e:
logger.error(f"获取每日数据失败: {date}, 错误: {e}")
return {
'date': date,
'ships': {},
'total_teu': 0,
'ship_count': 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
# 获取未统计数据
unaccounted = self.db.get_unaccounted(year_month)
planned = days_passed * config.DAILY_TARGET_TEU
actual = sum(daily_totals.values()) + unaccounted
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,
'completion': completion,
'daily_totals': daily_totals
}
except Exception as e:
logger.error(f"获取月度统计失败: {date}, 错误: {e}")
return {
'year_month': date[:7],
'days_passed': 0,
'planned': 0,
'actual': 0,
'unaccounted': 0,
'completion': 0,
'daily_totals': {}
}
def get_shift_personnel(self, date: str) -> Dict[str, str]:
"""
获取班次人员从飞书排班表获取
注意日报中显示的是次日的班次人员所以需要获取 date+1 的排班
例如生成 12/29 的日报显示的是 12/30 的人员
参数:
date: 日期字符串格式 "YYYY-MM-DD"
返回:
班次人员字典
"""
try:
# 检查飞书配置
if not config.FEISHU_TOKEN or not config.FEISHU_SPREADSHEET_TOKEN:
logger.warning("飞书配置不完整,跳过排班信息获取")
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()