feat(backend): complete modular monolith with ledger, fastapi and websocket

This commit is contained in:
Bot
2026-08-31 01:02:16 +08:00
parent 9f6ecf33f9
commit 30dc22a989
16 changed files with 706 additions and 17 deletions
+123
View File
@@ -0,0 +1,123 @@
import logging
from decimal import Decimal
from typing import Optional, Dict, Any, List
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.entities import (
AccountBalanceModel,
LedgerEntryModel,
LedgerEntryType,
)
logger = logging.getLogger(__name__)
class LedgerError(Exception):
pass
class InsufficientBalanceError(LedgerError):
pass
class LedgerService:
"""
【不可变复式记账核心领域服务】
所有资金变动必须通过本服务产生不可变流水并更新可用/冻结余额快照。
"""
def __init__(self, session: AsyncSession):
self.session = session
async def get_or_create_balance(self, address: str, collateral: str = "WUSD") -> AccountBalanceModel:
addr = address.lower()
stmt = select(AccountBalanceModel).where(
AccountBalanceModel.address == addr,
AccountBalanceModel.collateral_symbol == collateral
).with_for_update()
result = await self.session.execute(stmt)
balance = result.scalar_one_or_none()
if not balance:
balance = AccountBalanceModel(
address=addr,
collateral_symbol=collateral,
available_balance=Decimal("0"),
frozen_balance=Decimal("0")
)
self.session.add(balance)
await self.session.flush()
return balance
async def record_deposit(
self,
address: str,
amount: Decimal,
tx_hash: str,
chain_id: int = 46630
) -> LedgerEntryModel:
"""记录充值流水"""
if amount <= 0:
raise LedgerError("充值金额必须大于 0")
balance = await self.get_or_create_balance(address)
balance.available_balance += amount
entry = LedgerEntryModel(
tx_hash=tx_hash,
chain_id=chain_id,
user_address=address.lower(),
entry_type=LedgerEntryType.DEPOSIT,
amount=amount,
balance_after=balance.available_balance,
frozen_after=balance.frozen_balance,
extra_metadata={"reason": "On-chain Deposit Confirmed"}
)
self.session.add(entry)
await self.session.flush()
logger.info(f"Deposit recorded: User {address}, Amount {amount}, Balance {balance.available_balance}")
return entry
async def record_trade(
self,
address: str,
market_address: str,
order_id: str,
side: str,
amount: Decimal,
fee: Decimal,
tx_hash: Optional[str] = None
) -> LedgerEntryModel:
"""记录交易扣款与到账"""
balance = await self.get_or_create_balance(address)
total_deduct = amount + fee
if side == "BUY":
if balance.available_balance < total_deduct:
raise InsufficientBalanceError("可用余额不足以支付买入金额与手续费")
balance.available_balance -= total_deduct
entry_type = LedgerEntryType.TRADE_BUY
delta = -total_deduct
else:
balance.available_balance += (amount - fee)
entry_type = LedgerEntryType.TRADE_SELL
delta = amount - fee
entry = LedgerEntryModel(
tx_hash=tx_hash,
user_address=address.lower(),
entry_type=entry_type,
amount=delta,
balance_after=balance.available_balance,
frozen_after=balance.frozen_balance,
related_market=market_address.lower(),
related_order_id=order_id,
extra_metadata={"side": side, "fee": str(fee)}
)
self.session.add(entry)
await self.session.flush()
return entry
async def list_user_ledger(self, address: str, limit: int = 50) -> List[LedgerEntryModel]:
stmt = select(LedgerEntryModel).where(
LedgerEntryModel.user_address == address.lower()
).order_by(LedgerEntryModel.created_at.desc()).limit(limit)
result = await self.session.execute(stmt)
return list(result.scalars().all())
+75
View File
@@ -0,0 +1,75 @@
import uuid
from decimal import Decimal
from typing import List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.entities import MarketModel, OrderModel, MarketStatus
from app.domains.ledger.service import LedgerService
class MarketService:
"""预测市场领域服务"""
def __init__(self, session: AsyncSession):
self.session = session
async def list_markets(self, category: Optional[str] = None, status: Optional[str] = None) -> List[MarketModel]:
stmt = select(MarketModel)
if category:
stmt = stmt.where(MarketModel.category == category)
if status:
stmt = stmt.where(MarketModel.status == status)
stmt = stmt.order_by(MarketModel.created_at.desc())
result = await self.session.execute(stmt)
return list(result.scalars().all())
async def get_market(self, market_address: str) -> Optional[MarketModel]:
stmt = select(MarketModel).where(MarketModel.market_address == market_address.lower())
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
class TradingService:
"""交易撮合与下单领域服务"""
def __init__(self, session: AsyncSession):
self.session = session
self.ledger_service = LedgerService(session)
async def place_order(
self,
user_address: str,
market_address: str,
outcome_index: int,
side: str,
amount: Decimal,
price: Optional[Decimal] = None
) -> OrderModel:
"""执行下单流程"""
order_id = str(uuid.uuid4())
fee = amount * Decimal("0.006") # 0.6% protocol fee
# 1. 记账并扣款/冻结
await self.ledger_service.record_trade(
address=user_address,
market_address=market_address,
order_id=order_id,
side=side,
amount=amount,
fee=fee
)
# 2. 生成订单记录
order = OrderModel(
order_id=order_id,
market_address=market_address.lower(),
user_address=user_address.lower(),
side=side,
outcome_index=outcome_index,
amount=amount,
price=price,
status="FILLED",
filled_amount=amount,
fee=fee
)
self.session.add(order)
await self.session.flush()
return order