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:
@@ -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)):
|
||||
"""更新阶段/里程碑"""
|
||||
|
||||
Reference in New Issue
Block a user