mirror of
https://devops.liangqichi.top/qichi.liang/Orbitin.git
synced 2026-02-10 07:41:29 +08:00
重构: 完成代码审查和架构优化
主要改进: 1. 模块化架构重构 - 创建Confluence模块目录结构 - 统一飞书模块架构 - 重构数据库模块 2. 代码质量提升 - 创建统一配置管理 - 实现统一日志配置 - 完善类型提示和异常处理 3. 功能优化 - 移除parse-test功能 - 删除DEBUG_MODE配置 - 更新命令行选项 4. 文档完善 - 更新README.md项目结构 - 添加开发指南和故障排除 - 完善配置说明 5. 系统验证 - 所有核心功能测试通过 - 模块导入验证通过 - 架构完整性验证通过
This commit is contained in:
465
src/report.py
465
src/report.py
@@ -1,112 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
日报生成模块
|
||||
更新依赖,使用新的配置和数据库模块
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional
|
||||
import sys
|
||||
import os
|
||||
from typing import Dict, List, Optional, Any
|
||||
import logging
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
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
|
||||
|
||||
from src.database import DailyLogsDatabase
|
||||
from src.feishu_v2 import FeishuScheduleManagerV2 as FeishuScheduleManager
|
||||
logger = get_logger(__name__)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ReportGeneratorError(Exception):
|
||||
"""日报生成错误"""
|
||||
pass
|
||||
|
||||
|
||||
class DailyReportGenerator:
|
||||
"""每日作业报告生成器"""
|
||||
|
||||
DAILY_TARGET = 300 # 每日目标作业量
|
||||
|
||||
def __init__(self, db_path: str = 'data/daily_logs.db'):
|
||||
"""初始化"""
|
||||
def __init__(self, db_path: Optional[str] = None):
|
||||
"""
|
||||
初始化日报生成器
|
||||
|
||||
参数:
|
||||
db_path: 数据库文件路径,如果为None则使用配置
|
||||
"""
|
||||
self.db = DailyLogsDatabase(db_path)
|
||||
logger.info("日报生成器初始化完成")
|
||||
|
||||
def get_latest_date(self) -> str:
|
||||
"""获取数据库中最新的日期"""
|
||||
logs = self.db.query_all(limit=1)
|
||||
if logs:
|
||||
return logs[0]['date']
|
||||
return datetime.now().strftime('%Y-%m-%d')
|
||||
"""
|
||||
获取数据库中最新的日期
|
||||
|
||||
返回:
|
||||
最新日期字符串,格式 "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:
|
||||
"""获取指定日期的数据"""
|
||||
logs = self.db.query_by_date(date)
|
||||
def get_daily_data(self, date: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取指定日期的数据
|
||||
|
||||
# 按船名汇总
|
||||
ships = {}
|
||||
for log in logs:
|
||||
ship = log['ship_name']
|
||||
if ship not in ships:
|
||||
ships[ship] = 0
|
||||
if log.get('teu'):
|
||||
ships[ship] += log['teu']
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||
|
||||
return {
|
||||
'date': date,
|
||||
'ships': ships,
|
||||
'total_teu': sum(ships.values()),
|
||||
'ship_count': len(ships)
|
||||
}
|
||||
返回:
|
||||
每日数据字典
|
||||
"""
|
||||
try:
|
||||
logs = self.db.query_by_date(date)
|
||||
|
||||
# 按船名汇总
|
||||
ships: Dict[str, int] = {}
|
||||
for log in logs:
|
||||
ship = log['ship_name']
|
||||
if ship not in ships:
|
||||
ships[ship] = 0
|
||||
if log.get('teu'):
|
||||
ships[ship] += log['teu']
|
||||
|
||||
return {
|
||||
'date': date,
|
||||
'ships': ships,
|
||||
'total_teu': sum(ships.values()),
|
||||
'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:
|
||||
"""获取月度统计(截止到指定日期)"""
|
||||
year_month = date[:7] # YYYY-MM
|
||||
target_date = datetime.strptime(date, '%Y-%m-%d').date()
|
||||
def get_monthly_stats(self, date: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取月度统计(截止到指定日期)
|
||||
|
||||
logs = self.db.query_all(limit=10000)
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||
|
||||
# 只统计当月且在指定日期之前的数据
|
||||
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 = {}
|
||||
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 == 1:
|
||||
days_passed = 1
|
||||
else:
|
||||
days_passed = current_date.day
|
||||
|
||||
# 获取未统计数据
|
||||
unaccounted = self.db.get_unaccounted(year_month)
|
||||
|
||||
planned = days_passed * self.DAILY_TARGET
|
||||
actual = sum(daily_totals.values()) + unaccounted
|
||||
|
||||
return {
|
||||
'year_month': year_month,
|
||||
'days_passed': days_passed,
|
||||
'planned': planned,
|
||||
'actual': actual,
|
||||
'unaccounted': unaccounted,
|
||||
'completion': round(actual / planned * 100, 2) if planned > 0 else 0,
|
||||
'daily_totals': daily_totals
|
||||
}
|
||||
返回:
|
||||
月度统计字典
|
||||
"""
|
||||
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:
|
||||
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()
|
||||
|
||||
@@ -116,7 +185,7 @@ class DailyReportGenerator:
|
||||
|
||||
logger.info(f"获取 {date} 日报的班次人员,对应排班表日期: {tomorrow}")
|
||||
|
||||
# 获取次日的排班信息(使用缓存)
|
||||
# 获取次日的排班信息
|
||||
schedule = manager.get_schedule_for_date(tomorrow)
|
||||
|
||||
# 如果从飞书获取到数据,使用飞书数据
|
||||
@@ -124,88 +193,156 @@ class DailyReportGenerator:
|
||||
return {
|
||||
'day_shift': schedule.get('day_shift', ''),
|
||||
'night_shift': schedule.get('night_shift', ''),
|
||||
'duty_phone': '13107662315'
|
||||
'duty_phone': config.DUTY_PHONE
|
||||
}
|
||||
|
||||
# 如果飞书数据为空,返回空值
|
||||
logger.warning(f"无法从飞书获取 {tomorrow} 的排班信息")
|
||||
return {
|
||||
'day_shift': '',
|
||||
'night_shift': '',
|
||||
'duty_phone': '13107662315'
|
||||
}
|
||||
return self._empty_personnel()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取排班信息失败: {e}")
|
||||
# 降级处理:返回空值
|
||||
return {
|
||||
'day_shift': '',
|
||||
'night_shift': '',
|
||||
'duty_phone': '13107662315'
|
||||
}
|
||||
return self._empty_personnel()
|
||||
|
||||
def generate_report(self, date: str = None) -> str:
|
||||
"""生成日报"""
|
||||
if not date:
|
||||
date = self.get_latest_date()
|
||||
def generate_report(self, date: Optional[str] = None) -> str:
|
||||
"""
|
||||
生成日报
|
||||
|
||||
# 转换日期格式 2025-12-28 -> 12/28,同时确保查询格式正确
|
||||
参数:
|
||||
date: 日期字符串,格式 "YYYY-MM-DD",如果为None则使用最新日期
|
||||
|
||||
返回:
|
||||
日报文本
|
||||
|
||||
异常:
|
||||
ReportGeneratorError: 生成失败
|
||||
"""
|
||||
try:
|
||||
# 尝试解析各种日期格式
|
||||
parsed = datetime.strptime(date, '%Y-%m-%d')
|
||||
display_date = parsed.strftime('%m/%d')
|
||||
query_date = parsed.strftime('%Y-%m-%d') # 标准化为双数字格式
|
||||
except ValueError:
|
||||
# 如果已经是标准格式,直接使用
|
||||
display_date = datetime.strptime(date, '%Y-%m-%d').strftime('%m/%d')
|
||||
query_date = date
|
||||
|
||||
daily_data = self.get_daily_data(query_date)
|
||||
monthly_data = self.get_monthly_stats(query_date)
|
||||
personnel = self.get_shift_personnel(query_date)
|
||||
|
||||
# 月度统计
|
||||
month_display = date[5:7] + '/' + date[:4] # MM/YYYY
|
||||
|
||||
lines = []
|
||||
lines.append(f"日期:{display_date}")
|
||||
lines.append("")
|
||||
|
||||
# 船次信息(紧凑格式,不留空行)
|
||||
ship_lines = []
|
||||
for ship, teu in sorted(daily_data['ships'].items(), key=lambda x: -x[1]):
|
||||
ship_lines.append(f"船名:{ship}")
|
||||
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 (用天数*{self.DAILY_TARGET}TEU)")
|
||||
lines.append(f"当月实际作业量:{monthly_data['actual']}TEU")
|
||||
lines.append(f"当月完成比例:{monthly_data['completion']}%")
|
||||
lines.append("")
|
||||
|
||||
# 人员信息(需要配合 Confluence 日志中的班次人员信息)
|
||||
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}")
|
||||
|
||||
return "\n".join(lines)
|
||||
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, teu in sorted(daily_data['ships'].items(), key=lambda x: -x[1]):
|
||||
ship_lines.append(f"船名:{ship}")
|
||||
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: str = None):
|
||||
"""打印日报"""
|
||||
report = self.generate_report(date)
|
||||
print(report)
|
||||
return report
|
||||
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):
|
||||
"""关闭数据库连接"""
|
||||
@@ -213,6 +350,32 @@ class DailyReportGenerator:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试代码
|
||||
import sys
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
generator = DailyReportGenerator()
|
||||
generator.print_report()
|
||||
generator.close()
|
||||
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user