feat(backend): enhance task and schedule center APIs with expanded endpoints

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-11 08:47:39 +08:00
parent 8c7cf0732b
commit 3e39b40a50
6 changed files with 1015 additions and 107 deletions

View File

@@ -1,25 +1,62 @@
from calendar import monthrange
from datetime import UTC, date, datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.models.goal import Goal
from app.models.reminder import Reminder
from app.models.task import Task, TaskPriority
from app.models.task import Task, TaskDispatchStatus, TaskPriority, TaskQuadrant, TaskStatus
from app.models.todo import DailyTodo
from app.models.user import User
from app.routers.auth import get_current_user
from app.schemas.schedule_center import (
ScheduleCenterCommanderSummaryOut,
ScheduleCenterDateOut,
ScheduleCenterDaySummary,
ScheduleCenterFocusTaskOut,
ScheduleCenterMonthOut,
ScheduleCenterQuadrantOut,
ScheduleCenterQuadrantTaskOut,
)
from app.schemas.task import build_task_out
router = APIRouter(prefix="/api/schedule-center", tags=["调度中心"])
QUADRANT_META: dict[TaskQuadrant, dict[str, str]] = {
TaskQuadrant.URGENT_IMPORTANT: {
"title": "重要且紧急",
"subtitle": "CRITICAL",
"color": "#ff4757",
"glow_color": "rgba(255, 71, 87, 0.4)",
"icon": "",
},
TaskQuadrant.NOT_URGENT_IMPORTANT: {
"title": "重要不紧急",
"subtitle": "PLANNED",
"color": "#ffd93d",
"glow_color": "rgba(255, 217, 61, 0.4)",
"icon": "",
},
TaskQuadrant.URGENT_NOT_IMPORTANT: {
"title": "紧急不重要",
"subtitle": "DELEGATE",
"color": "#00d4ff",
"glow_color": "rgba(0, 212, 255, 0.4)",
"icon": "",
},
TaskQuadrant.NOT_URGENT_NOT_IMPORTANT: {
"title": "不重要不紧急",
"subtitle": "ELIMINATE",
"color": "#6bcf7f",
"glow_color": "rgba(107, 207, 127, 0.4)",
"icon": "",
},
}
def _build_summary(
target_date: str,
@@ -39,6 +76,146 @@ def _build_summary(
)
def _coerce_enum(value, enum_cls, default=None):
if value is None:
return default
if isinstance(value, enum_cls):
return value
if isinstance(value, str):
raw = value.strip()
if not raw:
return default
for item in enum_cls:
if raw == item.value or raw.lower() == item.value:
return item
if raw.upper() == item.name:
return item
return default
def _derive_quadrant(task: Task) -> TaskQuadrant:
quadrant = _coerce_enum(task.quadrant, TaskQuadrant, None)
if quadrant is not None:
return quadrant
priority = _coerce_enum(task.priority, TaskPriority, TaskPriority.MEDIUM)
status = _coerce_enum(task.status, TaskStatus, TaskStatus.TODO)
if priority in {TaskPriority.HIGH, TaskPriority.URGENT}:
return TaskQuadrant.URGENT_IMPORTANT
if status == TaskStatus.IN_PROGRESS:
return TaskQuadrant.NOT_URGENT_IMPORTANT
if priority == TaskPriority.MEDIUM:
return TaskQuadrant.URGENT_NOT_IMPORTANT
return TaskQuadrant.NOT_URGENT_NOT_IMPORTANT
def _enum_value(value) -> str | None:
if value is None:
return None
if hasattr(value, "value"):
return str(value.value)
if isinstance(value, str):
raw = value.strip()
return raw or None
return str(value)
def _build_focus_tasks(tasks: list[Task]) -> list[ScheduleCenterFocusTaskOut]:
priority_rank = {
TaskPriority.URGENT: 0,
TaskPriority.HIGH: 1,
TaskPriority.MEDIUM: 2,
TaskPriority.LOW: 3,
}
status_rank = {
TaskStatus.IN_PROGRESS: 0,
TaskStatus.TODO: 1,
TaskStatus.DONE: 2,
TaskStatus.CANCELLED: 3,
}
ordered = sorted(
tasks,
key=lambda item: (
status_rank.get(_coerce_enum(item.status, TaskStatus, TaskStatus.TODO), 99),
priority_rank.get(_coerce_enum(item.priority, TaskPriority, TaskPriority.MEDIUM), 99),
item.created_at,
),
)
return [
ScheduleCenterFocusTaskOut(
id=item.id,
title=item.title,
status=_coerce_enum(item.status, TaskStatus, TaskStatus.TODO),
priority=_coerce_enum(item.priority, TaskPriority, TaskPriority.MEDIUM),
quadrant=_derive_quadrant(item),
assignee_type=_enum_value(item.assignee_type),
assignee_id=item.assignee_id,
dispatch_status=_coerce_enum(item.dispatch_status, TaskDispatchStatus, TaskDispatchStatus.IDLE),
due_date=item.due_date,
)
for item in ordered[:6]
]
def _build_quadrants(tasks: list[Task]) -> list[ScheduleCenterQuadrantOut]:
buckets: dict[TaskQuadrant, list[ScheduleCenterQuadrantTaskOut]] = {
quadrant: [] for quadrant in QUADRANT_META
}
for task in tasks:
quadrant = _derive_quadrant(task)
buckets[quadrant].append(
ScheduleCenterQuadrantTaskOut(
id=task.id,
title=task.title,
status=_coerce_enum(task.status, TaskStatus, TaskStatus.TODO),
priority=_coerce_enum(task.priority, TaskPriority, TaskPriority.MEDIUM),
dispatch_status=_coerce_enum(task.dispatch_status, TaskDispatchStatus, TaskDispatchStatus.IDLE),
assignee_type=_enum_value(task.assignee_type),
assignee_id=task.assignee_id,
)
)
return [
ScheduleCenterQuadrantOut(
id=quadrant,
title=meta["title"],
subtitle=meta["subtitle"],
color=meta["color"],
glow_color=meta["glow_color"],
icon=meta["icon"],
tasks=buckets[quadrant],
)
for quadrant, meta in QUADRANT_META.items()
]
def _build_commander_summary(tasks: list[Task]) -> ScheduleCenterCommanderSummaryOut:
counts = ScheduleCenterCommanderSummaryOut()
for task in tasks:
states = [task.dispatch_status, *(subtask.dispatch_status for subtask in task.subtasks)]
for state in states:
normalized = _coerce_enum(state, TaskDispatchStatus, TaskDispatchStatus.IDLE)
if normalized == TaskDispatchStatus.IDLE:
continue
counts.total += 1
if normalized == TaskDispatchStatus.QUEUED:
counts.queued += 1
elif normalized == TaskDispatchStatus.RUNNING:
counts.running += 1
elif normalized == TaskDispatchStatus.COMPLETED:
counts.completed += 1
elif normalized == TaskDispatchStatus.FAILED:
counts.failed += 1
if counts.running > 0:
counts.overall_status = "running"
elif counts.queued > 0:
counts.overall_status = "queued"
elif counts.failed > 0 and counts.completed == 0:
counts.overall_status = "failed"
return counts
@router.get("/month", response_model=ScheduleCenterMonthOut)
async def get_month_schedule(
year: int = Query(..., ge=2000, le=2100),
@@ -53,27 +230,43 @@ async def get_month_schedule(
start_dt = datetime.combine(month_start, datetime.min.time())
end_dt = datetime.combine(month_start.replace(day=days_in_month), datetime.max.time())
todos = (await db.execute(
select(DailyTodo).where(DailyTodo.user_id == current_user.id, DailyTodo.todo_date >= start_key, DailyTodo.todo_date <= end_key)
)).scalars().all()
tasks = (await db.execute(
select(Task).where(
Task.user_id == current_user.id,
Task.due_date.is_not(None),
Task.due_date >= start_dt,
Task.due_date <= end_dt,
todos = (
await db.execute(
select(DailyTodo).where(
DailyTodo.user_id == current_user.id,
DailyTodo.todo_date >= start_key,
DailyTodo.todo_date <= end_key,
)
)
)).scalars().all()
reminders = (await db.execute(
select(Reminder).where(
Reminder.user_id == current_user.id,
Reminder.reminder_at >= start_dt,
Reminder.reminder_at <= end_dt,
).scalars().all()
tasks = (
await db.execute(
select(Task).where(
Task.user_id == current_user.id,
Task.due_date.is_not(None),
Task.due_date >= start_dt,
Task.due_date <= end_dt,
)
)
)).scalars().all()
goals = (await db.execute(
select(Goal).where(Goal.user_id == current_user.id, Goal.goal_date >= start_key, Goal.goal_date <= end_key)
)).scalars().all()
).scalars().all()
reminders = (
await db.execute(
select(Reminder).where(
Reminder.user_id == current_user.id,
Reminder.reminder_at >= start_dt,
Reminder.reminder_at <= end_dt,
)
)
).scalars().all()
goals = (
await db.execute(
select(Goal).where(
Goal.user_id == current_user.id,
Goal.goal_date >= start_key,
Goal.goal_date <= end_key,
)
)
).scalars().all()
todo_map: dict[str, list[DailyTodo]] = {}
for item in todos:
@@ -96,18 +289,20 @@ async def get_month_schedule(
days = []
for day in range(1, days_in_month + 1):
date_key = month_start.replace(day=day).isoformat()
days.append(_build_summary(
date_key,
todo_map.get(date_key, []),
task_map.get(date_key, []),
reminder_map.get(date_key, []),
goal_map.get(date_key, []),
))
days.append(
_build_summary(
date_key,
todo_map.get(date_key, []),
task_map.get(date_key, []),
reminder_map.get(date_key, []),
goal_map.get(date_key, []),
)
)
return ScheduleCenterMonthOut(month=f"{year:04d}-{month:02d}", days=days)
@router.get("/date", response_model=ScheduleCenterDateOut)
@router.get("/date", response_model=ScheduleCenterDateOut, response_model_exclude_none=True)
async def get_date_schedule(
date_str: date = Query(...),
current_user: User = Depends(get_current_user),
@@ -118,43 +313,55 @@ async def get_date_schedule(
end_dt = datetime.combine(target_date, datetime.max.time())
date_key = target_date.isoformat()
todos = (await db.execute(
select(DailyTodo)
.where(DailyTodo.user_id == current_user.id, DailyTodo.todo_date == date_key)
.order_by(DailyTodo.created_at.desc())
)).scalars().all()
tasks = (await db.execute(
select(Task)
.where(
Task.user_id == current_user.id,
Task.due_date.is_not(None),
Task.due_date >= start_dt,
Task.due_date <= end_dt,
todos = (
await db.execute(
select(DailyTodo)
.where(DailyTodo.user_id == current_user.id, DailyTodo.todo_date == date_key)
.order_by(DailyTodo.created_at.desc())
)
.order_by(Task.created_at.desc())
)).scalars().all()
reminders = (await db.execute(
select(Reminder)
.where(
Reminder.user_id == current_user.id,
Reminder.reminder_at >= start_dt,
Reminder.reminder_at <= end_dt,
).scalars().all()
tasks = (
await db.execute(
select(Task)
.options(selectinload(Task.subtasks), selectinload(Task.history))
.where(
Task.user_id == current_user.id,
Task.due_date.is_not(None),
Task.due_date >= start_dt,
Task.due_date <= end_dt,
)
.order_by(Task.priority.desc(), Task.created_at.desc())
)
.order_by(Reminder.reminder_at.asc(), Reminder.created_at.asc())
)).scalars().all()
goals = (await db.execute(
select(Goal)
.where(Goal.user_id == current_user.id, Goal.goal_date == date_key)
.order_by(Goal.created_at.desc())
)).scalars().all()
).scalars().unique().all()
reminders = (
await db.execute(
select(Reminder)
.where(
Reminder.user_id == current_user.id,
Reminder.reminder_at >= start_dt,
Reminder.reminder_at <= end_dt,
)
.order_by(Reminder.reminder_at.asc(), Reminder.created_at.asc())
)
).scalars().all()
goals = (
await db.execute(
select(Goal)
.where(Goal.user_id == current_user.id, Goal.goal_date == date_key)
.order_by(Goal.created_at.desc())
)
).scalars().all()
summary = _build_summary(date_key, todos, tasks, reminders, goals)
return ScheduleCenterDateOut(
date=date_key,
todos=todos,
tasks=tasks,
tasks=[build_task_out(task) for task in tasks],
reminders=reminders,
goals=goals,
summary=summary,
focus_tasks=_build_focus_tasks(tasks),
quadrants=_build_quadrants(tasks),
commander_summary=_build_commander_summary(tasks),
generated_at=datetime.now(UTC),
)