perf: reduce dodo data loading requests
ci / docker (push) Successful in 6m31s

This commit is contained in:
2026-09-05 19:51:00 +08:00
parent e8c49b045f
commit 2987a787d3
8 changed files with 248 additions and 44 deletions
+11 -2
View File
@@ -8,6 +8,7 @@ from sqlalchemy import (
DateTime,
Float,
ForeignKey,
Index,
Integer,
String,
Text,
@@ -94,7 +95,12 @@ class TaskTag(Base):
class Task(Base):
__tablename__ = "tasks"
__table_args__ = (UniqueConstraint("user_id", "external_id", name="uq_tasks_external_id"),)
__table_args__ = (
UniqueConstraint("user_id", "external_id", name="uq_tasks_external_id"),
Index("ix_tasks_user_active_created", "user_id", "deleted_at", "parent_id", "created_at", "id"),
Index("ix_tasks_user_due", "user_id", "deleted_at", "due_at"),
Index("ix_tasks_user_list_active", "user_id", "list_id", "deleted_at", "parent_id", "created_at", "id"),
)
id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id)
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
list_id: Mapped[UUID] = mapped_column(ForeignKey("task_lists.id", ondelete="CASCADE"), index=True)
@@ -154,7 +160,10 @@ class Habit(Base):
class HabitLog(Base):
__tablename__ = "habit_logs"
__table_args__ = (UniqueConstraint("habit_id", "day", name="uq_habit_log_day"),)
__table_args__ = (
UniqueConstraint("habit_id", "day", name="uq_habit_log_day"),
Index("ix_habit_logs_habit_day", "habit_id", "day"),
)
id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id)
habit_id: Mapped[UUID] = mapped_column(ForeignKey("habits.id", ondelete="CASCADE"), index=True)
day: Mapped[date] = mapped_column(Date)
+67 -16
View File
@@ -149,20 +149,42 @@ async def calendar(start: date, end: date, user: User = Depends(current_user), d
raise HTTPException(422, "日期范围无效或超过一年")
start_dt = datetime.combine(start, time.min, tzinfo=UTC)
end_dt = datetime.combine(end, time.max, tzinfo=UTC)
rows = (await db.execute(select(RecurrenceTemplate, Task).join(Task).where(RecurrenceTemplate.user_id == user.id, Task.deleted_at.is_(None)))).all()
output = []
for template, task in rows:
exception_rows = (await db.scalars(select(RecurrenceException).where(RecurrenceException.template_id == template.id))).all()
exceptions: dict[datetime, RecurrenceException] = {}
for exc in exception_rows:
key = exc.occurrence_at.replace(tzinfo=UTC) if exc.occurrence_at.tzinfo is None else exc.occurrence_at
exceptions[key] = exc
recurrence_rows = (await db.execute(select(RecurrenceTemplate, Task).join(Task).where(RecurrenceTemplate.user_id == user.id, Task.deleted_at.is_(None)))).all()
template_ids = [template.id for template, _ in recurrence_rows]
exception_rows = [] if not template_ids else list((await db.scalars(select(RecurrenceException).where(RecurrenceException.template_id.in_(template_ids)))).all())
exceptions_by_template = {}
for exception in exception_rows:
key = exception.occurrence_at.replace(tzinfo=UTC) if exception.occurrence_at.tzinfo is None else exception.occurrence_at
exceptions_by_template.setdefault(exception.template_id, {})[key] = exception
recurring_task_ids = {task.id for _, task in recurrence_rows}
normal_query = select(Task).where(
Task.user_id == user.id,
Task.deleted_at.is_(None),
Task.parent_id.is_(None),
Task.due_at >= start_dt,
Task.due_at <= end_dt,
)
if recurring_task_ids:
normal_query = normal_query.where(Task.id.not_in(recurring_task_ids))
normal_tasks = list((await db.scalars(normal_query)).all())
output = [{
"id": task.id,
"recurrence_id": None,
"task_id": task.id,
"occurrence_at": task.due_at,
"title": task.title,
"due_at": task.due_at,
"completed": task.completed,
"version": task.version,
} for task in normal_tasks]
for template, task in recurrence_rows:
exceptions = exceptions_by_template.get(template.id, {})
for at in occurrences(template.rrule, template.starts_at, start_dt, end_dt, template.ends_at):
exception = exceptions.get(at)
if exception and exception.deleted:
continue
output.append({"recurrence_id": template.id, "task_id": task.id, "occurrence_at": at, "title": exception.title if exception and exception.title else task.title, "due_at": exception.due_at if exception and exception.due_at else at, "completed": bool(exception and exception.completed)})
return sorted(output, key=lambda item: item["occurrence_at"])
return sorted(output, key=lambda item: item["occurrence_at"].replace(tzinfo=UTC) if item["occurrence_at"].tzinfo is None else item["occurrence_at"])
async def upsert_exception(db, template_id, at):
@@ -310,9 +332,14 @@ async def edit_habit_log(habit_id: UUID, day: date, payload: HabitLogEdit, user:
@router.get("/habits/{habit_id}/logs")
async def habit_logs(habit_id: UUID, user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
async def habit_logs(habit_id: UUID, from_date: date | None = Query(default=None, alias="from"), to_date: date | None = Query(default=None, alias="to"), user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
habit = await owned_habit(db, user.id, habit_id)
return [{"day": x.day, "value": x.value} for x in (await db.scalars(select(HabitLog).where(HabitLog.habit_id == habit.id).order_by(HabitLog.day.desc()))).all()]
condition = HabitLog.habit_id == habit.id
if from_date is not None:
condition = condition & (HabitLog.day >= from_date)
if to_date is not None:
condition = condition & (HabitLog.day <= to_date)
return [{"day": x.day, "value": x.value} for x in (await db.scalars(select(HabitLog).where(condition).order_by(HabitLog.day.desc()))).all()]
@router.post("/habits/{habit_id}/pauses", status_code=201)
@@ -331,12 +358,36 @@ def scheduled(h, day):
@router.get("/habits/grid")
async def habits_grid(week: date, user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
start = week - timedelta(days=week.weekday()); days = [start + timedelta(days=i) for i in range(7)]
habits = list((await db.scalars(select(Habit).where(Habit.user_id == user.id, Habit.archived_at.is_(None)))).all())
habits = list((await db.scalars(select(Habit).where(Habit.user_id == user.id, Habit.archived_at.is_(None)).order_by(Habit.created_at))).all())
if not habits:
return {"days": days, "habits": []}
habit_ids = [habit.id for habit in habits]
week_logs = (await db.scalars(select(HabitLog).where(HabitLog.habit_id.in_(habit_ids), HabitLog.day.between(days[0], days[-1])))).all()
all_logs = (await db.scalars(select(HabitLog).where(HabitLog.habit_id.in_(habit_ids)))).all()
pause_rows = (await db.scalars(select(HabitPause).where(HabitPause.habit_id.in_(habit_ids), HabitPause.end_date >= days[0], HabitPause.start_date <= days[-1]))).all()
logs_by_habit = {}
stats_by_habit = {}
pauses_by_habit = {}
for log in week_logs:
logs_by_habit.setdefault(log.habit_id, {})[log.day] = log.value
for log in all_logs:
stats = stats_by_habit.setdefault(log.habit_id, {"total": 0, "completed_days": 0, "logged_days": 0})
stats["total"] += log.value
stats["logged_days"] += 1
targets = {habit.id: habit.target for habit in habits}
for log in all_logs:
if log.value >= targets[log.habit_id]:
stats_by_habit[log.habit_id]["completed_days"] += 1
for pause in pause_rows:
pauses_by_habit.setdefault(pause.habit_id, []).append(pause)
output = []
for h in habits:
logs = {x.day: x.value for x in (await db.scalars(select(HabitLog).where(HabitLog.habit_id == h.id, HabitLog.day.between(days[0], days[-1])))).all()}
pauses = list((await db.scalars(select(HabitPause).where(HabitPause.habit_id == h.id))).all())
output.append({"id": h.id, "name": h.name, "cells": [{"day": d, "scheduled": scheduled(h, d), "paused": any(p.start_date <= d <= p.end_date for p in pauses), "value": logs.get(d, 0)} for d in days]})
for habit in habits:
logs = logs_by_habit.get(habit.id, {})
pauses = pauses_by_habit.get(habit.id, [])
data = habit_dict(habit)
data["cells"] = [{"day": day, "scheduled": scheduled(habit, day), "paused": any(pause.start_date <= day <= pause.end_date for pause in pauses), "value": logs.get(day, 0)} for day in days]
data["stats"] = stats_by_habit.get(habit.id, {"total": 0, "completed_days": 0, "logged_days": 0})
output.append(data)
return {"days": days, "habits": output}