release: Elysia ToDo v1.0.0

鍏ㄦ爤涓汉淇℃伅绠$悊搴旂敤锛岄泦鎴愬緟鍔炰换鍔°€佷範鎯墦鍗°€佺邯蹇垫棩鎻愰啋銆佽祫浜ф€昏鍔熻兘銆

Made-with: Cursor
This commit is contained in:
祀梦
2026-03-14 22:21:26 +08:00
commit 2979197b1c
104 changed files with 21737 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
from app.schemas.task import (
TaskBase,
TaskCreate,
TaskUpdate,
TaskResponse,
)
from app.schemas.category import (
CategoryBase,
CategoryCreate,
CategoryUpdate,
CategoryResponse,
)
from app.schemas.tag import (
TagBase,
TagCreate,
TagResponse,
)
from app.schemas.common import (
DeleteResponse,
PaginatedResponse,
)
from app.schemas.user_settings import (
UserSettingsUpdate,
UserSettingsResponse,
)
from app.schemas.habit import (
HabitGroupCreate,
HabitGroupUpdate,
HabitGroupResponse,
HabitCreate,
HabitUpdate,
HabitResponse,
CheckinCreate,
CheckinResponse,
HabitStatsResponse,
)
from app.schemas.anniversary import (
AnniversaryCategoryCreate,
AnniversaryCategoryUpdate,
AnniversaryCategoryResponse,
AnniversaryCreate,
AnniversaryUpdate,
AnniversaryResponse,
)

133
api/app/schemas/account.py Normal file
View File

@@ -0,0 +1,133 @@
from pydantic import BaseModel, Field
from datetime import date, datetime
from typing import Optional, List
# ============ 财务账户 Schema ============
class AccountBase(BaseModel):
"""账户基础模型"""
name: str = Field(..., min_length=1, max_length=100)
account_type: str = Field(default="savings", pattern="^(savings|debt)$")
balance: float = Field(default=0.0)
icon: str = Field(default="wallet", max_length=50)
color: str = Field(default="#FFB7C5", max_length=20)
sort_order: int = Field(default=0)
is_active: bool = Field(default=True)
description: Optional[str] = None
class AccountCreate(AccountBase):
"""创建账户请求模型"""
pass
class AccountUpdate(BaseModel):
"""更新账户请求模型"""
name: Optional[str] = Field(None, max_length=100)
account_type: Optional[str] = Field(None, pattern="^(savings|debt)$")
balance: Optional[float] = None
icon: Optional[str] = Field(None, max_length=50)
color: Optional[str] = Field(None, max_length=20)
sort_order: Optional[int] = None
is_active: Optional[bool] = None
description: Optional[str] = None
class AccountResponse(AccountBase):
"""账户响应模型"""
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class AccountListItemResponse(BaseModel):
"""账户列表项响应模型(含分期摘要)"""
id: int
name: str
account_type: str
balance: float
icon: str
color: str
sort_order: int
is_active: bool
description: Optional[str] = None
created_at: datetime
updated_at: datetime
installments: List[dict] = []
class BalanceUpdateRequest(BaseModel):
"""更新余额请求模型"""
new_balance: float
note: Optional[str] = Field(None, max_length=200)
# ============ 账户变更历史 Schema ============
class AccountHistoryResponse(BaseModel):
"""变更历史响应模型"""
id: int
account_id: int
change_amount: float
balance_before: float
balance_after: float
note: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
# ============ 分期还款计划 Schema ============
class DebtInstallmentBase(BaseModel):
"""分期计划基础模型"""
account_id: int
total_amount: float
total_periods: int = Field(..., ge=1)
current_period: int = Field(default=1, ge=1)
payment_day: int = Field(..., ge=1, le=31)
payment_amount: float = Field(..., gt=0)
start_date: date
is_completed: bool = Field(default=False)
class DebtInstallmentCreate(DebtInstallmentBase):
"""创建分期计划请求模型"""
pass
class DebtInstallmentUpdate(BaseModel):
"""更新分期计划请求模型"""
account_id: Optional[int] = None
total_amount: Optional[float] = None
total_periods: Optional[int] = Field(None, ge=1)
current_period: Optional[int] = Field(None, ge=1)
payment_day: Optional[int] = Field(None, ge=1, le=31)
payment_amount: Optional[float] = Field(None, gt=0)
start_date: Optional[date] = None
is_completed: Optional[bool] = None
class DebtInstallmentResponse(DebtInstallmentBase):
"""分期计划响应模型(含计算字段)"""
id: int
created_at: datetime
updated_at: datetime
# 计算字段
next_payment_date: Optional[date] = None
days_until_payment: Optional[int] = None
remaining_periods: Optional[int] = None
# 关联账户信息
account_name: Optional[str] = None
account_icon: Optional[str] = None
account_color: Optional[str] = None
class Config:
from_attributes = True

