feat: refine archived habit management
This commit is contained in:
+58
-1
@@ -702,8 +702,24 @@ async def require_positive_log_day(db, habit, day):
|
||||
raise HTTPException(409, "暂停日不可记录正向进度")
|
||||
|
||||
|
||||
async def lock_habit_order(db: AsyncSession, user_id: UUID) -> None:
|
||||
"""Serialize active-habit order changes for one user.
|
||||
|
||||
PostgreSQL supports a row-level user lock. SQLite ignores ``FOR UPDATE``, so
|
||||
a no-op write acquires its transaction-wide write lock before order reads.
|
||||
"""
|
||||
connection = await db.connection()
|
||||
if connection.dialect.name == "sqlite":
|
||||
await db.execute(
|
||||
update(User).where(User.id == user_id).values(id=User.id)
|
||||
)
|
||||
return
|
||||
await db.scalar(select(User.id).where(User.id == user_id).with_for_update())
|
||||
|
||||
|
||||
@router.post("/habits", status_code=201)
|
||||
async def create_habit(payload: HabitCreate, user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
|
||||
await lock_habit_order(db, user.id)
|
||||
max_position = await db.scalar(select(func.max(Habit.position)).where(Habit.user_id == user.id))
|
||||
row = Habit(user_id=user.id, position=(max_position if max_position is not None else -1) + 1, **payload.model_dump(exclude={"weekdays", "month_days"}), weekdays=",".join(map(str, payload.weekdays)) if payload.weekdays else None, month_days=",".join(map(str, payload.month_days)) if payload.month_days else None)
|
||||
db.add(row); await db.commit(); await db.refresh(row); return habit_dict(row)
|
||||
@@ -712,11 +728,24 @@ async def create_habit(payload: HabitCreate, user: User = Depends(current_user),
|
||||
@router.get("/habits")
|
||||
async def list_habits(archived: bool = False, user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
|
||||
condition = Habit.archived_at.is_not(None) if archived else Habit.archived_at.is_(None)
|
||||
return [habit_dict(h) for h in (await db.scalars(select(Habit).where(Habit.user_id == user.id, condition).order_by(Habit.position, Habit.created_at))).all()]
|
||||
order = (
|
||||
(Habit.archived_at.desc(), Habit.created_at.desc(), Habit.id.asc())
|
||||
if archived
|
||||
else (Habit.position, Habit.created_at)
|
||||
)
|
||||
return [
|
||||
habit_dict(h)
|
||||
for h in (
|
||||
await db.scalars(
|
||||
select(Habit).where(Habit.user_id == user.id, condition).order_by(*order)
|
||||
)
|
||||
).all()
|
||||
]
|
||||
|
||||
|
||||
@router.put("/habits/reorder", status_code=204)
|
||||
async def reorder_habits(payload: HabitReorder, user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
|
||||
await lock_habit_order(db, user.id)
|
||||
rows = list((await db.scalars(select(Habit).where(
|
||||
Habit.id.in_(payload.habit_ids), Habit.user_id == user.id, Habit.archived_at.is_(None)
|
||||
))).all())
|
||||
@@ -759,6 +788,34 @@ async def archive_habit(habit_id: UUID, user: User = Depends(current_user), db:
|
||||
row = await owned_habit(db, user.id, habit_id); row.archived_at = utcnow(); await db.commit(); return Response(status_code=204)
|
||||
|
||||
|
||||
@router.post("/habits/{habit_id}/restore")
|
||||
async def restore_habit(habit_id: UUID, user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
|
||||
await lock_habit_order(db, user.id)
|
||||
row = await owned_habit(db, user.id, habit_id)
|
||||
if row.archived_at is None:
|
||||
raise HTTPException(409, "习惯未归档")
|
||||
max_position = await db.scalar(select(func.max(Habit.position)).where(
|
||||
Habit.user_id == user.id, Habit.archived_at.is_(None)
|
||||
))
|
||||
position = (max_position if max_position is not None else -1) + 1
|
||||
result = await db.execute(
|
||||
update(Habit)
|
||||
.where(
|
||||
Habit.id == habit_id,
|
||||
Habit.user_id == user.id,
|
||||
Habit.archived_at.is_not(None),
|
||||
)
|
||||
.values(archived_at=None, position=position)
|
||||
)
|
||||
if result.rowcount != 1:
|
||||
await db.rollback()
|
||||
raise HTTPException(409, "习惯未归档")
|
||||
audit(db, user.id, "restore", "habit", row.id)
|
||||
await db.commit()
|
||||
await db.refresh(row)
|
||||
return habit_dict(row)
|
||||
|
||||
|
||||
@router.delete("/habits/{habit_id}/permanent", status_code=204)
|
||||
async def delete_habit_permanently(habit_id: UUID, user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
|
||||
row = await owned_habit(db, user.id, habit_id)
|
||||
|
||||
Reference in New Issue
Block a user