"""
数据模型 - Transaction, Account, Category 核心数据结构
实现记账系统的核心数据模型,包括:
- Transaction: 交易记录
- Account: 账户信息
- Category: 分类管理
- TransactionType: 交易类型枚举
"""
from decimal import Decimal, ROUND_HALF_UP
from datetime import datetime, date
from enum import Enum
from typing import Dict, Any, Optional, List
from uuid import uuid4
import json
class TransactionType(Enum):
"""交易类型枚举"""
INCOME = "income"
EXPENSE = "expense"
@classmethod
def from_amount(cls, amount: Decimal) -> "TransactionType":
"""根据金额判断交易类型"""
if amount == 0:
raise ValueError("金额不能为零")
return cls.INCOME if amount > 0 else cls.EXPENSE
class Transaction:
"""交易记录模型"""
def __init__(
self,
amount: Decimal,
category: str,
description: str = "",
date: Optional[date] = None,
id: Optional[str] = None,
timestamp: Optional[datetime] = None
):
# 验证和设置金额
self.amount = self._validate_amount(amount)
# 验证和设置分类
self.category = self._validate_category(category)
# 设置描述
self.description = description or ""
# 设置日期(默认为今天)
self.date = date or datetime.now().date()
self._validate_date(self.date)
# 设置ID(如果未提供则生成)
self.id = id or self._generate_id()
# 设置时间戳(如果未提供则使用当前时间)
self.timestamp = timestamp or datetime.now()
# 根据金额确定交易类型
self.transaction_type = TransactionType.from_amount(self.amount)
def _validate_amount(self, amount: Decimal) -> Decimal:
"""验证金额格式和精度"""
if not isinstance(amount, Decimal):
amount = Decimal(str(amount))
# 检查金额不能为零
if amount == 0:
raise ValueError("金额不能为零")
# 检查精度(最多2位小数)
if amount.as_tuple().exponent < -2:
raise ValueError("金额精度不能超过2位小数")
# 四舍五入到2位小数
return amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
def _validate_category(self, category: str) -> str:
"""验证分类"""
if not category or not category.strip():
raise ValueError("分类不能为空")
return category.strip()
def _validate_date(self, transaction_date: date) -> None:
"""验证交易日期"""
if transaction_date > date.today():
raise ValueError("交易日期不能是未来")
def _generate_id(self) -> str:
"""生成唯一的交易ID"""
return f"txn_{self.date.strftime('%Y%m%d')}_{str(uuid4())[:8]}"
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
return {
"id": self.id,
"amount": float(self.amount),
"category": self.category,
"description": self.description,
"date": self.date.isoformat(),
"timestamp": self.timestamp.isoformat() + "Z" if self.timestamp.tzinfo is None else self.timestamp.isoformat(),
"type": self.transaction_type.value
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Transaction":
"""从字典创建交易对象"""
# 解析日期
transaction_date = datetime.fromisoformat(data["date"]).date() if isinstance(data["date"], str) else data["date"]
# 解析时间戳
timestamp = None
if "timestamp" in data and data["timestamp"]:
timestamp_str = data["timestamp"].rstrip("Z")
timestamp = datetime.fromisoformat(timestamp_str)
return cls(
id=data["id"],
amount=Decimal(str(data["amount"])),
category=data["category"],
description=data.get("description", ""),
date=transaction_date,
timestamp=timestamp
)
def __str__(self) -> str:
"""字符串表示"""
type_symbol = "+" if self.transaction_type == TransactionType.INCOME else "-"
return f"{type_symbol}¥{abs(self.amount)} [{self.category}] {self.description}"
def __repr__(self) -> str:
"""调试字符串表示"""
return f"Transaction(id={self.id}, amount={self.amount}, category={self.category})"
class Account:
"""账户信息模型"""
def __init__(
self,
balance: Decimal = Decimal('0.00'),
total_transactions: int = 0,
created_at: Optional[datetime] = None,
last_updated: Optional[datetime] = None
):
self.balance = Decimal(str(balance)) if balance else Decimal('0.00')
self.total_transactions = total_transactions
self.created_at = created_at or datetime.now()
self.last_updated = last_updated or datetime.now()
def add_transaction(self, transaction: Transaction) -> None:
"""添加交易到账户"""
self.balance += transaction.amount
self.total_transactions += 1
self.last_updated = datetime.now()
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
return {
"balance": float(self.balance),
"total_transactions": self.total_transactions,
"created_at": self.created_at.isoformat() + "Z" if self.created_at.tzinfo is None else self.created_at.isoformat(),
"last_updated": self.last_updated.isoformat() + "Z" if self.last_updated.tzinfo is None else self.last_updated.isoformat()
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Account":
"""从字典创建账户对象"""
# 解析时间戳
created_at = None
last_updated = None
if "created_at" in data and data["created_at"]:
created_str = data["created_at"].rstrip("Z")
created_at = datetime.fromisoformat(created_str)
if "last_updated" in data and data["last_updated"]:
updated_str = data["last_updated"].rstrip("Z")
last_updated = datetime.fromisoformat(updated_str)
return cls(
balance=Decimal(str(data.get("balance", 0))),
total_transactions=data.get("total_transactions", 0),
created_at=created_at,
last_updated=last_updated
)
def __str__(self) -> str:
"""字符串表示"""
return f"Account(余额: ¥{self.balance}, 交易数: {self.total_transactions})"
class Category:
"""分类模型"""
def __init__(
self,
id: str,
name: str,
description: str = "",
category_type: TransactionType = TransactionType.EXPENSE,
color: str = "#D3D3D3",
icon: str = "📝"
):
# 验证分类ID
if not id or not id.strip():
raise ValueError("分类ID不能为空")
self.id = id.strip()
self.name = name or self.id
self.description = description
self.category_type = category_type
self.color = color
self.icon = icon
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
return {
"id": self.id,
"name": self.name,
"description": self.description,
"type": self.category_type.value,
"color": self.color,
"icon": self.icon
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Category":
"""从字典创建分类对象"""
category_type = TransactionType.EXPENSE
if "type" in data:
if data["type"] == "income":
category_type = TransactionType.INCOME
elif data["type"] == "expense":
category_type = TransactionType.EXPENSE
return cls(
id=data["id"],
name=data.get("name", data["id"]),
description=data.get("description", ""),
category_type=category_type,
color=data.get("color", "#D3D3D3"),
icon=data.get("icon", "📝")
)
def __str__(self) -> str:
"""字符串表示"""
return f"{self.icon} {self.name} ({self.category_type.value})"
def __repr__(self) -> str:
"""调试字符串表示"""
return f"Category(id={self.id}, name={self.name}, type={self.category_type.value})"
# 辅助函数
def create_default_categories() -> List[Category]:
"""创建默认分类列表"""
return [
Category(
id="food",
name="餐饮",
description="餐费、外卖、聚餐等饮食相关支出",
category_type=TransactionType.EXPENSE,
color="#FF6B6B",
icon="🍽️"
),
Category(
id="transport",
name="交通",
description="公交、地铁、打车、加油等交通费用",
category_type=TransactionType.EXPENSE,
color="#4ECDC4",
icon="🚗"
),
Category(
id="entertainment",
name="娱乐",
description="电影、游戏、旅游等娱乐消费",
category_type=TransactionType.EXPENSE,
color="#45B7D1",
icon="🎬"
),
Category(
id="shopping",
name="购物",
description="服装、日用品、电子产品等购物支出",
category_type=TransactionType.EXPENSE,
color="#96CEB4",
icon="🛍️"
),
Category(
id="healthcare",
name="医疗",
description="医院、药品、体检等医疗健康支出",
category_type=TransactionType.EXPENSE,
color="#FFEAA7",
icon="🏥"
),
Category(
id="education",
name="教育",
description="培训、课程、书籍等教育投资",
category_type=TransactionType.EXPENSE,
color="#DDA0DD",
icon="📚"
),
Category(
id="income",
name="收入",
description="工资、奖金、投资收益等收入",
category_type=TransactionType.INCOME,
color="#90EE90",
icon="💰"
),
Category(
id="other",
name="其他",
description="其他未分类的支出或收入",
category_type=TransactionType.EXPENSE,
color="#D3D3D3",
icon="📝"
)
]