重构: 完成代码审查和架构优化

主要改进:
1. 模块化架构重构
   - 创建Confluence模块目录结构
   - 统一飞书模块架构
   - 重构数据库模块

2. 代码质量提升
   - 创建统一配置管理
   - 实现统一日志配置
   - 完善类型提示和异常处理

3. 功能优化
   - 移除parse-test功能
   - 删除DEBUG_MODE配置
   - 更新命令行选项

4. 文档完善
   - 更新README.md项目结构
   - 添加开发指南和故障排除
   - 完善配置说明

5. 系统验证
   - 所有核心功能测试通过
   - 模块导入验证通过
   - 架构完整性验证通过
This commit is contained in:
2025-12-31 02:04:16 +08:00
parent 90317018b7
commit 5345dc75f2
30 changed files with 4355 additions and 2678 deletions

View File

@@ -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()