功能: - 通知提醒系统: 还款日提醒、逾期提醒、系统消息横幅、登录时通知 - 通知中心UI: 铃铛图标 + 下拉列表 + 未读徽章 - 定时任务: APScheduler 每小时检查即将到期的还款 - 邮件通知: SMTP 发送还款提醒和安全警告 - 异常登录通知: 3/5/10次失败触发网页和邮件通知 安全: - 登录安全告警: 异常登录次数达到阈值时发送通知 - 密码重置链接: 一次性使用, 5分钟有效期 - 登录频率限制: IP级和账户级限制 技术: - 后端: FastAPI + SQLAlchemy + PostgreSQL - 前端: 纯 HTML/CSS/JS - 部署: Docker Compose (新增 build 配置)
102 lines
3.8 KiB
Python
102 lines
3.8 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func
|
|
from datetime import datetime, timedelta
|
|
from app.core.database import get_db
|
|
from app.models.models import User, Debt, RepaymentPlan, RepaymentRecord, PlanStatus, DebtStatus
|
|
from app.schemas.schemas import RepaymentRecordCreate, RepaymentRecordResponse, DashboardSummary
|
|
from app.core.deps import get_current_user
|
|
|
|
router = APIRouter(prefix="/repayments", tags=["还款管理"])
|
|
|
|
|
|
def _naive(dt: datetime) -> datetime:
|
|
if dt and dt.tzinfo:
|
|
return dt.replace(tzinfo=None)
|
|
return dt
|
|
|
|
|
|
@router.post("", response_model=RepaymentRecordResponse, status_code=201)
|
|
async def create_repayment(
|
|
req: RepaymentRecordCreate,
|
|
user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(Debt).where(Debt.id == req.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="无权为此债务还款")
|
|
|
|
record = RepaymentRecord(
|
|
debt_id=req.debt_id, plan_id=req.plan_id,
|
|
amount=req.amount, paid_date=_naive(req.paid_date), notes=req.notes,
|
|
)
|
|
db.add(record)
|
|
|
|
if req.plan_id:
|
|
plan_result = await db.execute(select(RepaymentPlan).where(RepaymentPlan.id == req.plan_id))
|
|
plan = plan_result.scalar_one_or_none()
|
|
if plan:
|
|
plan.status = PlanStatus.PAID.value
|
|
|
|
total_paid_result = await db.execute(
|
|
select(func.coalesce(func.sum(RepaymentRecord.amount), 0)).where(RepaymentRecord.debt_id == req.debt_id)
|
|
)
|
|
total_paid = total_paid_result.scalar() + req.amount
|
|
if total_paid >= debt.amount:
|
|
debt.status = DebtStatus.PAID.value
|
|
|
|
await db.commit()
|
|
await db.refresh(record)
|
|
return record
|
|
|
|
|
|
@router.get("/dashboard", response_model=DashboardSummary)
|
|
async def get_dashboard(
|
|
user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
debts_result = await db.execute(select(Debt).where(Debt.status == DebtStatus.ACTIVE.value))
|
|
debts = debts_result.scalars().all()
|
|
|
|
total_debt = sum(d.amount for d in debts)
|
|
total_paid = 0
|
|
for d in debts:
|
|
pr = await db.execute(select(func.coalesce(func.sum(RepaymentRecord.amount), 0)).where(RepaymentRecord.debt_id == d.id))
|
|
total_paid += pr.scalar()
|
|
|
|
active_count = sum(1 for d in debts if d.status == DebtStatus.ACTIVE.value)
|
|
overdue_count = sum(1 for d in debts if d.status == DebtStatus.OVERDUE.value)
|
|
paid_all = await db.execute(select(Debt).where(Debt.status == DebtStatus.PAID.value))
|
|
paid_count = len(paid_all.scalars().all())
|
|
|
|
now = datetime.utcnow()
|
|
upcoming = []
|
|
for d in debts:
|
|
if d.status != DebtStatus.ACTIVE.value:
|
|
continue
|
|
plans_result = await db.execute(
|
|
select(RepaymentPlan).where(
|
|
RepaymentPlan.debt_id == d.id,
|
|
RepaymentPlan.status == PlanStatus.PENDING.value,
|
|
RepaymentPlan.due_date <= now + timedelta(days=7),
|
|
).order_by(RepaymentPlan.due_date)
|
|
)
|
|
for p in plans_result.scalars().all():
|
|
upcoming.append({
|
|
"debt_id": d.id, "creditor": d.creditor_name,
|
|
"due_date": p.due_date.isoformat(), "amount": p.total_due,
|
|
"overdue": p.due_date < now,
|
|
})
|
|
|
|
upcoming.sort(key=lambda x: x["due_date"])
|
|
|
|
return DashboardSummary(
|
|
total_debt=total_debt, total_paid=total_paid,
|
|
total_remaining=total_debt - total_paid,
|
|
active_count=active_count, overdue_count=overdue_count,
|
|
paid_count=paid_count, upcoming_payments=upcoming[:10],
|
|
)
|