功能: - 通知提醒系统: 还款日提醒、逾期提醒、系统消息横幅、登录时通知 - 通知中心UI: 铃铛图标 + 下拉列表 + 未读徽章 - 定时任务: APScheduler 每小时检查即将到期的还款 - 邮件通知: SMTP 发送还款提醒和安全警告 - 异常登录通知: 3/5/10次失败触发网页和邮件通知 安全: - 登录安全告警: 异常登录次数达到阈值时发送通知 - 密码重置链接: 一次性使用, 5分钟有效期 - 登录频率限制: IP级和账户级限制 技术: - 后端: FastAPI + SQLAlchemy + PostgreSQL - 前端: 纯 HTML/CSS/JS - 部署: Docker Compose (新增 build 配置)
80 lines
3.3 KiB
Python
80 lines
3.3 KiB
Python
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 []
|