Files
ToDoList/api/app/schemas/task.py
祀梦 0ab719500b feat: add WebDAV sync support and startup/shutdown scripts
Backend:
- Add uuid, sync_version, is_deleted fields to all syncable models
- Add SyncSettings model for WebDAV configuration (AES-256-GCM encrypted passwords)
- Add crypto.py: AES-256-GCM encryption derived from JWT_SECRET via PBKDF2
- Add sync_lock.py: thread-level sync lock with 503 middleware for write blocking
- Add webdav.py: WebDAV client using requests (PUT/GET/MKCOL/DELETE)
- Add sync_service.py: push/pull/bidirectional merge with LWW conflict resolution
- Add sync router with 8 endpoints: config, test, push, pull, sync, status, remote delete
- Add UUID backfill for existing records in init_db()
- Add SQLAlchemy before_update event to auto-increment sync_version
- Register sync middleware to block writes during sync (503)

Frontend:
- Add sync API client (WebUI/src/api/sync.ts)
- Add useSyncStore with config, test, push/pull/sync operations
- Add WebDAV config + sync UI in SettingsView
- Add 503 status code handling in axios interceptor
- Add uuid field to all TypeScript type definitions

Scripts:
- Add scripts/start.bat and scripts/stop.bat for project management

Design doc: docs/plan/webdav-sync-design.md
2026-05-17 21:18:54 +08:00

87 lines
2.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from pydantic import BaseModel, Field, field_validator
from datetime import datetime
from typing import Optional, List
from app.schemas.category import CategoryResponse
from app.schemas.tag import TagResponse
def parse_datetime(value):
"""解析日期时间字符串"""
if value is None or value == '':
return None
if isinstance(value, datetime):
return value
# 尝试多种格式
formats = [
'%Y-%m-%dT%H:%M:%S',
'%Y-%m-%d %H:%M:%S',
'%Y-%m-%dT%H:%M:%S.%f',
]
for fmt in formats:
try:
return datetime.strptime(value, fmt)
except ValueError:
continue
# 最后尝试 ISO 格式
return datetime.fromisoformat(value.replace('Z', '+00:00'))
class TaskBase(BaseModel):
"""任务基础模型"""
title: str = Field(..., max_length=200)
description: Optional[str] = None
priority: str = Field(default="q4", pattern="^(q1|q2|q3|q4)$")
due_date: Optional[datetime] = None
category_id: Optional[int] = None
@field_validator('due_date', mode='before')
@classmethod
def parse_due_date(cls, v):
return parse_datetime(v)
class TaskCreate(TaskBase):
"""创建任务请求模型"""
tag_ids: Optional[List[int]] = []
class TaskUpdate(BaseModel):
"""更新任务请求模型
通过 exclude_unset=True 区分"前端没传""前端传了 null"
- 前端没传某个字段 -> model_dump 结果中不包含该 key -> 不修改
- 前端传了 null -> model_dump 结果中包含 key: None -> 视为"清空"
"""
title: Optional[str] = Field(None, max_length=200)
description: Optional[str] = None
priority: Optional[str] = Field(None, pattern="^(q1|q2|q3|q4)$")
due_date: Optional[datetime] = None
is_completed: Optional[bool] = None
category_id: Optional[int] = None
tag_ids: Optional[List[int]] = None
@field_validator('due_date', mode='before')
@classmethod
def parse_due_date(cls, v):
return parse_datetime(v)
@property
def clearable_fields(self) -> set:
"""允许被显式清空(设为 None的字段集合"""
return {'description', 'due_date', 'category_id'}
class TaskResponse(TaskBase):
"""任务响应模型"""
id: int
uuid: Optional[str] = None
is_completed: bool
created_at: datetime
updated_at: datetime
category: Optional[CategoryResponse] = None
tags: List[TagResponse] = []
class Config:
from_attributes = True