refactor(todo): 移除 due_at 字段,改用 order 字段管理象限内顺序

This commit is contained in:
qzl
2026-03-20 11:09:38 +08:00
parent d574128815
commit fbf15bc937
22 changed files with 1458 additions and 1524 deletions
+10 -8
View File
@@ -26,8 +26,8 @@ class TodoRepository(Protocol):
owner_id: UUID,
title: str,
description: str | None = None,
due_at: datetime | None = None,
priority: int = TodoPriority.IMPORTANT_URGENT,
order: int = 0,
created_by: UUID | None = None,
) -> Todo:
"""Create a new todo."""
@@ -42,8 +42,8 @@ class TodoRepository(Protocol):
todo: Todo,
title: str | None = None,
description: str | None = None,
due_at: datetime | None = None,
priority: int | None = None,
order: int | None = None,
status: TodoStatus | None = None,
completed_at: datetime | None = None,
) -> Todo:
@@ -79,6 +79,8 @@ class SQLAlchemyTodoRepository(BaseRepository[Todo]):
- No HTTP exceptions - returns None or raises SQLAlchemyError
"""
_session: AsyncSession
def __init__(self, session: AsyncSession) -> None:
super().__init__(session, Todo)
self._session = session
@@ -88,8 +90,8 @@ class SQLAlchemyTodoRepository(BaseRepository[Todo]):
owner_id: UUID,
title: str,
description: str | None = None,
due_at: datetime | None = None,
priority: int = TodoPriority.IMPORTANT_URGENT,
order: int = 0,
created_by: UUID | None = None,
) -> Todo:
try:
@@ -97,8 +99,8 @@ class SQLAlchemyTodoRepository(BaseRepository[Todo]):
owner_id=owner_id,
title=title,
description=description,
due_at=due_at,
priority=priority,
order=order,
status=TodoStatus.PENDING,
created_by=created_by,
)
@@ -128,8 +130,8 @@ class SQLAlchemyTodoRepository(BaseRepository[Todo]):
todo: Todo,
title: str | None = None,
description: str | None = None,
due_at: datetime | None = None,
priority: int | None = None,
order: int | None = None,
status: TodoStatus | None = None,
completed_at: datetime | None = None,
) -> Todo:
@@ -138,10 +140,10 @@ class SQLAlchemyTodoRepository(BaseRepository[Todo]):
todo.title = title
if description is not None:
todo.description = description
if due_at is not None:
todo.due_at = due_at
if priority is not None:
todo.priority = priority
if order is not None:
todo.order = order
if status is not None:
todo.status = status
if completed_at is not None:
@@ -167,7 +169,7 @@ class SQLAlchemyTodoRepository(BaseRepository[Todo]):
select(Todo)
.where(Todo.owner_id == owner_id)
.where(Todo.deleted_at.is_(None))
.order_by(Todo.priority.asc(), Todo.due_at.asc().nullslast())
.order_by(Todo.priority.asc(), Todo.order.asc(), Todo.created_at.asc())
)
if status is not None:
+18 -1
View File
@@ -6,7 +6,13 @@ from uuid import UUID
from fastapi import APIRouter, Depends, Query, status
from v1.todo.dependencies import get_todo_service
from v1.todo.schemas import TodoComplete, TodoCreate, TodoResponse, TodoUpdate
from v1.todo.schemas import (
TodoComplete,
TodoCreate,
TodoReorderRequest,
TodoResponse,
TodoUpdate,
)
from v1.todo.service import TodoService
@@ -42,6 +48,17 @@ async def get_todo(
return await service.get_by_id(todo_id)
@router.patch(
"/reorder",
status_code=status.HTTP_204_NO_CONTENT,
)
async def reorder_todos(
payload: TodoReorderRequest,
service: Annotated[TodoService, Depends(get_todo_service)],
) -> None:
await service.reorder(payload)
@router.patch("/{todo_id}", response_model=TodoResponse)
async def update_todo(
todo_id: UUID,
+19 -3
View File
@@ -5,14 +5,16 @@ from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
from schemas.todo import TodoOrder
class TodoCreate(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
title: str = Field(..., min_length=1, max_length=255)
description: str | None = Field(None, max_length=1000)
due_at: datetime | None = None
priority: int = Field(1, ge=1, le=4)
order: TodoOrder | None = None
schedule_item_ids: list[UUID] = Field(default_factory=list)
@@ -21,8 +23,8 @@ class TodoUpdate(BaseModel):
title: str | None = Field(None, min_length=1, max_length=255)
description: str | None = Field(None, max_length=1000)
due_at: datetime | None = None
priority: int | None = Field(None, ge=1, le=4)
order: TodoOrder | None = None
status: Literal["pending", "done", "canceled"] | None = None
schedule_item_ids: list[UUID] | None = None
@@ -43,8 +45,8 @@ class TodoResponse(BaseModel):
owner_id: UUID
title: str
description: str | None
due_at: datetime | None
priority: int
order: TodoOrder
status: str
completed_at: datetime | None
created_at: datetime
@@ -52,6 +54,20 @@ class TodoResponse(BaseModel):
schedule_items: list[ScheduleItemBasic] = Field(default_factory=list)
class TodoReorderItem(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
id: UUID
priority: int = Field(..., ge=1, le=4)
order: TodoOrder
class TodoReorderRequest(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
items: list[TodoReorderItem] = Field(..., min_length=1)
class TodoComplete(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
+66 -5
View File
@@ -13,7 +13,13 @@ from core.logging import get_logger
from models.todos import Todo, TodoStatus
from v1.schedule_items.repository import SQLAlchemyScheduleItemRepository
from v1.todo.repository import TodoRepository
from v1.todo.schemas import ScheduleItemBasic, TodoCreate, TodoResponse, TodoUpdate
from v1.todo.schemas import (
ScheduleItemBasic,
TodoCreate,
TodoReorderRequest,
TodoResponse,
TodoUpdate,
)
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
@@ -52,12 +58,20 @@ class TodoService(BaseService):
user_id = self.require_user_id()
try:
order_value = request.order
if order_value is None:
todos_in_priority = await self._repository.list_by_owner(
owner_id=user_id,
priority=request.priority,
)
order_value = len(todos_in_priority)
todo = await self._repository.create(
owner_id=user_id,
title=request.title,
description=request.description,
due_at=request.due_at,
priority=request.priority,
order=order_value,
created_by=user_id,
)
@@ -144,8 +158,8 @@ class TodoService(BaseService):
todo,
title=request.title,
description=request.description,
due_at=request.due_at,
priority=request.priority,
order=request.order,
status=status_enum,
completed_at=completed_at,
)
@@ -253,6 +267,53 @@ class TodoService(BaseService):
},
)
async def reorder(self, request: TodoReorderRequest) -> None:
user_id = self.require_user_id()
seen_ids: set[UUID] = set()
original_priorities: set[int] = set()
target_priorities: set[int] = set()
try:
for item in request.items:
if item.id in seen_ids:
raise HTTPException(status_code=400, detail="Duplicate todo id")
seen_ids.add(item.id)
todo = await self._repository.get_by_id(item.id)
if todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
if todo.owner_id != user_id:
raise HTTPException(
status_code=403, detail="Not authorized to reorder this todo"
)
original_priorities.add(todo.priority)
target_priorities.add(item.priority)
await self._repository.update(
todo,
priority=item.priority,
order=item.order,
)
affected_priorities = original_priorities.union(target_priorities)
for priority in affected_priorities:
todos = await self._repository.list_by_owner(
owner_id=user_id,
status=TodoStatus.PENDING,
priority=priority,
)
todos.sort(key=lambda current: (current.order, current.created_at))
for index, todo in enumerate(todos):
if todo.order != index:
await self._repository.update(todo, order=index)
await self._session.commit()
except SQLAlchemyError:
await self._session.rollback()
raise HTTPException(status_code=503, detail="Todo service unavailable")
async def list_todos(
self,
status: str | None = None,
@@ -287,7 +348,7 @@ class TodoService(BaseService):
)
schedule_item_ids = await self._repository.get_schedule_items(todo.id)
schedule_items = []
schedule_items: list[ScheduleItemBasic] = []
for item_id in schedule_item_ids:
item = await self._schedule_item_repository.get_by_id(item_id)
if item:
@@ -305,8 +366,8 @@ class TodoService(BaseService):
owner_id=todo.owner_id,
title=todo.title,
description=todo.description,
due_at=todo.due_at,
priority=todo.priority,
order=todo.order,
status=status_value,
completed_at=todo.completed_at,
created_at=todo.created_at,