View File

@@ -0,0 +1,122 @@
from pydantic import BaseModel, Field, field_validator
from datetime import date, datetime, timezone
from typing import Optional, List
def parse_date(value):
"""解析日期字符串"""
if value is None or value == '':
return None
if isinstance(value, date):
return value
if isinstance(value, datetime):
return value.date()
formats = [
'%Y-%m-%d',
'%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).date()
except ValueError:
continue
try:
return date.fromisoformat(value)
except ValueError:
raise ValueError(f"无法解析日期: {value}")
# ============ 纪念日分类 Schema ============
class AnniversaryCategoryBase(BaseModel):
"""纪念日分类基础模型"""
name: str = Field(..., max_length=50)
icon: str = Field(default="calendar", max_length=50)
color: str = Field(default="#FFB7C5", max_length=20)
sort_order: int = Field(default=0)
class AnniversaryCategoryCreate(AnniversaryCategoryBase):
"""创建纪念日分类请求模型"""
pass
class AnniversaryCategoryUpdate(BaseModel):
"""更新纪念日分类请求模型"""
name: Optional[str] = Field(None, max_length=50)
icon: Optional[str] = Field(None, max_length=50)
color: Optional[str] = Field(None, max_length=20)
sort_order: Optional[int] = None
class AnniversaryCategoryResponse(AnniversaryCategoryBase):
"""纪念日分类响应模型"""
id: int
class Config:
from_attributes = True
# ============ 纪念日 Schema ============
class AnniversaryBase(BaseModel):
"""纪念日基础模型"""
title: str = Field(..., max_length=200)
date: date
year: Optional[int] = None
category_id: Optional[int] = None
description: Optional[str] = None
is_recurring: bool = Field(default=True)
remind_days_before: int = Field(default=3)
@field_validator('date', mode='before')
@classmethod
def parse_anniversary_date(cls, v):
result = parse_date(v)
if result is None:
raise ValueError("纪念日日期不能为空")
return result
class AnniversaryCreate(AnniversaryBase):
"""创建纪念日请求模型"""
pass
class AnniversaryUpdate(BaseModel):
"""更新纪念日请求模型"""
title: Optional[str] = Field(None, max_length=200)
date: Optional[date] = None
year: Optional[int] = None
category_id: Optional[int] = None
description: Optional[str] = None
is_recurring: Optional[bool] = None
remind_days_before: Optional[int] = None
@field_validator('date', mode='before')
@classmethod
def parse_anniversary_date(cls, v):
if v is None:
return None
return parse_date(v)
@property
def clearable_fields(self) -> set:
"""允许被显式清空(设为 None的字段集合"""
return {'description', 'category_id', 'year'}
class AnniversaryResponse(AnniversaryBase):
"""纪念日响应模型"""
id: int
created_at: datetime
updated_at: datetime
category: Optional[AnniversaryCategoryResponse] = None
next_date: Optional[date] = None
days_until: Optional[int] = None
year_count: Optional[int] = None
class Config:
from_attributes = True

View File

@@ -0,0 +1,28 @@
from pydantic import BaseModel, Field
class CategoryBase(BaseModel):
"""分类基础模型"""
name: str = Field(..., max_length=100)
color: str = Field(default="#FFB7C5", max_length=20)
icon: str = Field(default="folder", max_length=50)
class CategoryCreate(CategoryBase):
"""创建分类请求模型"""
pass
class CategoryUpdate(BaseModel):
"""更新分类请求模型"""
name: str = Field(None, max_length=100)
color: str = Field(None, max_length=20)
icon: str = Field(None, max_length=50)
class CategoryResponse(CategoryBase):
"""分类响应模型"""
id: int
class Config:
from_attributes = True

39
api/app/schemas/common.py Normal file
View File

@@ -0,0 +1,39 @@
"""
通用响应模型
"""
from typing import Generic, TypeVar, List, Optional
from pydantic import BaseModel, Field
T = TypeVar("T")
class DeleteResponse(BaseModel):
"""删除成功响应"""
success: bool = Field(default=True, description="操作是否成功")
message: str = Field(description="响应消息")
class Config:
json_schema_extra = {
"example": {
"success": True,
"message": "删除成功"
}
}
class PaginatedResponse(BaseModel, Generic[T]):
"""分页列表响应"""
items: List[T] = Field(description="数据列表")
total: int = Field(description="总记录数")
skip: int = Field(description="跳过的记录数")
limit: int = Field(description="返回的记录数")
class Config:
json_schema_extra = {
"example": {
"items": [],
"total": 0,
"skip": 0,
"limit": 20
}
}

105
api/app/schemas/habit.py Normal file
View File

@@ -0,0 +1,105 @@
from pydantic import BaseModel, Field, field_validator
from datetime import datetime, date
from typing import Optional, List
# ============ 习惯分组 Schemas ============
class HabitGroupBase(BaseModel):
"""习惯分组基础模型"""
name: str = Field(..., max_length=100)
color: str = Field(default="#FFB7C5", max_length=20)
icon: str = Field(default="flag", max_length=50)
sort_order: int = Field(default=0)
class HabitGroupCreate(HabitGroupBase):
"""创建习惯分组请求模型"""
pass
class HabitGroupUpdate(BaseModel):
"""更新习惯分组请求模型"""
name: Optional[str] = Field(None, max_length=100)
color: Optional[str] = Field(None, max_length=20)
icon: Optional[str] = Field(None, max_length=50)
sort_order: Optional[int] = None
class HabitGroupResponse(HabitGroupBase):
"""习惯分组响应模型"""
id: int
class Config:
from_attributes = True
# ============ 习惯 Schemas ============
class HabitBase(BaseModel):
"""习惯基础模型"""
name: str = Field(..., max_length=200)
description: Optional[str] = None
group_id: Optional[int] = None
target_count: int = Field(default=1, ge=1)
frequency: str = Field(default="daily", pattern="^(daily|weekly)$")
active_days: Optional[str] = None
class HabitCreate(HabitBase):
"""创建习惯请求模型"""
pass
class HabitUpdate(BaseModel):
"""更新习惯请求模型"""
name: Optional[str] = Field(None, max_length=200)
description: Optional[str] = None
group_id: Optional[int] = None
target_count: Optional[int] = Field(None, ge=1)
frequency: Optional[str] = Field(None, pattern="^(daily|weekly)$")
active_days: Optional[str] = None
@property
def clearable_fields(self) -> set:
return {"description", "group_id", "active_days"}
class HabitResponse(HabitBase):
"""习惯响应模型"""
id: int
is_archived: bool
created_at: datetime
updated_at: datetime
group: Optional[HabitGroupResponse] = None
class Config:
from_attributes = True
# ============ 打卡 Schemas ============
class CheckinCreate(BaseModel):
"""打卡请求模型"""
count: Optional[int] = Field(default=1, ge=1)
class CheckinResponse(BaseModel):
"""打卡记录响应模型"""
id: int
habit_id: int
checkin_date: date
count: int
created_at: datetime
class Config:
from_attributes = True
class HabitStatsResponse(BaseModel):
"""习惯统计响应模型"""
total_days: int = 0
current_streak: int = 0
longest_streak: int = 0
today_count: int = 0
today_completed: bool = False

19
api/app/schemas/tag.py Normal file
View File

@@ -0,0 +1,19 @@
from pydantic import BaseModel, Field
class TagBase(BaseModel):
"""标签基础模型"""
name: str = Field(..., max_length=50)
class TagCreate(TagBase):
"""创建标签请求模型"""
pass
class TagResponse(TagBase):
"""标签响应模型"""
id: int
class Config:
from_attributes = True

85
api/app/schemas/task.py Normal file
View File

@@ -0,0 +1,85 @@
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
is_completed: bool
created_at: datetime
updated_at: datetime
category: Optional[CategoryResponse] = None
tags: List[TagResponse] = []
class Config:
from_attributes = True

View File

@@ -0,0 +1,39 @@
from pydantic import BaseModel, Field
from datetime import datetime, date
from typing import Optional
class UserSettingsUpdate(BaseModel):
"""更新用户设置请求模型"""
nickname: Optional[str] = Field(None, max_length=50)
avatar: Optional[str] = None
signature: Optional[str] = Field(None, max_length=200)
birthday: Optional[date] = None
email: Optional[str] = Field(None, max_length=100)
site_name: Optional[str] = Field(None, max_length=50)
theme: Optional[str] = Field(None, max_length=20)
language: Optional[str] = Field(None, max_length=10)
default_view: Optional[str] = Field(None, max_length=20)
default_sort_by: Optional[str] = Field(None, max_length=20)
default_sort_order: Optional[str] = Field(None, max_length=10)
class UserSettingsResponse(BaseModel):
"""用户设置响应模型"""
id: int
nickname: str
avatar: Optional[str] = None
signature: Optional[str] = None
birthday: Optional[date] = None
email: Optional[str] = None
site_name: str
theme: str
language: str
default_view: str
default_sort_by: str
default_sort_order: str
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True