feat: add data backup/import, goal step ordering, and PostgreSQL migration

- Add GET /api/backup/export and POST /api/backup/import endpoints for full data backup
- Add drag-and-drop reorder for goal steps with PUT /api/goals/{id}/steps/reorder
- Auto-assign sort_order on step creation (preserves creation order)
- Fix duplicate milestone rendering in goal detail page
- Add category management button in goal dialog
- Migrate database default from SQLite to PostgreSQL
- Fix router guard redirect loop for logged-in users on setup/login pages
- Fix ALTER TABLE ADD COLUMN crash on callable defaults (uuid.uuid4)
- Add auth status rate limiter and token version caching
- Update CLAUDE.md to reflect current architecture

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
祀梦
2026-05-18 00:02:18 +08:00
parent 0ab719500b
commit 5048de4fa1
21 changed files with 543 additions and 225 deletions

View File

@@ -9,6 +9,7 @@ from app.schemas.goal import (
GoalCreate, GoalUpdate, GoalListResponse, GoalDetailResponse, GoalStatusUpdate,
GoalStepCreate, GoalStepUpdate, GoalStepResponse,
GoalReviewCreate, GoalReviewResponse,
ReorderRequest,
)
from app.schemas.common import DeleteResponse
from app.utils.crud import get_or_404
@@ -35,10 +36,10 @@ def recalc_progress(db: Session, goal_id: int):
def build_step_tree(steps: list[GoalStep]) -> list[dict]:
"""将扁平的 step 列表转为树形结构phase 包含子 milestone"""
"""将扁平的 step 列表转为树形结构phase 包含子 milestone,按 sort_order 排序"""
step_map = {}
roots = []
for s in steps:
for s in sorted(steps, key=lambda x: (x.sort_order or 0)):
step_map[s.id] = {
"id": s.id,
"goal_id": s.goal_id,
@@ -52,7 +53,7 @@ def build_step_tree(steps: list[GoalStep]) -> list[dict]:
"created_at": s.created_at,
"children": [],
}
for s in steps:
for s in sorted(steps, key=lambda x: (x.sort_order or 0)):
node = step_map[s.id]
if s.parent_id and s.parent_id in step_map:
step_map[s.parent_id]["children"].append(node)
@@ -206,7 +207,13 @@ def create_step(goal_id: int, data: GoalStepCreate, db: Session = Depends(get_db
"""添加阶段/里程碑"""
try:
get_or_404(db, Goal, goal_id, "目标")
step = GoalStep(goal_id=goal_id, **data.model_dump())
# 自动分配 sort_order同类步骤中取最大值 + 1
max_sort = db.query(GoalStep).filter(
GoalStep.goal_id == goal_id,
GoalStep.step_type == data.step_type,
).order_by(GoalStep.sort_order.desc()).first()
next_sort = (max_sort.sort_order + 1) if max_sort and max_sort.sort_order is not None else 0
step = GoalStep(goal_id=goal_id, sort_order=next_sort, **data.model_dump())
db.add(step)
db.commit()
db.refresh(step)
@@ -228,6 +235,31 @@ def create_step(goal_id: int, data: GoalStepCreate, db: Session = Depends(get_db
raise HTTPException(status_code=500, detail="添加步骤失败")
# ============ Reorder ============
@router.put("/{goal_id}/steps/reorder")
def reorder_steps(goal_id: int, data: ReorderRequest, db: Session = Depends(get_db)):
"""批量更新步骤排序"""
try:
get_or_404(db, Goal, goal_id, "目标")
for item in data.items:
step = db.query(GoalStep).filter(
GoalStep.id == item.id,
GoalStep.goal_id == goal_id,
).first()
if step:
step.sort_order = item.sort_order
db.commit()
logger.info(f"步骤排序更新成功: goal_id={goal_id}, count={len(data.items)}")
return {"message": "排序更新成功"}
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f"更新步骤排序失败: {str(e)}")
raise HTTPException(status_code=500, detail="更新排序失败")
@router.put("/{goal_id}/steps/{step_id}", response_model=GoalStepResponse)
def update_step(goal_id: int, step_id: int, data: GoalStepUpdate, db: Session = Depends(get_db)):
"""更新阶段/里程碑"""