release: Elysia ToDo v1.0.0
鍏ㄦ爤涓汉淇℃伅绠$悊搴旂敤锛岄泦鎴愬緟鍔炰换鍔°€佷範鎯墦鍗°€佺邯蹇垫棩鎻愰啋銆佽祫浜ф€昏鍔熻兘銆 Made-with: Cursor
This commit is contained in:
44
api/app/schemas/__init__.py
Normal file
44
api/app/schemas/__init__.py
Normal 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
133
api/app/schemas/account.py
Normal 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
|
||||
122
api/app/schemas/anniversary.py
Normal file
122
api/app/schemas/anniversary.py
Normal 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
|
||||
28
api/app/schemas/category.py
Normal file
28
api/app/schemas/category.py
Normal 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
39
api/app/schemas/common.py
Normal 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
105
api/app/schemas/habit.py
Normal 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
19
api/app/schemas/tag.py
Normal 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
85
api/app/schemas/task.py
Normal 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
|
||||
39
api/app/schemas/user_settings.py
Normal file
39
api/app/schemas/user_settings.py
Normal 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
|
||||
Reference in New Issue
Block a user