Files
gloria/feishu/manager.py

364 lines
12 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
飞书排班管理器模块
统一入口使用数据库存储和缓存
"""
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
import logging
import os
import sys
# 添加项目根目录到路径
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if project_root not in sys.path:
sys.path.insert(0, project_root)
# 简单实现不依赖src模块
config = None
get_logger = logging.getLogger
class ScheduleDatabase:
"""简单的排班数据库占位实现"""
def __init__(self, db_path=None):
self.db_path = db_path
def save_schedule(self, date_str, result, sheet_id, sheet_title):
pass
def get_schedule_by_range(self, start_date, end_date):
return []
def get_stats(self):
return {}
from .client import FeishuSheetsClient
from .parser import ScheduleDataParser
logger = get_logger(__name__)
class FeishuScheduleManager:
"""飞书排班管理器(统一入口)"""
def __init__(
self,
base_url: Optional[str] = None,
token: Optional[str] = None,
spreadsheet_token: Optional[str] = None,
db_path: Optional[str] = None,
):
"""
初始化管理器
参数:
base_url: 飞书API基础URL如果为None则使用配置
token: 飞书API令牌如果为None则使用配置
spreadsheet_token: 表格token如果为None则使用配置
db_path: 数据库路径如果为None则使用配置
"""
# 检查配置是否完整
self._check_config(token, spreadsheet_token)
# 初始化组件
self.client = FeishuSheetsClient(base_url, token, spreadsheet_token)
self.parser = ScheduleDataParser()
self.db = ScheduleDatabase(db_path)
logger.info("飞书排班管理器初始化完成")
def _check_config(
self, token: Optional[str], spreadsheet_token: Optional[str]
) -> None:
"""检查必要配置"""
# 检查是否有任何可用的认证方式
has_token = bool(token or os.getenv("FEISHU_TOKEN"))
has_app_credentials = bool(os.getenv("FEISHU_APP_ID") and os.getenv("FEISHU_APP_SECRET"))
if not has_token and not has_app_credentials:
logger.warning("飞书认证未配置,排班功能将不可用")
logger.warning("请配置 FEISHU_TOKEN 或 FEISHU_APP_ID + FEISHU_APP_SECRET")
elif has_app_credentials:
logger.info("使用飞书应用凭证自动获取token")
elif has_token:
logger.info("使用手动配置的FEISHU_TOKEN")
if not spreadsheet_token and not os.getenv("FEISHU_SPREADSHEET_TOKEN"):
logger.warning("飞书表格令牌未配置,排班功能将不可用")
def _select_sheet_for_date(
self, sheets: List[Dict[str, str]], target_year_month: str
) -> Optional[Dict[str, str]]:
"""
为指定日期选择最合适的表格
参数:
sheets: 表格列表
target_year_month: 目标年月格式 "2025-12"
返回:
选中的表格信息未找到返回None
"""
if not sheets:
logger.error("表格列表为空")
return None
# 提取年份和月份
try:
year = target_year_month[:4]
month = target_year_month[5:7].lstrip("0")
except (IndexError, ValueError) as e:
logger.error(f"解析年月失败: {target_year_month}, 错误: {e}")
return None
# 对于2026年优先使用年度表格
if year == "2026":
# 查找年度表格,如 "2026年排班表"
year_name = f"{year}"
for sheet in sheets:
title = sheet.get("title", "")
if year_name in title and "排班表" in title:
logger.info(f"找到2026年年度表格: {title}")
return sheet
# 优先查找月份表格,如 "12月"
month_name = f"{int(month)}"
for sheet in sheets:
title = sheet.get("title", "")
if month_name in title:
logger.info(f"找到月份表格: {title}")
return sheet
# 查找年度表格,如 "2026年排班表"
year_name = f"{year}"
for sheet in sheets:
title = sheet.get("title", "")
if year_name in title and "排班表" in title:
logger.info(f"找到年度表格: {title}")
return sheet
# 如果没有找到匹配的表格,使用第一个表格
logger.warning(
f"未找到 {target_year_month} 的匹配表格,使用第一个表格: {sheets[0]['title']}"
)
return sheets[0]
def get_schedule_for_date(self, date_str: str) -> Dict[str, any]:
"""
获取指定日期的排班信息
修复每次都从飞书获取最新数据并覆盖数据库确保日报中显示最新排班信息
参数:
date_str: 日期字符串格式 "2025-12-30"
返回:
排班信息字典
异常:
ValueError: 日期格式无效
Exception: 其他错误
"""
try:
# 解析日期
dt = datetime.strptime(date_str, "%Y-%m-%d")
# 生成两种格式的日期字符串,用于匹配不同表格
target_date_mm_dd = dt.strftime("%m/%d") # "01/01" 用于月度表格
target_date_chinese = f"{dt.month}{dt.day}" # "1月1日" 用于年度表格
target_year_month = dt.strftime("%Y-%m") # "2025-12"
logger.info(
f"获取 {date_str} 的排班信息 (格式: {target_date_mm_dd}/{target_date_chinese})"
)
# 1. 获取表格信息
sheets = self.client.get_sheets_info()
if not sheets:
logger.error("未获取到表格信息")
return self._empty_result()
# 2. 选择最合适的表格
selected_sheet = self._select_sheet_for_date(sheets, target_year_month)
if not selected_sheet:
logger.error("未找到合适的表格")
return self._empty_result()
sheet_id = selected_sheet["sheet_id"]
sheet_title = selected_sheet["title"]
# 3. 获取表格数据
sheet_data = self.client.get_sheet_data(sheet_id)
if not sheet_data:
logger.error("未获取到表格数据")
return self._empty_result()
values = sheet_data.get("valueRange", {}).get("values", [])
if not values:
logger.error("表格数据为空")
return self._empty_result()
# 4. 解析数据 - 根据表格类型选择合适的日期格式
# 如果是年度表格使用中文日期格式否则使用mm/dd格式
if "" in sheet_title and "排班表" in sheet_title:
target_date = target_date_chinese # "1月1日"
else:
target_date = target_date_mm_dd # "01/01"
logger.info(f"使用日期格式: {target_date} 解析表格: {sheet_title}")
result = self.parser.parse(values, target_date, sheet_title)
# 5. 每次都保存到数据库,覆盖旧数据,确保人员变动能及时更新
if result["day_shift"] or result["night_shift"]:
self.db.save_schedule(date_str, result, sheet_id, sheet_title)
logger.info(
f"已更新 {date_str} 的排班信息到数据库: 白班={result['day_shift']}, 夜班={result['night_shift']}"
)
else:
logger.warning(f"解析结果为空,{date_str} 未保存到数据库")
return result
except ValueError as e:
logger.error(f"日期格式无效: {date_str}, 错误: {e}")
raise
except Exception as e:
logger.error(f"获取排班信息失败: {e}")
# 降级处理:返回空值
return self._empty_result()
def get_schedule_for_today(self) -> Dict[str, any]:
"""获取今天的排班信息"""
today = datetime.now().strftime("%Y-%m-%d")
return self.get_schedule_for_date(today)
def get_schedule_for_tomorrow(self) -> Dict[str, any]:
"""获取明天的排班信息"""
tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
return self.get_schedule_for_date(tomorrow)
def refresh_all_schedules(self, days: Optional[int] = None):
"""
刷新未来指定天数的排班信息
参数:
days: 刷新未来多少天的排班信息如果为None则使用配置
"""
if days is None:
days = int(os.getenv("SCHEDULE_REFRESH_DAYS", 7))
logger.info(f"开始刷新未来 {days} 天的排班信息")
today = datetime.now()
success_count = 0
error_count = 0
for i in range(days):
date = (today + timedelta(days=i)).strftime("%Y-%m-%d")
try:
logger.debug(f"刷新 {date} 的排班信息...")
self.get_schedule_for_date(date)
success_count += 1
except Exception as e:
logger.error(f"刷新 {date} 的排班信息失败: {e}")
error_count += 1
logger.info(f"排班信息刷新完成,成功: {success_count}, 失败: {error_count}")
def get_schedule_by_range(
self, start_date: str, end_date: str
) -> List[Dict[str, any]]:
"""
获取日期范围内的排班信息
参数:
start_date: 开始日期 (YYYY-MM-DD)
end_date: 结束日期 (YYYY-MM-DD)
返回:
排班信息列表
"""
try:
# 验证日期格式
datetime.strptime(start_date, "%Y-%m-%d")
datetime.strptime(end_date, "%Y-%m-%d")
return self.db.get_schedule_by_range(start_date, end_date)
except ValueError as e:
logger.error(f"日期格式无效: {e}")
return []
except Exception as e:
logger.error(f"获取排班范围失败: {e}")
return []
def test_connection(self) -> bool:
"""测试飞书连接是否正常"""
return self.client.test_connection()
def get_stats(self) -> Dict[str, any]:
"""获取排班数据库统计信息"""
return self.db.get_stats()
def _empty_result(self) -> Dict[str, any]:
"""返回空结果"""
return {
"day_shift": "",
"night_shift": "",
"day_shift_list": [],
"night_shift_list": [],
}
def _format_db_result(self, db_result: Dict[str, any]) -> Dict[str, any]:
"""格式化数据库结果"""
return {
"day_shift": db_result["day_shift"],
"night_shift": db_result["night_shift"],
"day_shift_list": db_result["day_shift_list"],
"night_shift_list": db_result["night_shift_list"],
}
if __name__ == "__main__":
# 测试代码
import sys
# 设置日志
logging.basicConfig(level=logging.INFO)
# 初始化管理器
manager = FeishuScheduleManager()
# 测试连接
if not manager.test_connection():
print("飞书连接测试失败")
sys.exit(1)
print("飞书连接测试成功")
# 测试获取今天和明天的排班
today_schedule = manager.get_schedule_for_today()
print(
f"今天排班: 白班={today_schedule['day_shift']}, 夜班={today_schedule['night_shift']}"
)
tomorrow_schedule = manager.get_schedule_for_tomorrow()
print(
f"明天排班: 白班={tomorrow_schedule['day_shift']}, 夜班={tomorrow_schedule['night_shift']}"
)
# 测试统计
stats = manager.get_stats()
print(f"排班统计: {stats}")
# 测试范围查询最近7天
end_date = datetime.now().strftime("%Y-%m-%d")
start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
schedules = manager.get_schedule_by_range(start_date, end_date)
print(f"最近7天排班记录: {len(schedules)}")