from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from datetime import datetime from app.core.database import get_db from app.models.models import User, Debt, RepaymentPlan, RepaymentRecord, DebtStatus, PlanStatus from app.schemas.schemas import ( DebtCreate, DebtUpdate, DebtResponse, RepaymentPlanResponse, RepaymentRecordCreate, RepaymentRecordResponse, ) from app.core.deps import get_current_user from app.services.calculator import generate_repayment_plan router = APIRouter(prefix="/debts", tags=["债务管理"]) def _naive(dt: datetime) -> datetime: if dt and dt.tzinfo: return dt.replace(tzinfo=None) return dt async def _enrich(db: AsyncSession, d: Debt) -> DebtResponse: dr = DebtResponse.model_validate(d) pr = await db.execute(select(func.coalesce(func.sum(RepaymentRecord.amount), 0)).where(RepaymentRecord.debt_id == d.id)) dr.total_paid = pr.scalar() ir = await db.execute(select(func.coalesce(func.sum(RepaymentPlan.interest_due), 0)).where(RepaymentPlan.debt_id == d.id)) dr.total_interest = ir.scalar() owner = await db.execute(select(User.nickname, User.email).where(User.id == d.user_id)) row = owner.first() dr.owner_name = row[0] or row[1] if row else "" return dr @router.get("", response_model=list[DebtResponse]) async def list_debts( status: str | None = None, creditor: str | None = None, page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): q = select(Debt) if user.role != "admin": q = q.where(Debt.user_id == user.id) if status: q = q.where(Debt.status == status) if creditor: q = q.where(Debt.creditor_name.ilike(f"%{creditor}%")) q = q.order_by(Debt.created_at.desc()).offset((page - 1) * size).limit(size) result = await db.execute(q) debts = result.scalars().all() return [await _enrich(db, d) for d in debts] @router.post("", response_model=DebtResponse, status_code=201) async def create_debt( req: DebtCreate, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): debt = Debt( user_id=user.id, creditor_name=req.creditor_name, amount=req.amount, borrow_date=_naive(req.borrow_date), interest_rate=req.interest_rate, rate_type=req.rate_type, repayment_method=req.repayment_method, period_count=req.period_count, period_unit=req.period_unit, notes=req.notes, ) db.add(debt) await db.flush() plans = generate_repayment_plan( req.amount, req.interest_rate, req.rate_type, req.repayment_method, req.period_count, req.period_unit, _naive(req.borrow_date), ) for p in plans: db.add(RepaymentPlan(debt_id=debt.id, **p)) await db.commit() await db.refresh(debt) return await _enrich(db, debt) @router.get("/{debt_id}", response_model=DebtResponse) async def get_debt( debt_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(Debt).where(Debt.id == debt_id)) debt = result.scalar_one_or_none() if not debt: raise HTTPException(status_code=404, detail="债务不存在") if debt.user_id != user.id and user.role != "admin": raise HTTPException(status_code=403, detail="无权访问此债务") return await _enrich(db, debt) @router.put("/{debt_id}", response_model=DebtResponse) async def update_debt( debt_id: int, req: DebtUpdate, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(Debt).where(Debt.id == debt_id)) debt = result.scalar_one_or_none() if not debt: raise HTTPException(status_code=404, detail="债务不存在") if debt.user_id != user.id and user.role != "admin": raise HTTPException(status_code=403, detail="无权修改此债务") if debt.status not in ("active",): raise HTTPException(status_code=400, detail="该债务不可修改") data = req.model_dump(exclude_unset=True) for k, v in data.items(): setattr(debt, k, _naive(v) if k == "borrow_date" else v) if any(k in data for k in ["amount", "interest_rate", "rate_type", "repayment_method", "period_count", "period_unit", "borrow_date"]): await db.execute(select(RepaymentPlan).where(RepaymentPlan.debt_id == debt.id).delete()) plans = generate_repayment_plan( debt.amount, debt.interest_rate, debt.rate_type, debt.repayment_method, debt.period_count, debt.period_unit, _naive(debt.borrow_date), ) for p in plans: db.add(RepaymentPlan(debt_id=debt.id, **p)) await db.commit() await db.refresh(debt) return await _enrich(db, debt) @router.delete("/{debt_id}") async def delete_debt( debt_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(Debt).where(Debt.id == debt_id)) debt = result.scalar_one_or_none() if not debt: raise HTTPException(status_code=404, detail="债务不存在") if debt.user_id != user.id and user.role != "admin": raise HTTPException(status_code=403, detail="无权删除此债务") has_records = await db.execute(select(func.count(RepaymentRecord.id)).where(RepaymentRecord.debt_id == debt.id)) if has_records.scalar() > 0: debt.status = DebtStatus.CLOSED.value await db.commit() return {"message": "已有还款记录,债务已归档"} await db.delete(debt) await db.commit() return {"message": "债务已删除"} @router.get("/{debt_id}/plans", response_model=list[RepaymentPlanResponse]) async def get_plans( debt_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(Debt).where(Debt.id == debt_id)) debt = result.scalar_one_or_none() if not debt: raise HTTPException(status_code=404, detail="债务不存在") if debt.user_id != user.id and user.role != "admin": raise HTTPException(status_code=403, detail="无权访问此债务") pr = await db.execute(select(RepaymentPlan).where(RepaymentPlan.debt_id == debt_id).order_by(RepaymentPlan.due_date)) return pr.scalars().all() @router.get("/{debt_id}/records", response_model=list[RepaymentRecordResponse]) async def get_records( debt_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(Debt).where(Debt.id == debt_id)) debt = result.scalar_one_or_none() if not debt: raise HTTPException(status_code=404, detail="债务不存在") if debt.user_id != user.id and user.role != "admin": raise HTTPException(status_code=403, detail="无权访问此债务") rr = await db.execute(select(RepaymentRecord).where(RepaymentRecord.debt_id == debt_id).order_by(RepaymentRecord.paid_date.desc())) return rr.scalars().all()