功能: - 通知提醒系统: 还款日提醒、逾期提醒、系统消息横幅、登录时通知 - 通知中心UI: 铃铛图标 + 下拉列表 + 未读徽章 - 定时任务: APScheduler 每小时检查即将到期的还款 - 邮件通知: SMTP 发送还款提醒和安全警告 - 异常登录通知: 3/5/10次失败触发网页和邮件通知 安全: - 登录安全告警: 异常登录次数达到阈值时发送通知 - 密码重置链接: 一次性使用, 5分钟有效期 - 登录频率限制: IP级和账户级限制 技术: - 后端: FastAPI + SQLAlchemy + PostgreSQL - 前端: 纯 HTML/CSS/JS - 部署: Docker Compose (新增 build 配置)
153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from datetime import datetime
|
|
from pydantic import BaseModel
|
|
from app.core.database import get_db
|
|
from app.models.models import User, Debt
|
|
from app.schemas.schemas import LoanCreate, LoanResponse
|
|
from app.core.deps import get_current_user
|
|
|
|
router = APIRouter(prefix="/loans", tags=["借款管理"])
|
|
|
|
METHOD_MAP = {"e": "equal_installment", "p": "interest_first", "i": "lump_sum"}
|
|
METHOD_REV = {"equal_installment": "e", "interest_first": "p", "lump_sum": "i"}
|
|
|
|
|
|
class PaidUpdate(BaseModel):
|
|
paid: list[int]
|
|
|
|
|
|
def _debt_to_loan(d: Debt, owner_name: str = "") -> LoanResponse:
|
|
return LoanResponse(
|
|
id=d.id,
|
|
name=d.creditor_name,
|
|
date=d.borrow_date.strftime("%Y-%m-%d") if d.borrow_date else "",
|
|
amount=d.amount / 100,
|
|
rate=d.interest_rate,
|
|
periods=d.period_count,
|
|
method=METHOD_REV.get(d.repayment_method, "e"),
|
|
purpose=d.purpose or "",
|
|
schedule=d.schedule_data or [],
|
|
paid=d.paid_periods or [],
|
|
user_id=d.user_id,
|
|
owner_name=owner_name,
|
|
)
|
|
|
|
|
|
async def _get_visible_user_ids(user: User, db: AsyncSession) -> list[int]:
|
|
if user.role == "admin":
|
|
result = await db.execute(select(User.id))
|
|
return [r[0] for r in result.all()]
|
|
if user.group_id:
|
|
result = await db.execute(select(User.id).where(User.group_id == user.group_id))
|
|
ids = [r[0] for r in result.all()]
|
|
if user.id not in ids:
|
|
ids.append(user.id)
|
|
return ids
|
|
return [user.id]
|
|
|
|
|
|
@router.get("", response_model=list[LoanResponse])
|
|
async def list_loans(
|
|
user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
visible_ids = await _get_visible_user_ids(user, db)
|
|
result = await db.execute(select(Debt).where(Debt.user_id.in_(visible_ids)).order_by(Debt.created_at.desc()))
|
|
debts = result.scalars().all()
|
|
users_result = await db.execute(select(User.id, User.nickname))
|
|
users_map = {uid: (nick or "") for uid, nick in users_result.all()}
|
|
return [_debt_to_loan(d, users_map.get(d.user_id, "")) for d in debts]
|
|
|
|
|
|
@router.post("", response_model=LoanResponse, status_code=201)
|
|
async def create_loan(
|
|
req: LoanCreate,
|
|
user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
debt = Debt(
|
|
user_id=user.id,
|
|
creditor_name=req.name,
|
|
amount=int(req.amount * 100),
|
|
borrow_date=datetime.strptime(req.date, "%Y-%m-%d"),
|
|
interest_rate=req.rate,
|
|
rate_type="annual",
|
|
repayment_method=METHOD_MAP.get(req.method, "equal_installment"),
|
|
period_count=req.periods,
|
|
period_unit="month",
|
|
purpose=req.purpose,
|
|
schedule_data=req.schedule,
|
|
paid_periods=req.paid,
|
|
)
|
|
db.add(debt)
|
|
await db.commit()
|
|
await db.refresh(debt)
|
|
return _debt_to_loan(debt, user.nickname or "")
|
|
|
|
|
|
@router.put("/{loan_id}", response_model=LoanResponse)
|
|
async def update_loan(
|
|
loan_id: int,
|
|
req: LoanCreate,
|
|
user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(Debt).where(Debt.id == loan_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="无权修改")
|
|
|
|
debt.creditor_name = req.name
|
|
debt.amount = int(req.amount * 100)
|
|
debt.borrow_date = datetime.strptime(req.date, "%Y-%m-%d")
|
|
debt.interest_rate = req.rate
|
|
debt.repayment_method = METHOD_MAP.get(req.method, "equal_installment")
|
|
debt.period_count = req.periods
|
|
debt.purpose = req.purpose
|
|
debt.schedule_data = req.schedule
|
|
debt.paid_periods = req.paid
|
|
|
|
await db.commit()
|
|
await db.refresh(debt)
|
|
return _debt_to_loan(debt, user.nickname or "")
|
|
|
|
|
|
@router.delete("/{loan_id}")
|
|
async def delete_loan(
|
|
loan_id: int,
|
|
user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(Debt).where(Debt.id == loan_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="无权删除")
|
|
await db.delete(debt)
|
|
await db.commit()
|
|
return {"message": "已删除"}
|
|
|
|
|
|
@router.patch("/{loan_id}/toggle-paid")
|
|
async def toggle_paid(
|
|
loan_id: int,
|
|
req: PaidUpdate,
|
|
user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(Debt).where(Debt.id == loan_id))
|
|
debt = result.scalar_one_or_none()
|
|
if not debt:
|
|
raise HTTPException(status_code=404, detail="借款不存在")
|
|
if debt.user_id != user.id:
|
|
raise HTTPException(status_code=403, detail="只能标记自己的还款")
|
|
|
|
debt.paid_periods = sorted(req.paid)
|
|
await db.commit()
|
|
return {"paid": debt.paid_periods}
|