perf: batch import dedupe, scoped export links, calendar TTL cache
ci / docker (push) Successful in 3m17s

This commit is contained in:
2026-09-06 13:18:07 +08:00
parent 5924a80a39
commit fcaabce99f
+37 -4
View File
@@ -428,7 +428,16 @@ def _request_pinned_calendar_url(url: str):
connection.close()
_CAL_CACHE: dict[str, tuple[float, list[dict]]] = {}
_CAL_CACHE_TTL = 300.0
def fetch_calendar_events(url: str, source_name: str, color: str, window_start: datetime | None = None, window_end: datetime | None = None):
cache_key = f"{source_name}|{url}|{window_start.isoformat() if window_start else ''}|{window_end.isoformat() if window_end else ''}"
now = datetime.now(UTC).timestamp()
hit = _CAL_CACHE.get(cache_key)
if hit and now - hit[0] < _CAL_CACHE_TTL:
return [dict(item) for item in hit[1]]
current_url = url
for _ in range(4):
status, headers, content_type, body = _request_pinned_calendar_url(current_url)
@@ -444,7 +453,11 @@ def fetch_calendar_events(url: str, source_name: str, color: str, window_start:
raise HTTPException(422, "订阅链接没有返回 ICS 日历内容")
if len(body) > 2_000_000:
raise HTTPException(413, "日历订阅内容超过 2MB")
return parse_ics_events(body.decode("utf-8", errors="ignore"), source_name, color, window_start, window_end)
parsed = parse_ics_events(body.decode("utf-8", errors="ignore"), source_name, color, window_start, window_end)
if len(_CAL_CACHE) >= 64:
_CAL_CACHE.clear()
_CAL_CACHE[cache_key] = (now, parsed)
return [dict(item) for item in parsed]
raise HTTPException(502, "日历订阅重定向次数过多")
@@ -724,15 +737,25 @@ async def import_ticktick(file: UploadFile = File(...), user: User = Depends(cur
rows, errors = read_ticktick(await file.read())
if errors: raise HTTPException(422, errors)
inbox = await db.scalar(select(TaskList).where(TaskList.user_id == user.id, TaskList.is_inbox.is_(True)))
external_ids = [row["ID"].strip() for row in rows if row.get("ID", "").strip()]
existing_ids = set(
(
await db.scalars(
select(Task.external_id).where(Task.user_id == user.id, Task.external_id.in_(external_ids))
)
).all()
) if external_ids else set()
imported = skipped = 0
for raw in rows:
external_id = raw["ID"].strip()
if await db.scalar(select(Task.id).where(Task.user_id == user.id, Task.external_id == external_id)): skipped += 1; continue
if not external_id or external_id in existing_ids:
skipped += 1
continue
due = None
if raw.get("Due Date"):
try: due = datetime.combine(date.fromisoformat(raw["Due Date"][:10]), time.min, tzinfo=UTC)
except ValueError: raise HTTPException(422, f"无效日期: {raw['Due Date']}")
task = Task(user_id=user.id, list_id=inbox.id, title=raw["Title"].strip(), completed=raw.get("Status", "0").lower() in {"1", "completed", "true"}, due_at=due, external_id=external_id); db.add(task); imported += 1
task = Task(user_id=user.id, list_id=inbox.id, title=raw["Title"].strip(), completed=raw.get("Status", "0").lower() in {"1", "completed", "true"}, due_at=due, external_id=external_id); db.add(task); existing_ids.add(external_id); imported += 1
audit(db, user.id, "import", "task", count=imported); await db.commit(); return {"imported": imported, "skipped": skipped}
@@ -741,6 +764,16 @@ async def export_json(user: User = Depends(current_user), db: AsyncSession = Dep
def serialize(row, fields):
return {f: (str(v) if isinstance((v := getattr(row, f)), UUID) else v.isoformat() if isinstance(v, (date, datetime)) else v) for f in fields}
folders = list((await db.scalars(select(Folder).where(Folder.user_id == user.id))).all()); lists = list((await db.scalars(select(TaskList).where(TaskList.user_id == user.id))).all()); tags = list((await db.scalars(select(Tag).where(Tag.user_id == user.id))).all()); tasks = list((await db.scalars(select(Task).where(Task.user_id == user.id))).all()); habits = list((await db.scalars(select(Habit).where(Habit.user_id == user.id))).all())
task_tags = [
{"task_id": str(x.task_id), "tag_id": str(x.tag_id)}
for x in (
await db.scalars(
select(TaskTag)
.join(Task, Task.id == TaskTag.task_id)
.where(Task.user_id == user.id)
)
).all()
]
return {
"version": 1,
"exported_at": utcnow(),
@@ -748,7 +781,7 @@ async def export_json(user: User = Depends(current_user), db: AsyncSession = Dep
"lists": [serialize(x, ["id", "folder_id", "name", "is_inbox", "position", "deleted_at"]) for x in lists],
"tags": [serialize(x, ["id", "name", "color"]) for x in tags],
"tasks": [serialize(x, ["id", "list_id", "parent_id", "title", "description", "priority", "completed", "due_at", "external_id", "deleted_at"]) for x in tasks],
"task_tags": [{"task_id": str(x.task_id), "tag_id": str(x.tag_id)} for x in (await db.scalars(select(TaskTag))).all() if x.task_id in {task.id for task in tasks}],
"task_tags": task_tags,
"habits": [serialize(x, ["id", "name", "kind", "target", "max_value", "schedule_type", "weekdays", "month_days", "interval_days", "start_date", "archived_at"]) for x in habits],
}