v2.1: 新增通知提醒功能和安全增强

功能:
- 通知提醒系统: 还款日提醒、逾期提醒、系统消息横幅、登录时通知
- 通知中心UI: 铃铛图标 + 下拉列表 + 未读徽章
- 定时任务: APScheduler 每小时检查即将到期的还款
- 邮件通知: SMTP 发送还款提醒和安全警告
- 异常登录通知: 3/5/10次失败触发网页和邮件通知

安全:
- 登录安全告警: 异常登录次数达到阈值时发送通知
- 密码重置链接: 一次性使用, 5分钟有效期
- 登录频率限制: IP级和账户级限制

技术:
- 后端: FastAPI + SQLAlchemy + PostgreSQL
- 前端: 纯 HTML/CSS/JS
- 部署: Docker Compose (新增 build 配置)
This commit is contained in:
OP
2026-06-28 18:06:56 +08:00
commit ffca168dbe
41 changed files with 4834 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
def calculate_monthly_rate(interest_rate: float, rate_type: str) -> float:
if rate_type == "annual":
return interest_rate / 100 / 12
return interest_rate / 100
def generate_equal_installment_plan(
amount: int, interest_rate: float, rate_type: str,
period_count: int, start_date: datetime
) -> list[dict]:
mr = calculate_monthly_rate(interest_rate, rate_type)
if mr == 0:
principal_each = amount // period_count
plans = []
for i in range(period_count):
due = start_date + relativedelta(months=i + 1)
p = principal_each if i < period_count - 1 else amount - principal_each * (period_count - 1)
plans.append({"due_date": due, "principal_due": p, "interest_due": 0, "total_due": p})
return plans
mp = amount * mr * (1 + mr) ** period_count / ((1 + mr) ** period_count - 1)
plans = []
remaining = amount
for i in range(period_count):
due = start_date + relativedelta(months=i + 1)
interest = int(remaining * mr)
principal = int(mp) - interest
if i == period_count - 1:
principal = remaining
interest = int(remaining * mr)
remaining -= principal
total = principal + interest
plans.append({"due_date": due, "principal_due": principal, "interest_due": interest, "total_due": total})
return plans
def generate_interest_first_plan(
amount: int, interest_rate: float, rate_type: str,
period_count: int, start_date: datetime
) -> list[dict]:
mr = calculate_monthly_rate(interest_rate, rate_type)
plans = []
for i in range(period_count):
due = start_date + relativedelta(months=i + 1)
interest = int(amount * mr)
principal = amount if i == period_count - 1 else 0
total = principal + interest
plans.append({"due_date": due, "principal_due": principal, "interest_due": interest, "total_due": total})
return plans
def generate_lump_sum_plan(
amount: int, interest_rate: float, rate_type: str,
period_count: int, period_unit: str, start_date: datetime
) -> list[dict]:
if period_unit == "year":
total_interest = int(amount * interest_rate / 100 * period_count)
else:
total_interest = int(amount * interest_rate / 100 / 12 * period_count)
due = start_date + relativedelta(months=period_count) if period_unit == "month" else start_date + relativedelta(years=period_count)
return [{"due_date": due, "principal_due": amount, "interest_due": total_interest, "total_due": amount + total_interest}]
def generate_repayment_plan(
amount: int, interest_rate: float, rate_type: str,
repayment_method: str, period_count: int, period_unit: str,
start_date: datetime
) -> list[dict]:
if repayment_method == "equal_installment":
return generate_equal_installment_plan(amount, interest_rate, rate_type, period_count, start_date)
elif repayment_method == "interest_first":
return generate_interest_first_plan(amount, interest_rate, rate_type, period_count, start_date)
elif repayment_method == "lump_sum":
return generate_lump_sum_plan(amount, interest_rate, rate_type, period_count, period_unit, start_date)
return []