Normalize uploaded documents into structured markdown, add clearer parser errors for missing dependencies, and cover the ingestion flow with backend tests. This also replaces deprecated UTC timestamp helpers in the touched backend paths so the knowledge pipeline stays warning-free. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
155 lines
5.2 KiB
Python
155 lines
5.2 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, func
|
||
from datetime import date
|
||
from app.database import get_db
|
||
from app.models.todo import DailyTodo, TodoSource
|
||
from app.models.user import User
|
||
from app.routers.auth import get_current_user
|
||
from app.schemas.todo import (
|
||
TodoCreate, TodoUpdate, TodoOut, TodoListOut, TodoSummaryOut
|
||
)
|
||
from app.services.todo_service import generate_daily_todos
|
||
|
||
router = APIRouter(prefix="/api/todos", tags=["待办"])
|
||
|
||
|
||
@router.get("", response_model=TodoListOut)
|
||
async def list_todos(
|
||
date_str: str = Query(default=None), # YYYY-MM-DD,默认当天
|
||
page: int = Query(default=1, ge=1),
|
||
page_size: int = Query(default=50, ge=1, le=100),
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
target_date = date_str or date.today().isoformat()
|
||
offset = (page - 1) * page_size
|
||
|
||
# 查询总数
|
||
count_q = select(func.count()).select_from(DailyTodo).where(
|
||
DailyTodo.user_id == current_user.id,
|
||
DailyTodo.todo_date == target_date,
|
||
)
|
||
total = (await db.execute(count_q)).scalar()
|
||
|
||
# 查询列表
|
||
q = select(DailyTodo).where(
|
||
DailyTodo.user_id == current_user.id,
|
||
DailyTodo.todo_date == target_date,
|
||
).order_by(DailyTodo.created_at.desc()).offset(offset).limit(page_size)
|
||
|
||
items = (await db.execute(q)).scalars().all()
|
||
return TodoListOut(items=items, total=total, page=page, page_size=page_size)
|
||
|
||
|
||
@router.post("", response_model=TodoOut, status_code=201)
|
||
async def create_todo(
|
||
data: TodoCreate,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
todo = DailyTodo(
|
||
user_id=current_user.id,
|
||
title=data.title,
|
||
source=TodoSource.MANUAL,
|
||
todo_date=date.today().isoformat(),
|
||
)
|
||
db.add(todo)
|
||
await db.commit()
|
||
await db.refresh(todo)
|
||
return todo
|
||
|
||
|
||
@router.patch("/{todo_id}", response_model=TodoOut)
|
||
async def update_todo(
|
||
todo_id: str,
|
||
data: TodoUpdate,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
result = await db.execute(
|
||
select(DailyTodo).where(DailyTodo.id == todo_id, DailyTodo.user_id == current_user.id)
|
||
)
|
||
todo = result.scalar_one_or_none()
|
||
if not todo:
|
||
raise HTTPException(status_code=404, detail="待办不存在")
|
||
|
||
# 历史日期不允许修改
|
||
if todo.todo_date != date.today().isoformat():
|
||
raise HTTPException(status_code=403, detail="历史待办不可修改")
|
||
|
||
if data.title is not None:
|
||
todo.title = data.title
|
||
if data.is_completed is not None:
|
||
from datetime import UTC, datetime
|
||
todo.is_completed = data.is_completed
|
||
todo.completed_at = datetime.now(UTC) if data.is_completed else None
|
||
|
||
await db.commit()
|
||
await db.refresh(todo)
|
||
return todo
|
||
|
||
|
||
@router.delete("/{todo_id}", status_code=204)
|
||
async def delete_todo(
|
||
todo_id: str,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
result = await db.execute(
|
||
select(DailyTodo).where(DailyTodo.id == todo_id, DailyTodo.user_id == current_user.id)
|
||
)
|
||
todo = result.scalar_one_or_none()
|
||
if not todo:
|
||
raise HTTPException(status_code=404, detail="待办不存在")
|
||
if todo.todo_date != date.today().isoformat():
|
||
raise HTTPException(status_code=403, detail="历史待办不可删除")
|
||
|
||
await db.delete(todo)
|
||
await db.commit()
|
||
|
||
|
||
@router.post("/ai-generate", response_model=TodoListOut)
|
||
async def ai_generate_todos(
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
target_date = date.today().isoformat()
|
||
|
||
# 幂等检查:是否已有AI生成记录
|
||
check_q = select(func.count()).select_from(DailyTodo).where(
|
||
DailyTodo.user_id == current_user.id,
|
||
DailyTodo.todo_date == target_date,
|
||
DailyTodo.source.in_([TodoSource.AI_KANBAN, TodoSource.AI_CHAT]),
|
||
)
|
||
count = (await db.execute(check_q)).scalar()
|
||
|
||
if count > 0:
|
||
# 已生成,返回现有记录
|
||
q = select(DailyTodo).where(
|
||
DailyTodo.user_id == current_user.id,
|
||
DailyTodo.todo_date == target_date,
|
||
).order_by(DailyTodo.created_at.desc())
|
||
items = (await db.execute(q)).scalars().all()
|
||
return TodoListOut(items=items, total=len(items), page=1, page_size=50)
|
||
|
||
# 执行AI生成
|
||
todos = await generate_daily_todos(current_user.id, db)
|
||
return TodoListOut(items=todos, total=len(todos), page=1, page_size=50)
|
||
|
||
|
||
@router.get("/summary", response_model=TodoSummaryOut)
|
||
async def get_summary(
|
||
date_str: str = Query(default=None),
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
target_date = date_str or date.today().isoformat()
|
||
q = select(DailyTodo).where(
|
||
DailyTodo.user_id == current_user.id,
|
||
DailyTodo.todo_date == target_date,
|
||
)
|
||
todos = (await db.execute(q)).scalars().all()
|
||
completed = sum(1 for t in todos if t.is_completed)
|
||
return TodoSummaryOut(date=target_date, total=len(todos), completed=completed, pending=len(todos) - completed)
|