perf: batch import dedupe, scoped export links, calendar TTL cache
ci / docker (push) Successful in 3m17s
ci / docker (push) Successful in 3m17s
This commit is contained in:
+37
-4
@@ -428,7 +428,16 @@ def _request_pinned_calendar_url(url: str):
|
|||||||
connection.close()
|
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):
|
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
|
current_url = url
|
||||||
for _ in range(4):
|
for _ in range(4):
|
||||||
status, headers, content_type, body = _request_pinned_calendar_url(current_url)
|
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 日历内容")
|
raise HTTPException(422, "订阅链接没有返回 ICS 日历内容")
|
||||||
if len(body) > 2_000_000:
|
if len(body) > 2_000_000:
|
||||||
raise HTTPException(413, "日历订阅内容超过 2MB")
|
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, "日历订阅重定向次数过多")
|
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())
|
rows, errors = read_ticktick(await file.read())
|
||||||
if errors: raise HTTPException(422, errors)
|
if errors: raise HTTPException(422, errors)
|
||||||
inbox = await db.scalar(select(TaskList).where(TaskList.user_id == user.id, TaskList.is_inbox.is_(True)))
|
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
|
imported = skipped = 0
|
||||||
for raw in rows:
|
for raw in rows:
|
||||||
external_id = raw["ID"].strip()
|
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
|
due = None
|
||||||
if raw.get("Due Date"):
|
if raw.get("Due Date"):
|
||||||
try: due = datetime.combine(date.fromisoformat(raw["Due Date"][:10]), time.min, tzinfo=UTC)
|
try: due = datetime.combine(date.fromisoformat(raw["Due Date"][:10]), time.min, tzinfo=UTC)
|
||||||
except ValueError: raise HTTPException(422, f"无效日期: {raw['Due Date']}")
|
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}
|
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):
|
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}
|
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())
|
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 {
|
return {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"exported_at": utcnow(),
|
"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],
|
"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],
|
"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],
|
"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],
|
"habits": [serialize(x, ["id", "name", "kind", "target", "max_value", "schedule_type", "weekdays", "month_days", "interval_days", "start_date", "archived_at"]) for x in habits],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user