from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from typing import Optional, List from app.database import get_db from app.models import Task, Tag from app.schemas import TaskCreate, TaskUpdate, TaskResponse from app.schemas.common import DeleteResponse from app.utils.crud import get_or_404 from app.utils.datetime import utcnow from app.utils.logger import logger router = APIRouter(prefix="/api/tasks", tags=["任务"]) @router.get("", response_model=List[TaskResponse]) def get_tasks( status: Optional[str] = Query(None, description="筛选状态: all/in_progress/completed"), category_id: Optional[int] = Query(None, description="分类ID"), priority: Optional[str] = Query(None, description="优先级: q1/q2/q3/q4"), sort_by: Optional[str] = Query("created_at", description="排序字段: created_at/priority/due_date"), sort_order: Optional[str] = Query("desc", description="排序方向: asc/desc"), db: Session = Depends(get_db) ) -> List[TaskResponse]: """获取任务列表(支持筛选和排序)""" try: query = db.query(Task) # 状态筛选 if status == "in_progress": query = query.filter(Task.is_completed == False) elif status == "completed": query = query.filter(Task.is_completed == True) # 分类筛选 if category_id is not None: query = query.filter(Task.category_id == category_id) # 优先级筛选 if priority: query = query.filter(Task.priority == priority) # 排序 if sort_by == "priority": order_col = Task.priority elif sort_by == "due_date": order_col = Task.due_date else: order_col = Task.created_at if sort_order == "asc": query = query.order_by(order_col.asc().nullslast()) else: query = query.order_by(order_col.desc().nullslast()) tasks = query.all() logger.info(f"获取任务列表成功,总数: {len(tasks)}") return tasks except Exception as e: logger.error(f"获取任务列表失败: {str(e)}") raise HTTPException(status_code=500, detail="获取任务列表失败") @router.post("", response_model=TaskResponse, status_code=201) def create_task(task_data: TaskCreate, db: Session = Depends(get_db)): """创建任务""" try: # 创建任务对象 db_task = Task( title=task_data.title, description=task_data.description, priority=task_data.priority, due_date=task_data.due_date, category_id=task_data.category_id, ) # 添加标签 if task_data.tag_ids: tags = db.query(Tag).filter(Tag.id.in_(task_data.tag_ids)).all() db_task.tags = tags db.add(db_task) db.commit() db.refresh(db_task) logger.info(f"创建任务成功: id={db_task.id}, title={db_task.title}") return db_task except Exception as e: db.rollback() logger.error(f"创建任务失败: {str(e)}") raise HTTPException(status_code=500, detail="创建任务失败") @router.get("/{task_id}", response_model=TaskResponse) def get_task(task_id: int, db: Session = Depends(get_db)): """获取单个任务""" try: task = get_or_404(db, Task, task_id, "任务") return task except HTTPException: raise except Exception as e: logger.error(f"获取任务失败: {str(e)}") raise HTTPException(status_code=500, detail="获取任务失败") @router.put("/{task_id}", response_model=TaskResponse) def update_task(task_id: int, task_data: TaskUpdate, db: Session = Depends(get_db)): """更新任务""" try: task = get_or_404(db, Task, task_id, "任务") # exclude_unset=True 保证:前端没传的字段不会出现在 dict 中,不会意外清空 # 前端显式传了 null 的字段会出现在 dict 中,允许清空可空字段 update_data = task_data.model_dump(exclude_unset=True) tag_ids = update_data.pop("tag_ids", None) for field, value in update_data.items(): # 非 clearable 字段(如 title)只有非 None 值才更新 # clearable 字段(description, due_date, category_id)允许设为 None if value is not None or field in task_data.clearable_fields: setattr(task, field, value) # 更新标签 if tag_ids is not None: tags = db.query(Tag).filter(Tag.id.in_(tag_ids)).all() task.tags = tags task.updated_at = utcnow() db.commit() db.refresh(task) logger.info(f"更新任务成功: id={task_id}") return task except HTTPException: raise except Exception as e: db.rollback() logger.error(f"更新任务失败: {str(e)}") raise HTTPException(status_code=500, detail="更新任务失败") @router.delete("/{task_id}") def delete_task(task_id: int, db: Session = Depends(get_db)): """删除任务""" try: task = get_or_404(db, Task, task_id, "任务") db.delete(task) db.commit() logger.info(f"删除任务成功: id={task_id}") return DeleteResponse(message="任务删除成功") except HTTPException: raise except Exception as e: db.rollback() logger.error(f"删除任务失败: {str(e)}") raise HTTPException(status_code=500, detail="删除任务失败") @router.patch("/{task_id}/toggle", response_model=TaskResponse) def toggle_task(task_id: int, db: Session = Depends(get_db)): """切换任务完成状态""" try: task = get_or_404(db, Task, task_id, "任务") task.is_completed = not task.is_completed task.updated_at = utcnow() db.commit() db.refresh(task) logger.info(f"切换任务状态成功: id={task_id}, is_completed={task.is_completed}") return task except HTTPException: raise except Exception as e: db.rollback() logger.error(f"切换任务状态失败: {str(e)}") raise HTTPException(status_code=500, detail="切换任务状态失败")