refactor: remove all asset/account functionality (models, schemas, routers, store, views, components, tests, docs)
This commit is contained in:
@@ -59,7 +59,7 @@ def init_db():
|
||||
"""初始化数据库表,自动补充新增的列"""
|
||||
# 导入所有模型,确保 Base.metadata 包含全部表定义
|
||||
from app.models import ( # noqa: F401
|
||||
task, category, tag, user_settings, habit, anniversary, account,
|
||||
task, category, tag, user_settings, habit, anniversary,
|
||||
)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@ from app.models.tag import Tag, task_tags
|
||||
from app.models.user_settings import UserSettings
|
||||
from app.models.habit import HabitGroup, Habit, HabitCheckin
|
||||
from app.models.anniversary import AnniversaryCategory, Anniversary
|
||||
from app.models.account import FinancialAccount, AccountHistory, DebtInstallment
|
||||
|
||||
__all__ = [
|
||||
"Task", "Category", "Tag", "task_tags", "UserSettings",
|
||||
"HabitGroup", "Habit", "HabitCheckin",
|
||||
"AnniversaryCategory", "Anniversary",
|
||||
"FinancialAccount", "AccountHistory", "DebtInstallment",
|
||||
]
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, Boolean, Float, DateTime, ForeignKey, Date
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.database import Base
|
||||
from app.utils.datetime import utcnow
|
||||
|
||||
|
||||
class FinancialAccount(Base):
|
||||
"""财务账户模型"""
|
||||
__tablename__ = "financial_accounts"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
account_type = Column(String(20), nullable=False, default="savings") # savings / debt
|
||||
balance = Column(Float, default=0.0)
|
||||
icon = Column(String(50), default="wallet")
|
||||
color = Column(String(20), default="#FFB7C5")
|
||||
sort_order = Column(Integer, default=0)
|
||||
is_active = Column(Boolean, default=True)
|
||||
description = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=utcnow)
|
||||
updated_at = Column(DateTime, default=utcnow, onupdate=utcnow)
|
||||
|
||||
# 关联关系
|
||||
history_records = relationship("AccountHistory", back_populates="account", cascade="all, delete-orphan")
|
||||
debt_installments = relationship("DebtInstallment", back_populates="account", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class AccountHistory(Base):
|
||||
"""余额变更历史"""
|
||||
__tablename__ = "account_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
account_id = Column(Integer, ForeignKey("financial_accounts.id"), nullable=False)
|
||||
change_amount = Column(Float, nullable=False)
|
||||
balance_before = Column(Float, nullable=False)
|
||||
balance_after = Column(Float, nullable=False)
|
||||
note = Column(String(200), nullable=True)
|
||||
created_at = Column(DateTime, default=utcnow)
|
||||
|
||||
# 关联关系
|
||||
account = relationship("FinancialAccount", back_populates="history_records")
|
||||
|
||||
|
||||
class DebtInstallment(Base):
|
||||
"""分期还款计划"""
|
||||
__tablename__ = "debt_installments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
account_id = Column(Integer, ForeignKey("financial_accounts.id"), nullable=False)
|
||||
total_amount = Column(Float, nullable=False)
|
||||
total_periods = Column(Integer, nullable=False)
|
||||
current_period = Column(Integer, nullable=False, default=1) # 1-based, 指向下一期待还
|
||||
payment_day = Column(Integer, nullable=False) # 每月还款日 1-31
|
||||
payment_amount = Column(Float, nullable=False)
|
||||
start_date = Column(Date, nullable=False)
|
||||
is_completed = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, default=utcnow)
|
||||
updated_at = Column(DateTime, default=utcnow, onupdate=utcnow)
|
||||
|
||||
# 关联关系
|
||||
account = relationship("FinancialAccount", back_populates="debt_installments")
|
||||
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from app.routers import tasks, categories, tags, user_settings, habits, anniversaries, accounts, auth
|
||||
from app.routers import tasks, categories, tags, user_settings, habits, anniversaries, auth
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -10,4 +10,3 @@ api_router.include_router(tags.router)
|
||||
api_router.include_router(user_settings.router)
|
||||
api_router.include_router(habits.router)
|
||||
api_router.include_router(anniversaries.router)
|
||||
api_router.include_router(accounts.router)
|
||||
|
||||
@@ -1,490 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional, List
|
||||
from datetime import date
|
||||
from calendar import monthrange
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.account import FinancialAccount, AccountHistory, DebtInstallment
|
||||
from app.schemas.account import (
|
||||
AccountCreate, AccountUpdate, AccountResponse, BalanceUpdateRequest,
|
||||
AccountHistoryResponse, AccountListItemResponse, PaginatedAccountHistoryResponse,
|
||||
DebtInstallmentCreate, DebtInstallmentUpdate, DebtInstallmentResponse,
|
||||
)
|
||||
from app.schemas.common import DeleteResponse
|
||||
from app.utils.crud import get_or_404
|
||||
from app.utils.datetime import utcnow
|
||||
from app.utils.logger import logger
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["资产"])
|
||||
|
||||
|
||||
def compute_installment_info(installment: DebtInstallment, today: date) -> dict:
|
||||
"""计算分期计划的下次还款日期、距今天数、剩余期数"""
|
||||
if installment.is_completed:
|
||||
return {
|
||||
"next_payment_date": None,
|
||||
"days_until_payment": None,
|
||||
"remaining_periods": 0,
|
||||
}
|
||||
|
||||
remaining = installment.total_periods - installment.current_period + 1
|
||||
if remaining <= 0:
|
||||
return {
|
||||
"next_payment_date": None,
|
||||
"days_until_payment": None,
|
||||
"remaining_periods": 0,
|
||||
}
|
||||
|
||||
# 根据 start_date 和 payment_day 计算下一还款日期
|
||||
payment_day = installment.payment_day
|
||||
start_year = installment.start_date.year
|
||||
start_month = installment.start_date.month
|
||||
|
||||
# 计算当前应还的期数对应的月份
|
||||
period_index = installment.current_period - 1
|
||||
next_month_year = start_year * 12 + (start_month - 1) + period_index
|
||||
next_year = next_month_year // 12
|
||||
next_month = next_month_year % 12 + 1
|
||||
|
||||
# 处理 payment_day 超出当月天数的情况
|
||||
max_day = monthrange(next_year, next_month)[1]
|
||||
actual_day = min(payment_day, max_day)
|
||||
next_payment_date = date(next_year, next_month, actual_day)
|
||||
|
||||
# 如果计算出的日期在 start_date 之前(边界情况),使用 start_date
|
||||
if next_payment_date < installment.start_date:
|
||||
next_payment_date = installment.start_date
|
||||
|
||||
days_until = (next_payment_date - today).days
|
||||
|
||||
return {
|
||||
"next_payment_date": next_payment_date,
|
||||
"days_until_payment": days_until,
|
||||
"remaining_periods": remaining,
|
||||
}
|
||||
|
||||
|
||||
# ============ 财务账户 API ============
|
||||
|
||||
@router.get("/accounts", response_model=List[AccountListItemResponse])
|
||||
def get_accounts(db: Session = Depends(get_db)):
|
||||
"""获取所有账户列表"""
|
||||
try:
|
||||
accounts = db.query(FinancialAccount).order_by(
|
||||
FinancialAccount.sort_order.asc(),
|
||||
FinancialAccount.id.asc()
|
||||
).all()
|
||||
|
||||
result = []
|
||||
for acc in accounts:
|
||||
data = {
|
||||
"id": acc.id,
|
||||
"name": acc.name,
|
||||
"account_type": acc.account_type,
|
||||
"balance": acc.balance,
|
||||
"icon": acc.icon,
|
||||
"color": acc.color,
|
||||
"sort_order": acc.sort_order,
|
||||
"is_active": acc.is_active,
|
||||
"description": acc.description,
|
||||
"created_at": acc.created_at,
|
||||
"updated_at": acc.updated_at,
|
||||
}
|
||||
|
||||
# 附加分期计划摘要(欠款账户)
|
||||
if acc.account_type == "debt":
|
||||
installments = db.query(DebtInstallment).filter(
|
||||
DebtInstallment.account_id == acc.id,
|
||||
DebtInstallment.is_completed == False
|
||||
).all()
|
||||
today = date.today()
|
||||
active_installments = []
|
||||
for inst in installments:
|
||||
info = compute_installment_info(inst, today)
|
||||
active_installments.append(info)
|
||||
data["installments"] = active_installments
|
||||
else:
|
||||
data["installments"] = []
|
||||
|
||||
result.append(data)
|
||||
|
||||
logger.info(f"获取账户列表成功,总数: {len(result)}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"获取账户列表失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="获取账户列表失败")
|
||||
|
||||
|
||||
@router.post("/accounts", response_model=AccountResponse, status_code=201)
|
||||
def create_account(data: AccountCreate, db: Session = Depends(get_db)):
|
||||
"""创建账户"""
|
||||
try:
|
||||
account = FinancialAccount(
|
||||
name=data.name,
|
||||
account_type=data.account_type,
|
||||
balance=data.balance,
|
||||
icon=data.icon,
|
||||
color=data.color,
|
||||
sort_order=data.sort_order,
|
||||
is_active=data.is_active,
|
||||
description=data.description,
|
||||
)
|
||||
db.add(account)
|
||||
db.commit()
|
||||
db.refresh(account)
|
||||
logger.info(f"创建账户成功: id={account.id}, name={account.name}")
|
||||
return account
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"创建账户失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="创建账户失败")
|
||||
|
||||
|
||||
@router.get("/accounts/{account_id}", response_model=AccountResponse)
|
||||
def get_account(account_id: int, db: Session = Depends(get_db)):
|
||||
"""获取单个账户"""
|
||||
try:
|
||||
account = get_or_404(db, FinancialAccount, account_id, "账户")
|
||||
return account
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取账户失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="获取账户失败")
|
||||
|
||||
|
||||
@router.put("/accounts/{account_id}", response_model=AccountResponse)
|
||||
def update_account(account_id: int, data: AccountUpdate, db: Session = Depends(get_db)):
|
||||
"""更新账户基本信息"""
|
||||
try:
|
||||
account = get_or_404(db, FinancialAccount, account_id, "账户")
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
|
||||
# 不允许通过此接口修改余额(使用专门的余额更新接口)
|
||||
if "balance" in update_data:
|
||||
del update_data["balance"]
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(account, field, value)
|
||||
|
||||
account.updated_at = utcnow()
|
||||
db.commit()
|
||||
db.refresh(account)
|
||||
logger.info(f"更新账户成功: id={account_id}")
|
||||
return account
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"更新账户失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="更新账户失败")
|
||||
|
||||
|
||||
@router.delete("/accounts/{account_id}")
|
||||
def delete_account(account_id: int, db: Session = Depends(get_db)):
|
||||
"""删除账户(级联删除历史记录和分期计划)"""
|
||||
try:
|
||||
account = get_or_404(db, FinancialAccount, account_id, "账户")
|
||||
db.delete(account)
|
||||
db.commit()
|
||||
logger.info(f"删除账户成功: id={account_id}")
|
||||
return DeleteResponse(message="账户删除成功")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"删除账户失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="删除账户失败")
|
||||
|
||||
|
||||
@router.post("/accounts/{account_id}/balance", response_model=AccountResponse)
|
||||
def update_balance(account_id: int, data: BalanceUpdateRequest, db: Session = Depends(get_db)):
|
||||
"""更新账户余额(自动记录变更历史)"""
|
||||
try:
|
||||
account = get_or_404(db, FinancialAccount, account_id, "账户")
|
||||
old_balance = account.balance
|
||||
new_balance = data.new_balance
|
||||
change_amount = round(new_balance - old_balance, 2)
|
||||
|
||||
# 创建历史记录
|
||||
history = AccountHistory(
|
||||
account_id=account_id,
|
||||
change_amount=change_amount,
|
||||
balance_before=old_balance,
|
||||
balance_after=new_balance,
|
||||
note=data.note,
|
||||
)
|
||||
db.add(history)
|
||||
|
||||
# 更新余额
|
||||
account.balance = new_balance
|
||||
account.updated_at = utcnow()
|
||||
db.commit()
|
||||
db.refresh(account)
|
||||
logger.info(f"更新余额成功: account_id={account_id}, {old_balance} -> {new_balance}")
|
||||
return account
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"更新余额失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="更新余额失败")
|
||||
|
||||
|
||||
@router.get("/accounts/{account_id}/history", response_model=PaginatedAccountHistoryResponse)
|
||||
def get_account_history(
|
||||
account_id: int,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db)):
|
||||
"""获取账户变更历史"""
|
||||
try:
|
||||
account = get_or_404(db, FinancialAccount, account_id, "账户")
|
||||
|
||||
total = db.query(AccountHistory).filter(
|
||||
AccountHistory.account_id == account_id
|
||||
).count()
|
||||
|
||||
records = db.query(AccountHistory).filter(
|
||||
AccountHistory.account_id == account_id
|
||||
).order_by(
|
||||
AccountHistory.created_at.desc()
|
||||
).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
logger.info(f"获取账户历史成功: account_id={account_id}, total={total}")
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"records": records,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取账户历史失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="获取账户历史失败")
|
||||
|
||||
|
||||
# ============ 分期还款计划 API ============
|
||||
|
||||
@router.get("/debt-installments", response_model=List[DebtInstallmentResponse])
|
||||
def get_installments(db: Session = Depends(get_db)):
|
||||
"""获取所有分期计划(含下次还款计算)"""
|
||||
try:
|
||||
installments = db.query(DebtInstallment).order_by(
|
||||
DebtInstallment.is_completed.asc(),
|
||||
DebtInstallment.id.asc()
|
||||
).all()
|
||||
|
||||
today = date.today()
|
||||
result = []
|
||||
for inst in installments:
|
||||
info = compute_installment_info(inst, today)
|
||||
data = {
|
||||
"id": inst.id,
|
||||
"account_id": inst.account_id,
|
||||
"total_amount": inst.total_amount,
|
||||
"total_periods": inst.total_periods,
|
||||
"current_period": inst.current_period,
|
||||
"payment_day": inst.payment_day,
|
||||
"payment_amount": inst.payment_amount,
|
||||
"start_date": inst.start_date,
|
||||
"is_completed": inst.is_completed,
|
||||
"created_at": inst.created_at,
|
||||
"updated_at": inst.updated_at,
|
||||
**info,
|
||||
}
|
||||
|
||||
if inst.account:
|
||||
data["account_name"] = inst.account.name
|
||||
data["account_icon"] = inst.account.icon
|
||||
data["account_color"] = inst.account.color
|
||||
|
||||
result.append(data)
|
||||
|
||||
# 排序:未完成且临近的排前面
|
||||
result.sort(key=lambda x: (
|
||||
0 if not x["is_completed"] and x["days_until_payment"] is not None else 1,
|
||||
0 if not x["is_completed"] else 1,
|
||||
x["days_until_payment"] if x["days_until_payment"] is not None else 9999,
|
||||
))
|
||||
|
||||
logger.info(f"获取分期计划列表成功,总数: {len(result)}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"获取分期计划列表失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="获取分期计划列表失败")
|
||||
|
||||
|
||||
@router.post("/debt-installments", response_model=DebtInstallmentResponse, status_code=201)
|
||||
def create_installment(data: DebtInstallmentCreate, db: Session = Depends(get_db)):
|
||||
"""创建分期计划"""
|
||||
try:
|
||||
# 验证关联账户存在且为欠款类型
|
||||
account = get_or_404(db, FinancialAccount, data.account_id, "账户")
|
||||
if account.account_type != "debt":
|
||||
raise HTTPException(status_code=400, detail="分期计划只能关联欠款类型的账户")
|
||||
|
||||
installment = DebtInstallment(
|
||||
account_id=data.account_id,
|
||||
total_amount=data.total_amount,
|
||||
total_periods=data.total_periods,
|
||||
current_period=data.current_period,
|
||||
payment_day=data.payment_day,
|
||||
payment_amount=data.payment_amount,
|
||||
start_date=data.start_date,
|
||||
is_completed=data.is_completed,
|
||||
)
|
||||
db.add(installment)
|
||||
db.commit()
|
||||
db.refresh(installment)
|
||||
|
||||
# 返回含计算字段的响应
|
||||
today = date.today()
|
||||
info = compute_installment_info(installment, today)
|
||||
|
||||
logger.info(f"创建分期计划成功: id={installment.id}")
|
||||
return DebtInstallmentResponse(
|
||||
id=installment.id,
|
||||
account_id=installment.account_id,
|
||||
total_amount=installment.total_amount,
|
||||
total_periods=installment.total_periods,
|
||||
current_period=installment.current_period,
|
||||
payment_day=installment.payment_day,
|
||||
payment_amount=installment.payment_amount,
|
||||
start_date=installment.start_date,
|
||||
is_completed=installment.is_completed,
|
||||
created_at=installment.created_at,
|
||||
updated_at=installment.updated_at,
|
||||
**info,
|
||||
account_name=account.name,
|
||||
account_icon=account.icon,
|
||||
account_color=account.color,
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"创建分期计划失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="创建分期计划失败")
|
||||
|
||||
|
||||
@router.put("/debt-installments/{installment_id}", response_model=DebtInstallmentResponse)
|
||||
def update_installment(installment_id: int, data: DebtInstallmentUpdate, db: Session = Depends(get_db)):
|
||||
"""更新分期计划"""
|
||||
try:
|
||||
installment = get_or_404(db, DebtInstallment, installment_id, "分期计划")
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
|
||||
if "account_id" in update_data:
|
||||
account = get_or_404(db, FinancialAccount, update_data["account_id"], "账户")
|
||||
if account.account_type != "debt":
|
||||
raise HTTPException(status_code=400, detail="分期计划只能关联欠款类型的账户")
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(installment, field, value)
|
||||
|
||||
installment.updated_at = utcnow()
|
||||
db.commit()
|
||||
db.refresh(installment)
|
||||
|
||||
today = date.today()
|
||||
info = compute_installment_info(installment, today)
|
||||
|
||||
result = DebtInstallmentResponse(
|
||||
id=installment.id,
|
||||
account_id=installment.account_id,
|
||||
total_amount=installment.total_amount,
|
||||
total_periods=installment.total_periods,
|
||||
current_period=installment.current_period,
|
||||
payment_day=installment.payment_day,
|
||||
payment_amount=installment.payment_amount,
|
||||
start_date=installment.start_date,
|
||||
is_completed=installment.is_completed,
|
||||
created_at=installment.created_at,
|
||||
updated_at=installment.updated_at,
|
||||
**info,
|
||||
)
|
||||
if installment.account:
|
||||
result.account_name = installment.account.name
|
||||
result.account_icon = installment.account.icon
|
||||
result.account_color = installment.account.color
|
||||
|
||||
logger.info(f"更新分期计划成功: id={installment_id}")
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"更新分期计划失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="更新分期计划失败")
|
||||
|
||||
|
||||
@router.delete("/debt-installments/{installment_id}")
|
||||
def delete_installment(installment_id: int, db: Session = Depends(get_db)):
|
||||
"""删除分期计划"""
|
||||
try:
|
||||
installment = get_or_404(db, DebtInstallment, installment_id, "分期计划")
|
||||
db.delete(installment)
|
||||
db.commit()
|
||||
logger.info(f"删除分期计划成功: id={installment_id}")
|
||||
return DeleteResponse(message="分期计划删除成功")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"删除分期计划失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="删除分期计划失败")
|
||||
|
||||
|
||||
@router.patch("/debt-installments/{installment_id}/pay")
|
||||
def pay_installment(installment_id: int, db: Session = Depends(get_db)):
|
||||
"""标记已还一期"""
|
||||
try:
|
||||
installment = get_or_404(db, DebtInstallment, installment_id, "分期计划")
|
||||
|
||||
if installment.is_completed:
|
||||
raise HTTPException(status_code=400, detail="该分期计划已全部还清")
|
||||
|
||||
installment.current_period += 1
|
||||
|
||||
# 检查是否已全部还清
|
||||
if installment.current_period > installment.total_periods:
|
||||
installment.is_completed = True
|
||||
installment.current_period = installment.total_periods
|
||||
|
||||
installment.updated_at = utcnow()
|
||||
db.commit()
|
||||
db.refresh(installment)
|
||||
|
||||
today = date.today()
|
||||
info = compute_installment_info(installment, today)
|
||||
|
||||
result = DebtInstallmentResponse(
|
||||
id=installment.id,
|
||||
account_id=installment.account_id,
|
||||
total_amount=installment.total_amount,
|
||||
total_periods=installment.total_periods,
|
||||
current_period=installment.current_period,
|
||||
payment_day=installment.payment_day,
|
||||
payment_amount=installment.payment_amount,
|
||||
start_date=installment.start_date,
|
||||
is_completed=installment.is_completed,
|
||||
created_at=installment.created_at,
|
||||
updated_at=installment.updated_at,
|
||||
**info,
|
||||
)
|
||||
if installment.account:
|
||||
result.account_name = installment.account.name
|
||||
result.account_icon = installment.account.icon
|
||||
result.account_color = installment.account.color
|
||||
|
||||
logger.info(f"分期还款成功: id={installment_id}, current_period={installment.current_period}")
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"分期还款失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="分期还款失败")
|
||||
@@ -1,141 +0,0 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import date, datetime
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
# ============ 财务账户 Schema ============
|
||||
|
||||
class AccountBase(BaseModel):
|
||||
"""账户基础模型"""
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
account_type: str = Field(default="savings", pattern="^(savings|debt)$")
|
||||
balance: float = Field(default=0.0)
|
||||
icon: str = Field(default="wallet", max_length=50)
|
||||
color: str = Field(default="#FFB7C5", max_length=20)
|
||||
sort_order: int = Field(default=0)
|
||||
is_active: bool = Field(default=True)
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class AccountCreate(AccountBase):
|
||||
"""创建账户请求模型"""
|
||||
pass
|
||||
|
||||
|
||||
class AccountUpdate(BaseModel):
|
||||
"""更新账户请求模型"""
|
||||
name: Optional[str] = Field(None, max_length=100)
|
||||
account_type: Optional[str] = Field(None, pattern="^(savings|debt)$")
|
||||
balance: Optional[float] = None
|
||||
icon: Optional[str] = Field(None, max_length=50)
|
||||
color: Optional[str] = Field(None, max_length=20)
|
||||
sort_order: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class AccountResponse(AccountBase):
|
||||
"""账户响应模型"""
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AccountListItemResponse(BaseModel):
|
||||
"""账户列表项响应模型(含分期摘要)"""
|
||||
id: int
|
||||
name: str
|
||||
account_type: str
|
||||
balance: float
|
||||
icon: str
|
||||
color: str
|
||||
sort_order: int
|
||||
is_active: bool
|
||||
description: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
installments: List[dict] = []
|
||||
|
||||
|
||||
class BalanceUpdateRequest(BaseModel):
|
||||
"""更新余额请求模型"""
|
||||
new_balance: float
|
||||
note: Optional[str] = Field(None, max_length=200)
|
||||
|
||||
|
||||
# ============ 账户变更历史 Schema ============
|
||||
|
||||
class AccountHistoryResponse(BaseModel):
|
||||
"""变更历史响应模型"""
|
||||
id: int
|
||||
account_id: int
|
||||
change_amount: float
|
||||
balance_before: float
|
||||
balance_after: float
|
||||
note: Optional[str] = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PaginatedAccountHistoryResponse(BaseModel):
|
||||
"""分页账户变更历史响应模型"""
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
records: List[AccountHistoryResponse] = []
|
||||
|
||||
|
||||
# ============ 分期还款计划 Schema ============
|
||||
|
||||
class DebtInstallmentBase(BaseModel):
|
||||
"""分期计划基础模型"""
|
||||
account_id: int
|
||||
total_amount: float
|
||||
total_periods: int = Field(..., ge=1)
|
||||
current_period: int = Field(default=1, ge=1)
|
||||
payment_day: int = Field(..., ge=1, le=31)
|
||||
payment_amount: float = Field(..., gt=0)
|
||||
start_date: date
|
||||
is_completed: bool = Field(default=False)
|
||||
|
||||
|
||||
class DebtInstallmentCreate(DebtInstallmentBase):
|
||||
"""创建分期计划请求模型"""
|
||||
pass
|
||||
|
||||
|
||||
class DebtInstallmentUpdate(BaseModel):
|
||||
"""更新分期计划请求模型"""
|
||||
account_id: Optional[int] = None
|
||||
total_amount: Optional[float] = None
|
||||
total_periods: Optional[int] = Field(None, ge=1)
|
||||
current_period: Optional[int] = Field(None, ge=1)
|
||||
payment_day: Optional[int] = Field(None, ge=1, le=31)
|
||||
payment_amount: Optional[float] = Field(None, gt=0)
|
||||
start_date: Optional[date] = None
|
||||
is_completed: Optional[bool] = None
|
||||
|
||||
|
||||
class DebtInstallmentResponse(DebtInstallmentBase):
|
||||
"""分期计划响应模型(含计算字段)"""
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
# 计算字段
|
||||
next_payment_date: Optional[date] = None
|
||||
days_until_payment: Optional[int] = None
|
||||
remaining_periods: Optional[int] = None
|
||||
|
||||
# 关联账户信息
|
||||
account_name: Optional[str] = None
|
||||
account_icon: Optional[str] = None
|
||||
account_color: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
Reference in New Issue
Block a user