feat(monthly_plan): 增强月度计划系统,引入状态管理和智能抽取
对月度计划系统进行了全面的重构和功能增强,以提供更智能、更可持续的计划管理体验。 主要变更包括: - **引入状态生命周期**: 废弃了原有的 `is_deleted` 软删除标记,引入了更明确的 `status` 字段 (`active`, `completed`, `archived`),用于管理计划的整个生命周期。 - **增加使用统计与自动完成**: 新增 `usage_count` 和 `last_used_date` 字段来跟踪计划的使用情况。当计划使用次数达到可配置的阈值后,会自动标记为 `completed`。 - **实现智能计划抽取**: 为每日日程生成实现了新的智能抽取算法。该算法会优先选择使用次数较少且近期未被使用的计划,以增加计划的多样性并避免重复。 - **更新配置选项**: 移除了旧的概率删除相关配置,增加了 `completion_threshold`、`avoid_repetition_days` 等新选项以支持新逻辑。 - **数据库模型更新**: 更新了 `MonthlyPlan` 的数据库模型和索引,以支持新功能并优化查询性能。保留 `is_deleted` 字段以兼容旧数据。
This commit is contained in:
@@ -16,10 +16,10 @@ def add_new_plans(plans: List[str], month: str):
|
||||
"""
|
||||
with get_db_session() as session:
|
||||
try:
|
||||
# 1. 获取当前有效计划数量
|
||||
# 1. 获取当前有效计划数量(状态为 'active')
|
||||
current_plan_count = session.query(MonthlyPlan).filter(
|
||||
MonthlyPlan.target_month == month,
|
||||
not MonthlyPlan.is_deleted
|
||||
MonthlyPlan.status == 'active'
|
||||
).count()
|
||||
|
||||
# 2. 从配置获取上限
|
||||
@@ -36,7 +36,7 @@ def add_new_plans(plans: List[str], month: str):
|
||||
plans_to_add = plans[:remaining_slots]
|
||||
|
||||
new_plan_objects = [
|
||||
MonthlyPlan(plan_text=plan, target_month=month)
|
||||
MonthlyPlan(plan_text=plan, target_month=month, status='active')
|
||||
for plan in plans_to_add
|
||||
]
|
||||
session.add_all(new_plan_objects)
|
||||
@@ -53,7 +53,7 @@ def add_new_plans(plans: List[str], month: str):
|
||||
|
||||
def get_active_plans_for_month(month: str) -> List[MonthlyPlan]:
|
||||
"""
|
||||
获取指定月份所有未被软删除的计划。
|
||||
获取指定月份所有状态为 'active' 的计划。
|
||||
|
||||
:param month: 目标月份,格式为 "YYYY-MM"。
|
||||
:return: MonthlyPlan 对象列表。
|
||||
@@ -62,18 +62,18 @@ def get_active_plans_for_month(month: str) -> List[MonthlyPlan]:
|
||||
try:
|
||||
plans = session.query(MonthlyPlan).filter(
|
||||
MonthlyPlan.target_month == month,
|
||||
not MonthlyPlan.is_deleted
|
||||
MonthlyPlan.status == 'active'
|
||||
).all()
|
||||
return plans
|
||||
except Exception as e:
|
||||
logger.error(f"查询 {month} 的有效月度计划时发生错误: {e}")
|
||||
return []
|
||||
|
||||
def soft_delete_plans(plan_ids: List[int]):
|
||||
def mark_plans_completed(plan_ids: List[int]):
|
||||
"""
|
||||
将指定ID的计划标记为软删除。
|
||||
将指定ID的计划标记为已完成。
|
||||
|
||||
:param plan_ids: 需要软删除的计划ID列表。
|
||||
:param plan_ids: 需要标记为完成的计划ID列表。
|
||||
"""
|
||||
if not plan_ids:
|
||||
return
|
||||
@@ -82,10 +82,178 @@ def soft_delete_plans(plan_ids: List[int]):
|
||||
try:
|
||||
session.query(MonthlyPlan).filter(
|
||||
MonthlyPlan.id.in_(plan_ids)
|
||||
).update({"is_deleted": True}, synchronize_session=False)
|
||||
).update({"status": "completed"}, synchronize_session=False)
|
||||
session.commit()
|
||||
logger.info(f"成功软删除了 {len(plan_ids)} 条月度计划。")
|
||||
logger.info(f"成功将 {len(plan_ids)} 条月度计划标记为已完成。")
|
||||
except Exception as e:
|
||||
logger.error(f"软删除月度计划时发生错误: {e}")
|
||||
logger.error(f"标记月度计划为完成时发生错误: {e}")
|
||||
session.rollback()
|
||||
raise
|
||||
raise
|
||||
|
||||
def soft_delete_plans(plan_ids: List[int]):
|
||||
"""
|
||||
将指定ID的计划标记为软删除(兼容旧接口)。
|
||||
现在实际上是标记为已完成。
|
||||
|
||||
:param plan_ids: 需要软删除的计划ID列表。
|
||||
"""
|
||||
logger.warning("soft_delete_plans 已弃用,请使用 mark_plans_completed")
|
||||
mark_plans_completed(plan_ids)
|
||||
|
||||
def update_plan_usage(plan_ids: List[int], used_date: str):
|
||||
"""
|
||||
更新计划的使用统计信息。
|
||||
|
||||
:param plan_ids: 使用的计划ID列表。
|
||||
:param used_date: 使用日期,格式为 "YYYY-MM-DD"。
|
||||
"""
|
||||
if not plan_ids:
|
||||
return
|
||||
|
||||
with get_db_session() as session:
|
||||
try:
|
||||
# 获取完成阈值配置,如果不存在则使用默认值
|
||||
completion_threshold = getattr(global_config.monthly_plan_system, 'completion_threshold', 3)
|
||||
|
||||
# 批量更新使用次数和最后使用日期
|
||||
session.query(MonthlyPlan).filter(
|
||||
MonthlyPlan.id.in_(plan_ids)
|
||||
).update({
|
||||
"usage_count": MonthlyPlan.usage_count + 1,
|
||||
"last_used_date": used_date
|
||||
}, synchronize_session=False)
|
||||
|
||||
# 检查是否有计划达到完成阈值
|
||||
plans_to_complete = session.query(MonthlyPlan).filter(
|
||||
MonthlyPlan.id.in_(plan_ids),
|
||||
MonthlyPlan.usage_count >= completion_threshold,
|
||||
MonthlyPlan.status == 'active'
|
||||
).all()
|
||||
|
||||
if plans_to_complete:
|
||||
completed_ids = [plan.id for plan in plans_to_complete]
|
||||
session.query(MonthlyPlan).filter(
|
||||
MonthlyPlan.id.in_(completed_ids)
|
||||
).update({
|
||||
"status": "completed"
|
||||
}, synchronize_session=False)
|
||||
|
||||
logger.info(f"计划 {completed_ids} 已达到使用阈值 ({completion_threshold}),标记为已完成。")
|
||||
|
||||
session.commit()
|
||||
logger.info(f"成功更新了 {len(plan_ids)} 条月度计划的使用统计。")
|
||||
except Exception as e:
|
||||
logger.error(f"更新月度计划使用统计时发生错误: {e}")
|
||||
session.rollback()
|
||||
raise
|
||||
|
||||
def get_smart_plans_for_daily_schedule(month: str, max_count: int = 3, avoid_days: int = 7) -> List[MonthlyPlan]:
|
||||
"""
|
||||
智能抽取月度计划用于每日日程生成。
|
||||
|
||||
抽取规则:
|
||||
1. 避免短期内重复(avoid_days 天内不重复抽取同一个计划)
|
||||
2. 优先抽取使用次数较少的计划
|
||||
3. 在满足以上条件的基础上随机抽取
|
||||
|
||||
:param month: 目标月份,格式为 "YYYY-MM"。
|
||||
:param max_count: 最多抽取的计划数量。
|
||||
:param avoid_days: 避免重复的天数。
|
||||
:return: MonthlyPlan 对象列表。
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
with get_db_session() as session:
|
||||
try:
|
||||
# 计算避免重复的日期阈值
|
||||
avoid_date = (datetime.now() - timedelta(days=avoid_days)).strftime("%Y-%m-%d")
|
||||
|
||||
# 查询符合条件的计划
|
||||
query = session.query(MonthlyPlan).filter(
|
||||
MonthlyPlan.target_month == month,
|
||||
MonthlyPlan.status == 'active'
|
||||
)
|
||||
|
||||
# 排除最近使用过的计划
|
||||
query = query.filter(
|
||||
(MonthlyPlan.last_used_date.is_(None)) |
|
||||
(MonthlyPlan.last_used_date < avoid_date)
|
||||
)
|
||||
|
||||
# 按使用次数升序排列,优先选择使用次数少的
|
||||
plans = query.order_by(MonthlyPlan.usage_count.asc()).all()
|
||||
|
||||
if not plans:
|
||||
logger.info(f"没有找到符合条件的 {month} 月度计划。")
|
||||
return []
|
||||
|
||||
# 如果计划数量超过需要的数量,进行随机抽取
|
||||
if len(plans) > max_count:
|
||||
import random
|
||||
plans = random.sample(plans, max_count)
|
||||
|
||||
logger.info(f"智能抽取了 {len(plans)} 条 {month} 的月度计划用于每日日程生成。")
|
||||
return plans
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"智能抽取 {month} 的月度计划时发生错误: {e}")
|
||||
return []
|
||||
|
||||
def archive_active_plans_for_month(month: str):
|
||||
"""
|
||||
将指定月份所有状态为 'active' 的计划归档为 'archived'。
|
||||
通常在月底调用。
|
||||
|
||||
:param month: 目标月份,格式为 "YYYY-MM"。
|
||||
"""
|
||||
with get_db_session() as session:
|
||||
try:
|
||||
updated_count = session.query(MonthlyPlan).filter(
|
||||
MonthlyPlan.target_month == month,
|
||||
MonthlyPlan.status == 'active'
|
||||
).update({"status": "archived"}, synchronize_session=False)
|
||||
|
||||
session.commit()
|
||||
logger.info(f"成功将 {updated_count} 条 {month} 的活跃月度计划归档。")
|
||||
return updated_count
|
||||
except Exception as e:
|
||||
logger.error(f"归档 {month} 的月度计划时发生错误: {e}")
|
||||
session.rollback()
|
||||
raise
|
||||
|
||||
def get_archived_plans_for_month(month: str) -> List[MonthlyPlan]:
|
||||
"""
|
||||
获取指定月份所有状态为 'archived' 的计划。
|
||||
用于生成下个月计划时的参考。
|
||||
|
||||
:param month: 目标月份,格式为 "YYYY-MM"。
|
||||
:return: MonthlyPlan 对象列表。
|
||||
"""
|
||||
with get_db_session() as session:
|
||||
try:
|
||||
plans = session.query(MonthlyPlan).filter(
|
||||
MonthlyPlan.target_month == month,
|
||||
MonthlyPlan.status == 'archived'
|
||||
).all()
|
||||
return plans
|
||||
except Exception as e:
|
||||
logger.error(f"查询 {month} 的归档月度计划时发生错误: {e}")
|
||||
return []
|
||||
|
||||
def has_active_plans(month: str) -> bool:
|
||||
"""
|
||||
检查指定月份是否存在任何状态为 'active' 的计划。
|
||||
|
||||
:param month: 目标月份,格式为 "YYYY-MM"。
|
||||
:return: 如果存在则返回 True,否则返回 False。
|
||||
"""
|
||||
with get_db_session() as session:
|
||||
try:
|
||||
count = session.query(MonthlyPlan).filter(
|
||||
MonthlyPlan.target_month == month,
|
||||
MonthlyPlan.status == 'active'
|
||||
).count()
|
||||
return count > 0
|
||||
except Exception as e:
|
||||
logger.error(f"检查 {month} 的有效月度计划时发生错误: {e}")
|
||||
return False
|
||||
@@ -5,11 +5,12 @@
|
||||
|
||||
from sqlalchemy import Column, String, Float, Integer, Boolean, Text, Index, create_engine, DateTime
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.pool import QueuePool
|
||||
import os
|
||||
import datetime
|
||||
import time
|
||||
from typing import Iterator, Optional
|
||||
from src.common.logger import get_logger
|
||||
from contextlib import contextmanager
|
||||
|
||||
@@ -508,16 +509,25 @@ class CacheEntries(Base):
|
||||
)
|
||||
|
||||
class MonthlyPlan(Base):
|
||||
"""月层计划模型"""
|
||||
"""月度计划模型"""
|
||||
__tablename__ = 'monthly_plans'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
plan_text = Column(Text, nullable=False)
|
||||
target_month = Column(String(7), nullable=False, index=True) # "YYYY-MM"
|
||||
is_deleted = Column(Boolean, nullable=False, default=False, index=True)
|
||||
status = Column(get_string_field(20), nullable=False, default='active', index=True) # 'active', 'completed', 'archived'
|
||||
usage_count = Column(Integer, nullable=False, default=0)
|
||||
last_used_date = Column(String(10), nullable=True, index=True) # "YYYY-MM-DD" format
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
|
||||
# 保留 is_deleted 字段以兼容现有数据,但标记为已弃用
|
||||
is_deleted = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_monthlyplan_target_month_status', 'target_month', 'status'),
|
||||
Index('idx_monthlyplan_last_used_date', 'last_used_date'),
|
||||
Index('idx_monthlyplan_usage_count', 'usage_count'),
|
||||
# 保留旧索引以兼容
|
||||
Index('idx_monthlyplan_target_month_is_deleted', 'target_month', 'is_deleted'),
|
||||
)
|
||||
|
||||
@@ -628,9 +638,9 @@ def initialize_database():
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_session():
|
||||
def get_db_session() -> Iterator[Session]:
|
||||
"""数据库会话上下文管理器 - 推荐使用这个而不是get_session()"""
|
||||
session = None
|
||||
session: Optional[Session] = None
|
||||
try:
|
||||
_, SessionLocal = initialize_database()
|
||||
session = SessionLocal()
|
||||
|
||||
Reference in New Issue
Block a user