严重修复: - C1: JWT密钥生成64字节随机字符串替代硬编码占位符 - C2: 数据库密码改为随机强密码,移除宿主机端口映射 - C3: 管理员默认密码改为强密码 高危修复: - H1: 移除数据库54326端口映射,仅内部网络访问 - H3: 添加X-Frame-Options/CSP/XSS-Protection等安全头 - H4: 前端所有innerHTML拼接处添加escapeHtml转义防XSS - H5: SMTP密码在API响应中脱敏显示 - H6: 债务列表接口添加用户隔离,普通用户只能看自己的数据 - H7: 债务详情/计划/记录接口添加归属校验 - H8: 账户级暴力破解锁定(5次失败锁定5分钟) - H9: 速率限制数据增加过期清理机制防内存泄漏 中危修复: - M1: 注册/重置密码接口添加后端密码强度校验(最少8位) - M4: 添加全局异常处理器,500错误不再暴露堆栈 - M5: Docker容器改为非root用户运行 - M6: 添加.dockerignore排除pyc和敏感文件 - M7: 管理员操作写入审计日志 - M8: CORS限制为实际使用的HTTP方法和头 其他: - Nginx隐藏版本号(server_tokens off) - 管理员角色修改添加枚举校验 - 密码重置验证码接口返回统一错误信息
183 lines
7.1 KiB
Python
183 lines
7.1 KiB
Python
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()
|