feat: 实现日历提醒完整功能(操作执行、通知服务重构、归档)

- 新增 ReminderActionExecutor 处理取消/稍后提醒操作
- 新增 ReminderOutboxStore 本地存储待处理操作
- 重构 LocalNotificationService 支持聚合提醒和交互操作
- 新增 event_color_resolver 工具类统一颜色解析
- 新增 CalendarService.archiveEvent 归档方法
- 增强 ModelTracking 支持缓存命中、推理token和成本追踪
- 添加 qwen3.5-35b-a3b 模型配置
- 更新 AndroidManifest 全屏intent权限
- 补充相关单元测试和文档
This commit is contained in:
qzl
2026-03-18 19:12:47 +08:00
parent 257cb0f5d5
commit 00f37d7e19
35 changed files with 2676 additions and 244 deletions
+53 -3
View File
@@ -9,7 +9,7 @@ from sqlalchemy.exc import SQLAlchemyError
from core.db.base_repository import BaseRepository
from core.logging import get_logger
from models.schedule_items import ScheduleItem
from models.schedule_items import ScheduleItem, ScheduleItemStatus
from models.schedule_subscriptions import ScheduleSubscription, SubscriptionStatus
if TYPE_CHECKING:
@@ -61,6 +61,11 @@ class ScheduleItemRepository(Protocol):
start_at: datetime,
end_at: datetime,
) -> Sequence[tuple[ScheduleItem, ScheduleSubscription]]: ...
async def archive_expired_subscribed_items(
self,
subscriber_id: UUID,
now_at: datetime,
) -> int: ...
class SQLAlchemyScheduleItemRepository(BaseRepository[ScheduleItem]):
@@ -149,8 +154,13 @@ class SQLAlchemyScheduleItemRepository(BaseRepository[ScheduleItem]):
select(ScheduleItem)
.where(ScheduleItem.owner_id == owner_id)
.where(ScheduleItem.deleted_at.is_(None))
.where(ScheduleItem.start_at >= start_at)
.where(ScheduleItem.start_at <= end_at)
.where(
or_(
ScheduleItem.end_at.is_(None),
ScheduleItem.end_at >= start_at,
)
)
.order_by(ScheduleItem.start_at.asc())
)
result = await self._session.execute(stmt)
@@ -308,8 +318,13 @@ class SQLAlchemyScheduleItemRepository(BaseRepository[ScheduleItem]):
.where(ScheduleSubscription.subscriber_id == subscriber_id)
.where(ScheduleSubscription.status == SubscriptionStatus.ACTIVE)
.where(ScheduleItem.deleted_at.is_(None))
.where(ScheduleItem.start_at >= start_at)
.where(ScheduleItem.start_at <= end_at)
.where(
or_(
ScheduleItem.end_at.is_(None),
ScheduleItem.end_at >= start_at,
)
)
.order_by(ScheduleItem.start_at.asc())
)
result = await self._session.execute(stmt)
@@ -317,3 +332,38 @@ class SQLAlchemyScheduleItemRepository(BaseRepository[ScheduleItem]):
except SQLAlchemyError:
logger.exception("Failed to list subscribed items")
raise
async def archive_expired_subscribed_items(
self,
subscriber_id: UUID,
now_at: datetime,
) -> int:
try:
item_ids_subquery = (
select(ScheduleItem.id)
.join(
ScheduleSubscription,
ScheduleSubscription.item_id == ScheduleItem.id,
)
.where(ScheduleSubscription.subscriber_id == subscriber_id)
.where(ScheduleSubscription.status == SubscriptionStatus.ACTIVE)
.where(ScheduleItem.deleted_at.is_(None))
.where(ScheduleItem.status == ScheduleItemStatus.ACTIVE)
.where(ScheduleItem.end_at.is_not(None))
.where(ScheduleItem.end_at <= now_at)
)
stmt = (
update(ScheduleItem)
.where(ScheduleItem.id.in_(item_ids_subquery))
.values(status=ScheduleItemStatus.ARCHIVED)
)
result = await self._session.execute(stmt)
await self._session.flush()
return int(getattr(result, "rowcount", 0) or 0)
except SQLAlchemyError:
logger.exception(
"Failed to archive expired subscribed items",
subscriber_id=str(subscriber_id),
)
raise
+8
View File
@@ -240,6 +240,11 @@ class ScheduleItemService(BaseService):
raise HTTPException(status_code=400, detail="end_at must be after start_at")
try:
archived_count = await self._repository.archive_expired_subscribed_items(
user_id,
datetime.now(timezone.utc),
)
subscribed_items = (
await self._repository.list_subscribed_items_by_date_range(
user_id, normalized_start_at, normalized_end_at
@@ -256,9 +261,12 @@ class ScheduleItemService(BaseService):
)
results.sort(key=lambda x: x.start_at)
if archived_count > 0:
await self._session.commit()
return results
except SQLAlchemyError:
await self._session.rollback()
logger.exception("Failed to list schedule items")
raise HTTPException(
status_code=503, detail="Schedule item store unavailable"