refactor(schedule_api): 重构日程与计划API为只读数据库查询

将日程API从依赖`schedule_manager`的内存状态改为直接从数据库异步查询。这提高了数据一致性,并解除了模块间的紧密耦合。

主要变更:
- **解耦**: 移除对`schedule_manager`的依赖,所有数据直接来自数据库。
- **只读设计**: 移除了`regenerate_schedule`, `ensure_monthly_plans`, `archive_monthly_plans`等写操作API,使API职责更清晰,专注于查询。
- **功能增强**:
    - `get_schedule` (原`get_today_schedule`) 现在支持查询任意日期的日程。
    - `get_monthly_plans` 新增随机抽样功能。
    - 新增`get_activities_between`用于查询特定时间范围的活动。
    - 新增`count_monthly_plans`用于统计月度计划数量。
- **格式化输出**: 所有查询函数均增加了`formatted`参数,方便插件直接获取格式化后的字符串。
- **文档更新**: 全面更新了模块和函数的文档字符串,以反映新的API设计和用法。
This commit is contained in:
minecraft1024a
2025-10-25 13:29:49 +08:00
parent 577c76b4a4
commit 5fc9d1b9da

View File

@@ -1,180 +1,330 @@
""" """
日程表与月度计划API模块 日程表与月度计划查询API模块
专门负责日程和月度计划信息的查询与管理采用标准Python包设计模式 本模块提供了一系列用于查询日程和月度计划的只读接口。
所有对外接口均为异步函数,以便于插件开发者在异步环境中使用 所有对外接口均为异步函数,专为插件开发者设计,以便在异步环境中无缝集成
核心功能:
- 查询指定日期的日程安排。
- 获取当前正在进行的活动。
- 筛选特定时间范围内的活动。
- 查询月度计划,支持随机抽样和计数。
- 所有查询接口均提供格式化输出选项。
使用方式: 使用方式:
import asyncio import asyncio
from src.plugin_system.apis import schedule_api from src.plugin_system.apis import schedule_api
async def main(): async def main():
# 获取今日日程 # 获取今天的日程(原始数据)
today_schedule = await schedule_api.get_today_schedule() today_schedule = await schedule_api.get_schedule()
if today_schedule: if today_schedule:
print("今天的日程:", today_schedule) print("今天的日程:", today_schedule)
# 获取昨天的日程,并格式化为字符串
from datetime import datetime, timedelta
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
formatted_schedule = await schedule_api.get_schedule(date=yesterday, formatted=True)
if formatted_schedule:
print(f"\\n昨天的日程 (格式化):\\n{formatted_schedule}")
# 获取当前活动 # 获取当前活动
current_activity = await schedule_api.get_current_activity() current_activity = await schedule_api.get_current_activity()
if current_activity: if current_activity:
print("当前活动:", current_activity) print(f"\\n当前活动: {current_activity.get('activity')}")
# 获取本月月度计划 # 获取本月月度计划总数
from datetime import datetime plan_count = await schedule_api.count_monthly_plans()
this_month = datetime.now().strftime("%Y-%m") print(f"\\n本月月度计划总数: {plan_count}")
plans = await schedule_api.get_monthly_plans(this_month)
if plans: # 随机获取本月的2个计划
print(f"{this_month} 的月度计划:", [p.plan_text for p in plans]) random_plans = await schedule_api.get_monthly_plans(random_count=2)
if random_plans:
print("\\n随机的2个计划:", [p.plan_text for p in random_plans])
asyncio.run(main()) asyncio.run(main())
""" """
from datetime import datetime import random
from typing import Any from datetime import datetime, time
from typing import Any, List, Optional, Union
from src.common.database.sqlalchemy_models import MonthlyPlan import orjson
from sqlalchemy import func, select
from src.common.database.sqlalchemy_models import MonthlyPlan, Schedule, get_db_session
from src.common.logger import get_logger from src.common.logger import get_logger
from src.schedule.database import get_active_plans_for_month from src.schedule.database import get_active_plans_for_month
from src.schedule.schedule_manager import schedule_manager
logger = get_logger("schedule_api") logger = get_logger("schedule_api")
# --- 内部辅助函数 ---
def _format_schedule_list(
items: Union[List[dict[str, Any]], List[MonthlyPlan]],
template: str,
item_type: str,
) -> str:
"""将日程或计划列表格式化为字符串"""
if not items:
return ""
lines = []
for item in items:
if item_type == "schedule" and isinstance(item, dict):
lines.append(template.format(time_range=item.get("time_range", ""), activity=item.get("activity", "")))
elif item_type == "plan" and isinstance(item, MonthlyPlan):
lines.append(template.format(plan_text=item.plan_text))
return "\\n".join(lines)
async def _get_schedule_from_db(date_str: str) -> Optional[List[dict[str, Any]]]:
"""从数据库中获取并解析指定日期的日程"""
async with get_db_session() as session:
result = await session.execute(select(Schedule).filter(Schedule.date == date_str))
schedule_record = result.scalars().first()
if schedule_record and schedule_record.schedule_data:
try:
return orjson.loads(str(schedule_record.schedule_data))
except orjson.JSONDecodeError:
logger.warning(f"无法解析数据库中的日程数据 (日期: {date_str})")
return None
# --- API实现 ---
class ScheduleAPI: class ScheduleAPI:
"""日程表与月度计划API - 负责日程和计划信息的查询与管理""" """日程表与月度计划查询API"""
@staticmethod @staticmethod
async def get_today_schedule() -> list[dict[str, Any]] | None: async def get_schedule(
"""(异步) 获取今天的日程安排 date: Optional[str] = None,
formatted: bool = False,
format_template: str = "{time_range}: {activity}",
) -> Union[List[dict[str, Any]], str, None]:
"""
(异步) 获取指定日期的日程安排。
Args:
date (Optional[str]): 目标日期,格式 "YYYY-MM-DD"。如果为None则使用当前日期。
formatted (bool): 如果为True返回格式化的字符串否则返回原始数据列表。
format_template (str): 当 formatted=True 时使用的格式化模板。
Returns: Returns:
Optional[List[Dict[str, Any]]]: 今天的日程列表,如果未生成或未启用则返回None Union[List[Dict[str, Any]], str, None]: 日程数据或None
""" """
target_date = date or datetime.now().strftime("%Y-%m-%d")
try: try:
logger.debug("[ScheduleAPI] 正在获取今天的日程安排...") logger.debug(f"[ScheduleAPI] 正在获取 {target_date} 的日程安排...")
return schedule_manager.today_schedule schedule_data = await _get_schedule_from_db(target_date)
if schedule_data is None:
return None
if formatted:
return _format_schedule_list(schedule_data, format_template, "schedule")
return schedule_data
except Exception as e: except Exception as e:
logger.error(f"[ScheduleAPI] 获取今日日程失败: {e}") logger.error(f"[ScheduleAPI] 获取 {target_date} 日程失败: {e}")
return None return None
@staticmethod @staticmethod
async def get_current_activity() -> str | None: async def get_current_activity(
"""(异步) 获取当前正在进行的活动 formatted: bool = False,
format_template: str = "{time_range}: {activity}",
) -> Union[dict[str, Any], str, None]:
"""
(异步) 获取当前正在进行的活动。
Args:
formatted (bool): 如果为True返回格式化的字符串否则返回活动字典。
format_template (str): 当 formatted=True 时使用的格式化模板。
Returns: Returns:
Optional[str]: 当前活动名称,如果没有则返回None Union[Dict[str, Any], str, None]: 当前活动数据或None
""" """
try: try:
logger.debug("[ScheduleAPI] 正在获取当前活动...") logger.debug("[ScheduleAPI] 正在获取当前活动...")
return schedule_manager.get_current_activity() today_schedule = await _get_schedule_from_db(datetime.now().strftime("%Y-%m-%d"))
if not today_schedule:
return None
now = datetime.now().time()
for event in today_schedule:
time_range = event.get("time_range")
if not time_range:
continue
try:
start_str, end_str = time_range.split("-")
start_time = datetime.strptime(start_str.strip(), "%H:%M").time()
end_time = datetime.strptime(end_str.strip(), "%H:%M").time()
if (start_time <= now < end_time) or \
(end_time < start_time and (now >= start_time or now < end_time)):
if formatted:
return _format_schedule_list([event], format_template, "schedule")
return event
except (ValueError, KeyError):
continue
return None
except Exception as e: except Exception as e:
logger.error(f"[ScheduleAPI] 获取当前活动失败: {e}") logger.error(f"[ScheduleAPI] 获取当前活动失败: {e}")
return None return None
@staticmethod @staticmethod
async def regenerate_schedule() -> bool: async def get_activities_between(
"""(异步) 触发后台重新生成今天的日程 start_time: str,
end_time: str,
Returns: date: Optional[str] = None,
bool: 是否成功触发 formatted: bool = False,
format_template: str = "{time_range}: {activity}",
) -> Union[List[dict[str, Any]], str, None]:
""" """
try: (异步) 获取指定日期和时间范围内的所有活动。
logger.info("[ScheduleAPI] 正在触发后台重新生成日程...")
await schedule_manager.generate_and_save_schedule()
return True
except Exception as e:
logger.error(f"[ScheduleAPI] 触发日程重新生成失败: {e}")
return False
@staticmethod
async def get_monthly_plans(target_month: str | None = None) -> list[MonthlyPlan]:
"""(异步) 获取指定月份的有效月度计划
Args: Args:
target_month (Optional[str]): 目标月份,格式 "YYYY-MM"如果为None则使用当前月份。 start_time (str): 开始时间,格式 "HH:MM"
end_time (str): 结束时间,格式 "HH:MM"
date (Optional[str]): 目标日期,格式 "YYYY-MM-DD"。如果为None则使用当前日期。
formatted (bool): 如果为True返回格式化的字符串否则返回活动列表。
format_template (str): 当 formatted=True 时使用的格式化模板。
Returns: Returns:
List[MonthlyPlan]: 月度计划对象列表 Union[List[Dict[str, Any]], str, None]: 在时间范围内的活动列表或None。
""" """
if target_month is None: target_date = date or datetime.now().strftime("%Y-%m-%d")
target_month = datetime.now().strftime("%Y-%m")
try: try:
logger.debug(f"[ScheduleAPI] 正在获取 {target_month}月度计划...") logger.debug(f"[ScheduleAPI] 正在获取 {target_date}{start_time}{end_time}活动...")
return await get_active_plans_for_month(target_month) schedule_data = await _get_schedule_from_db(target_date)
if not schedule_data:
return None
start = datetime.strptime(start_time, "%H:%M").time()
end = datetime.strptime(end_time, "%H:%M").time()
activities_in_range = []
for event in schedule_data:
time_range = event.get("time_range")
if not time_range:
continue
try:
event_start_str, event_end_str = time_range.split("-")
event_start = datetime.strptime(event_start_str.strip(), "%H:%M").time()
if start <= event_start < end:
activities_in_range.append(event)
except (ValueError, KeyError):
continue
if formatted:
return _format_schedule_list(activities_in_range, format_template, "schedule")
return activities_in_range
except Exception as e: except Exception as e:
logger.error(f"[ScheduleAPI] 获取 {target_month} 月度计划失败: {e}") logger.error(f"[ScheduleAPI] 获取时间段内活动失败: {e}")
return [] return None
@staticmethod @staticmethod
async def ensure_monthly_plans(target_month: str | None = None) -> bool: async def get_monthly_plans(
"""(异步) 确保指定月份存在月度计划,如果不存在则触发生成 target_month: Optional[str] = None,
random_count: Optional[int] = None,
formatted: bool = False,
format_template: str = "- {plan_text}",
) -> Union[List[MonthlyPlan], str, None]:
"""
(异步) 获取指定月份的有效月度计划。
Args: Args:
target_month (Optional[str]): 目标月份,格式 "YYYY-MM"。如果为None则使用当前月份。 target_month (Optional[str]): 目标月份,格式 "YYYY-MM"。如果为None则使用当前月份。
random_count (Optional[int]): 如果设置,将随机返回指定数量的计划。
formatted (bool): 如果为True返回格式化的字符串否则返回对象列表。
format_template (str): 当 formatted=True 时使用的格式化模板。
Returns: Returns:
bool: 操作是否成功 (如果已存在或成功生成) Union[List[MonthlyPlan], str, None]: 月度计划列表、格式化字符串或None。
""" """
if target_month is None: month = target_month or datetime.now().strftime("%Y-%m")
target_month = datetime.now().strftime("%Y-%m")
try: try:
logger.info(f"[ScheduleAPI] 正在确保 {target_month} 的月度计划存在...") logger.debug(f"[ScheduleAPI] 正在获取 {month} 的月度计划...")
return await schedule_manager.plan_manager.ensure_and_generate_plans_if_needed(target_month) plans = await get_active_plans_for_month(month)
if not plans:
return None
if random_count is not None and random_count > 0 and len(plans) > random_count:
plans = random.sample(plans, random_count)
if formatted:
return _format_schedule_list(plans, format_template, "plan")
return plans
except Exception as e: except Exception as e:
logger.error(f"[ScheduleAPI] 确保 {target_month} 月度计划失败: {e}") logger.error(f"[ScheduleAPI] 获取 {month} 月度计划失败: {e}")
return False return None
@staticmethod @staticmethod
async def archive_monthly_plans(target_month: str | None = None) -> bool: async def count_monthly_plans(target_month: Optional[str] = None) -> int:
"""(异步) 归档指定月份的月度计划 """
(异步) 获取指定月份的有效月度计划总数。
Args: Args:
target_month (Optional[str]): 目标月份,格式 "YYYY-MM"。如果为None则使用当前月份。 target_month (Optional[str]): 目标月份,格式 "YYYY-MM"。如果为None则使用当前月份。
Returns: Returns:
bool: 操作是否成功 int: 有效月度计划的数量。
""" """
if target_month is None: month = target_month or datetime.now().strftime("%Y-%m")
target_month = datetime.now().strftime("%Y-%m")
try: try:
logger.info(f"[ScheduleAPI] 正在归档 {target_month} 的月度计划...") logger.debug(f"[ScheduleAPI] 正在统计 {month} 的月度计划数量...")
await schedule_manager.plan_manager.archive_current_month_plans(target_month) async with get_db_session() as session:
return True result = await session.execute(
select(func.count(MonthlyPlan.id)).where(
MonthlyPlan.target_month == month, MonthlyPlan.status == "active"
)
)
return result.scalar_one() or 0
except Exception as e: except Exception as e:
logger.error(f"[ScheduleAPI] 归档 {target_month} 月度计划失败: {e}") logger.error(f"[ScheduleAPI] 统计 {month} 月度计划数量失败: {e}")
return False return 0
# ============================================================================= # =============================================================================
# 模块级别的便捷函数 (全部为异步) # 模块级别的便捷函数 (全部为异步)
# ============================================================================= # =============================================================================
async def get_schedule(
async def get_today_schedule() -> list[dict[str, Any]] | None: date: Optional[str] = None,
"""(异步) 获取今天的日程安排的便捷函数""" formatted: bool = False,
return await ScheduleAPI.get_today_schedule() format_template: str = "{time_range}: {activity}",
) -> Union[List[dict[str, Any]], str, None]:
"""(异步) 获取指定日期的日程安排的便捷函数。"""
return await ScheduleAPI.get_schedule(date, formatted, format_template)
async def get_current_activity() -> str | None: async def get_current_activity(
"""(异步) 获取当前正在进行的活动的便捷函数""" formatted: bool = False,
return await ScheduleAPI.get_current_activity() format_template: str = "{time_range}: {activity}",
) -> Union[dict[str, Any], str, None]:
"""(异步) 获取当前正在进行的活动的便捷函数。"""
return await ScheduleAPI.get_current_activity(formatted, format_template)
async def regenerate_schedule() -> bool: async def get_activities_between(
"""(异步) 触发后台重新生成今天的日程的便捷函数""" start_time: str,
return await ScheduleAPI.regenerate_schedule() end_time: str,
date: Optional[str] = None,
formatted: bool = False,
format_template: str = "{time_range}: {activity}",
) -> Union[List[dict[str, Any]], str, None]:
"""(异步) 获取指定时间范围内活动的便捷函数。"""
return await ScheduleAPI.get_activities_between(start_time, end_time, date, formatted, format_template)
async def get_monthly_plans(target_month: str | None = None) -> list[MonthlyPlan]: async def get_monthly_plans(
"""(异步) 获取指定月份的有效月度计划的便捷函数""" target_month: Optional[str] = None,
return await ScheduleAPI.get_monthly_plans(target_month) random_count: Optional[int] = None,
formatted: bool = False,
format_template: str = "- {plan_text}",
) -> Union[List[MonthlyPlan], str, None]:
"""(异步) 获取月度计划的便捷函数。"""
return await ScheduleAPI.get_monthly_plans(target_month, random_count, formatted, format_template)
async def ensure_monthly_plans(target_month: str | None = None) -> bool: async def count_monthly_plans(target_month: Optional[str] = None) -> int:
"""(异步) 确保指定月份存在月度计划的便捷函数""" """(异步) 获取月度计划总数的便捷函数"""
return await ScheduleAPI.ensure_monthly_plans(target_month) return await ScheduleAPI.count_monthly_plans(target_month)
async def archive_monthly_plans(target_month: str | None = None) -> bool:
"""(异步) 归档指定月份的月度计划的便捷函数"""
return await ScheduleAPI.archive_monthly_plans(target_month)