758 lines
20 KiB
Markdown
758 lines
20 KiB
Markdown
# Wallet Service 开发指导
|
||
|
||
## 项目概述
|
||
|
||
Wallet Service 是 RWA 榴莲女皇平台的钱包账本微服务,负责管理用户的平台内部余额、充值入账、提现、资金流水记账等功能。
|
||
|
||
## 技术栈
|
||
|
||
- **框架**: NestJS
|
||
- **数据库**: PostgreSQL + Prisma ORM
|
||
- **架构**: DDD + Hexagonal Architecture (六边形架构)
|
||
- **语言**: TypeScript
|
||
|
||
## 架构参考
|
||
|
||
请参考 `identity-service` 的架构模式,保持一致性:
|
||
|
||
```
|
||
wallet-service/
|
||
├── prisma/
|
||
│ └── schema.prisma # 数据库模型
|
||
├── src/
|
||
│ ├── api/ # Presentation Layer (API层)
|
||
│ │ ├── controllers/ # HTTP 控制器
|
||
│ │ │ ├── wallet.controller.ts
|
||
│ │ │ ├── ledger.controller.ts
|
||
│ │ │ ├── deposit.controller.ts
|
||
│ │ │ └── settlement.controller.ts
|
||
│ │ ├── dto/ # 数据传输对象
|
||
│ │ │ ├── wallet.dto.ts
|
||
│ │ │ ├── ledger.dto.ts
|
||
│ │ │ ├── deposit.dto.ts
|
||
│ │ │ └── settlement.dto.ts
|
||
│ │ └── api.module.ts
|
||
│ │
|
||
│ ├── application/ # Application Layer (应用层)
|
||
│ │ ├── commands/ # 命令对象
|
||
│ │ │ ├── handle-deposit.command.ts
|
||
│ │ │ ├── deduct-for-planting.command.ts
|
||
│ │ │ ├── allocate-funds.command.ts
|
||
│ │ │ ├── add-rewards.command.ts
|
||
│ │ │ ├── settle-rewards.command.ts
|
||
│ │ │ └── withdraw.command.ts
|
||
│ │ ├── queries/ # 查询对象
|
||
│ │ │ ├── get-my-wallet.query.ts
|
||
│ │ │ └── get-my-ledger.query.ts
|
||
│ │ └── services/
|
||
│ │ └── wallet-application.service.ts
|
||
│ │
|
||
│ ├── domain/ # Domain Layer (领域层)
|
||
│ │ ├── aggregates/ # 聚合根
|
||
│ │ │ ├── wallet-account.aggregate.ts
|
||
│ │ │ ├── ledger-entry.aggregate.ts
|
||
│ │ │ ├── deposit-order.aggregate.ts
|
||
│ │ │ └── settlement-order.aggregate.ts
|
||
│ │ ├── value-objects/ # 值对象
|
||
│ │ │ ├── wallet-id.vo.ts
|
||
│ │ │ ├── money.vo.ts
|
||
│ │ │ ├── balance.vo.ts
|
||
│ │ │ ├── wallet-balances.vo.ts
|
||
│ │ │ ├── wallet-rewards.vo.ts
|
||
│ │ │ ├── hashpower.vo.ts
|
||
│ │ │ ├── asset-type.enum.ts
|
||
│ │ │ ├── chain-type.enum.ts
|
||
│ │ │ ├── wallet-status.enum.ts
|
||
│ │ │ ├── ledger-entry-type.enum.ts
|
||
│ │ │ ├── deposit-status.enum.ts
|
||
│ │ │ └── settlement-status.enum.ts
|
||
│ │ ├── events/ # 领域事件
|
||
│ │ │ ├── deposit-completed.event.ts
|
||
│ │ │ ├── withdrawal-requested.event.ts
|
||
│ │ │ ├── reward-moved-to-settleable.event.ts
|
||
│ │ │ ├── reward-expired.event.ts
|
||
│ │ │ └── settlement-completed.event.ts
|
||
│ │ ├── repositories/ # 仓储接口 (Port)
|
||
│ │ │ ├── wallet-account.repository.interface.ts
|
||
│ │ │ ├── ledger-entry.repository.interface.ts
|
||
│ │ │ ├── deposit-order.repository.interface.ts
|
||
│ │ │ └── settlement-order.repository.interface.ts
|
||
│ │ └── services/ # 领域服务
|
||
│ │ └── wallet-ledger.service.ts
|
||
│ │
|
||
│ ├── infrastructure/ # Infrastructure Layer (基础设施层)
|
||
│ │ ├── persistence/ # 持久化实现 (Adapter)
|
||
│ │ │ ├── entities/ # Prisma 实体映射
|
||
│ │ │ ├── mappers/ # 领域模型与实体的映射
|
||
│ │ │ └── repositories/ # 仓储实现
|
||
│ │ └── infrastructure.module.ts
|
||
│ │
|
||
│ ├── app.module.ts
|
||
│ └── main.ts
|
||
├── .env.development
|
||
├── .env.example
|
||
├── package.json
|
||
└── tsconfig.json
|
||
```
|
||
|
||
---
|
||
|
||
## 第一阶段:项目初始化
|
||
|
||
### 1.1 创建 NestJS 项目
|
||
|
||
```bash
|
||
cd backend/services
|
||
npx @nestjs/cli new wallet-service --skip-git --package-manager npm
|
||
cd wallet-service
|
||
```
|
||
|
||
### 1.2 安装依赖
|
||
|
||
```bash
|
||
npm install @nestjs/config @prisma/client class-validator class-transformer uuid
|
||
npm install -D prisma @types/uuid
|
||
```
|
||
|
||
### 1.3 配置环境变量
|
||
|
||
创建 `.env.development`:
|
||
```env
|
||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwadurian_wallet?schema=public"
|
||
NODE_ENV=development
|
||
PORT=3002
|
||
```
|
||
|
||
创建 `.env.example`:
|
||
```env
|
||
DATABASE_URL="postgresql://user:password@host:5432/database?schema=public"
|
||
NODE_ENV=development
|
||
PORT=3002
|
||
```
|
||
|
||
---
|
||
|
||
## 第二阶段:数据库设计 (Prisma Schema)
|
||
|
||
### 2.1 创建 prisma/schema.prisma
|
||
|
||
```prisma
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
// ============================================
|
||
// 钱包账户表 (状态表)
|
||
// ============================================
|
||
model WalletAccount {
|
||
id BigInt @id @default(autoincrement()) @map("wallet_id")
|
||
userId BigInt @unique @map("user_id")
|
||
|
||
// USDT 余额
|
||
usdtAvailable Decimal @default(0) @map("usdt_available") @db.Decimal(20, 8)
|
||
usdtFrozen Decimal @default(0) @map("usdt_frozen") @db.Decimal(20, 8)
|
||
|
||
// DST 余额
|
||
dstAvailable Decimal @default(0) @map("dst_available") @db.Decimal(20, 8)
|
||
dstFrozen Decimal @default(0) @map("dst_frozen") @db.Decimal(20, 8)
|
||
|
||
// BNB 余额
|
||
bnbAvailable Decimal @default(0) @map("bnb_available") @db.Decimal(20, 8)
|
||
bnbFrozen Decimal @default(0) @map("bnb_frozen") @db.Decimal(20, 8)
|
||
|
||
// OG 余额
|
||
ogAvailable Decimal @default(0) @map("og_available") @db.Decimal(20, 8)
|
||
ogFrozen Decimal @default(0) @map("og_frozen") @db.Decimal(20, 8)
|
||
|
||
// RWAD 余额
|
||
rwadAvailable Decimal @default(0) @map("rwad_available") @db.Decimal(20, 8)
|
||
rwadFrozen Decimal @default(0) @map("rwad_frozen") @db.Decimal(20, 8)
|
||
|
||
// 算力
|
||
hashpower Decimal @default(0) @map("hashpower") @db.Decimal(20, 8)
|
||
|
||
// 待领取收益
|
||
pendingUsdt Decimal @default(0) @map("pending_usdt") @db.Decimal(20, 8)
|
||
pendingHashpower Decimal @default(0) @map("pending_hashpower") @db.Decimal(20, 8)
|
||
pendingExpireAt DateTime? @map("pending_expire_at")
|
||
|
||
// 可结算收益
|
||
settleableUsdt Decimal @default(0) @map("settleable_usdt") @db.Decimal(20, 8)
|
||
settleableHashpower Decimal @default(0) @map("settleable_hashpower") @db.Decimal(20, 8)
|
||
|
||
// 已结算总额
|
||
settledTotalUsdt Decimal @default(0) @map("settled_total_usdt") @db.Decimal(20, 8)
|
||
settledTotalHashpower Decimal @default(0) @map("settled_total_hashpower") @db.Decimal(20, 8)
|
||
|
||
// 已过期总额
|
||
expiredTotalUsdt Decimal @default(0) @map("expired_total_usdt") @db.Decimal(20, 8)
|
||
expiredTotalHashpower Decimal @default(0) @map("expired_total_hashpower") @db.Decimal(20, 8)
|
||
|
||
// 状态
|
||
status String @default("ACTIVE") @map("status") @db.VarChar(20)
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
@@map("wallet_accounts")
|
||
@@index([userId])
|
||
@@index([usdtAvailable(sort: Desc)])
|
||
@@index([hashpower(sort: Desc)])
|
||
@@index([status])
|
||
}
|
||
|
||
// ============================================
|
||
// 账本流水表 (行为表, append-only)
|
||
// ============================================
|
||
model LedgerEntry {
|
||
id BigInt @id @default(autoincrement()) @map("entry_id")
|
||
userId BigInt @map("user_id")
|
||
|
||
// 流水类型
|
||
entryType String @map("entry_type") @db.VarChar(50)
|
||
|
||
// 金额变动 (正数入账, 负数支出)
|
||
amount Decimal @map("amount") @db.Decimal(20, 8)
|
||
assetType String @map("asset_type") @db.VarChar(20)
|
||
|
||
// 余额快照 (操作后余额)
|
||
balanceAfter Decimal? @map("balance_after") @db.Decimal(20, 8)
|
||
|
||
// 关联引用
|
||
refOrderId String? @map("ref_order_id") @db.VarChar(100)
|
||
refTxHash String? @map("ref_tx_hash") @db.VarChar(100)
|
||
|
||
// 备注
|
||
memo String? @map("memo") @db.VarChar(500)
|
||
|
||
// 扩展数据
|
||
payloadJson Json? @map("payload_json")
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
@@map("wallet_ledger_entries")
|
||
@@index([userId, createdAt(sort: Desc)])
|
||
@@index([entryType])
|
||
@@index([assetType])
|
||
@@index([refOrderId])
|
||
@@index([refTxHash])
|
||
@@index([createdAt])
|
||
}
|
||
|
||
// ============================================
|
||
// 充值订单表
|
||
// ============================================
|
||
model DepositOrder {
|
||
id BigInt @id @default(autoincrement()) @map("order_id")
|
||
userId BigInt @map("user_id")
|
||
|
||
// 充值信息
|
||
chainType String @map("chain_type") @db.VarChar(20)
|
||
amount Decimal @map("amount") @db.Decimal(20, 8)
|
||
txHash String @unique @map("tx_hash") @db.VarChar(100)
|
||
|
||
// 状态
|
||
status String @default("PENDING") @map("status") @db.VarChar(20)
|
||
confirmedAt DateTime? @map("confirmed_at")
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
@@map("deposit_orders")
|
||
@@index([userId])
|
||
@@index([txHash])
|
||
@@index([status])
|
||
@@index([chainType])
|
||
}
|
||
|
||
// ============================================
|
||
// 结算订单表
|
||
// ============================================
|
||
model SettlementOrder {
|
||
id BigInt @id @default(autoincrement()) @map("order_id")
|
||
userId BigInt @map("user_id")
|
||
|
||
// 结算信息
|
||
usdtAmount Decimal @map("usdt_amount") @db.Decimal(20, 8)
|
||
settleCurrency String @map("settle_currency") @db.VarChar(10)
|
||
|
||
// SWAP 信息
|
||
swapTxHash String? @map("swap_tx_hash") @db.VarChar(100)
|
||
receivedAmount Decimal? @map("received_amount") @db.Decimal(20, 8)
|
||
|
||
// 状态
|
||
status String @default("PENDING") @map("status") @db.VarChar(20)
|
||
settledAt DateTime? @map("settled_at")
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
@@map("settlement_orders")
|
||
@@index([userId])
|
||
@@index([status])
|
||
@@index([settleCurrency])
|
||
@@index([createdAt])
|
||
}
|
||
```
|
||
|
||
### 2.2 初始化数据库
|
||
|
||
```bash
|
||
npx prisma migrate dev --name init
|
||
npx prisma generate
|
||
```
|
||
|
||
---
|
||
|
||
## 第三阶段:领域层实现
|
||
|
||
### 3.1 值对象 (Value Objects)
|
||
|
||
#### 3.1.1 asset-type.enum.ts
|
||
```typescript
|
||
export enum AssetType {
|
||
USDT = 'USDT',
|
||
DST = 'DST',
|
||
BNB = 'BNB',
|
||
OG = 'OG',
|
||
RWAD = 'RWAD',
|
||
HASHPOWER = 'HASHPOWER',
|
||
}
|
||
```
|
||
|
||
#### 3.1.2 chain-type.enum.ts
|
||
```typescript
|
||
export enum ChainType {
|
||
KAVA = 'KAVA',
|
||
DST = 'DST',
|
||
BSC = 'BSC',
|
||
}
|
||
```
|
||
|
||
#### 3.1.3 wallet-status.enum.ts
|
||
```typescript
|
||
export enum WalletStatus {
|
||
ACTIVE = 'ACTIVE',
|
||
FROZEN = 'FROZEN',
|
||
CLOSED = 'CLOSED',
|
||
}
|
||
```
|
||
|
||
#### 3.1.4 ledger-entry-type.enum.ts
|
||
```typescript
|
||
export enum LedgerEntryType {
|
||
DEPOSIT_KAVA = 'DEPOSIT_KAVA',
|
||
DEPOSIT_BSC = 'DEPOSIT_BSC',
|
||
PLANT_PAYMENT = 'PLANT_PAYMENT',
|
||
REWARD_PENDING = 'REWARD_PENDING',
|
||
REWARD_TO_SETTLEABLE = 'REWARD_TO_SETTLEABLE',
|
||
REWARD_EXPIRED = 'REWARD_EXPIRED',
|
||
REWARD_SETTLED = 'REWARD_SETTLED',
|
||
TRANSFER_TO_POOL = 'TRANSFER_TO_POOL',
|
||
SWAP_EXECUTED = 'SWAP_EXECUTED',
|
||
WITHDRAWAL = 'WITHDRAWAL',
|
||
TRANSFER_IN = 'TRANSFER_IN',
|
||
TRANSFER_OUT = 'TRANSFER_OUT',
|
||
FREEZE = 'FREEZE',
|
||
UNFREEZE = 'UNFREEZE',
|
||
}
|
||
```
|
||
|
||
#### 3.1.5 deposit-status.enum.ts
|
||
```typescript
|
||
export enum DepositStatus {
|
||
PENDING = 'PENDING',
|
||
CONFIRMED = 'CONFIRMED',
|
||
FAILED = 'FAILED',
|
||
}
|
||
```
|
||
|
||
#### 3.1.6 settlement-status.enum.ts
|
||
```typescript
|
||
export enum SettlementStatus {
|
||
PENDING = 'PENDING',
|
||
SWAPPING = 'SWAPPING',
|
||
COMPLETED = 'COMPLETED',
|
||
FAILED = 'FAILED',
|
||
}
|
||
|
||
export enum SettleCurrency {
|
||
BNB = 'BNB',
|
||
OG = 'OG',
|
||
USDT = 'USDT',
|
||
DST = 'DST',
|
||
}
|
||
```
|
||
|
||
#### 3.1.7 money.vo.ts
|
||
```typescript
|
||
export class Money {
|
||
private constructor(
|
||
public readonly amount: number,
|
||
public readonly currency: string,
|
||
) {
|
||
if (amount < 0) {
|
||
throw new Error('Money amount cannot be negative');
|
||
}
|
||
}
|
||
|
||
static create(amount: number, currency: string): Money {
|
||
return new Money(amount, currency);
|
||
}
|
||
|
||
static USDT(amount: number): Money {
|
||
return new Money(amount, 'USDT');
|
||
}
|
||
|
||
static zero(currency: string = 'USDT'): Money {
|
||
return new Money(0, currency);
|
||
}
|
||
|
||
add(other: Money): Money {
|
||
this.ensureSameCurrency(other);
|
||
return new Money(this.amount + other.amount, this.currency);
|
||
}
|
||
|
||
subtract(other: Money): Money {
|
||
this.ensureSameCurrency(other);
|
||
if (this.amount < other.amount) {
|
||
throw new Error('Insufficient balance');
|
||
}
|
||
return new Money(this.amount - other.amount, this.currency);
|
||
}
|
||
|
||
lessThan(other: Money): boolean {
|
||
this.ensureSameCurrency(other);
|
||
return this.amount < other.amount;
|
||
}
|
||
|
||
greaterThan(other: Money): boolean {
|
||
this.ensureSameCurrency(other);
|
||
return this.amount > other.amount;
|
||
}
|
||
|
||
isZero(): boolean {
|
||
return this.amount === 0;
|
||
}
|
||
|
||
private ensureSameCurrency(other: Money): void {
|
||
if (this.currency !== other.currency) {
|
||
throw new Error('Currency mismatch');
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3.1.8 balance.vo.ts
|
||
```typescript
|
||
import { Money } from './money.vo';
|
||
|
||
export class Balance {
|
||
constructor(
|
||
public readonly available: Money,
|
||
public readonly frozen: Money,
|
||
) {}
|
||
|
||
static zero(): Balance {
|
||
return new Balance(Money.zero(), Money.zero());
|
||
}
|
||
|
||
add(amount: Money): Balance {
|
||
return new Balance(this.available.add(amount), this.frozen);
|
||
}
|
||
|
||
deduct(amount: Money): Balance {
|
||
if (this.available.lessThan(amount)) {
|
||
throw new Error('Insufficient available balance');
|
||
}
|
||
return new Balance(this.available.subtract(amount), this.frozen);
|
||
}
|
||
|
||
freeze(amount: Money): Balance {
|
||
if (this.available.lessThan(amount)) {
|
||
throw new Error('Insufficient available balance to freeze');
|
||
}
|
||
return new Balance(
|
||
this.available.subtract(amount),
|
||
this.frozen.add(amount),
|
||
);
|
||
}
|
||
|
||
unfreeze(amount: Money): Balance {
|
||
if (this.frozen.lessThan(amount)) {
|
||
throw new Error('Insufficient frozen balance to unfreeze');
|
||
}
|
||
return new Balance(
|
||
this.available.add(amount),
|
||
this.frozen.subtract(amount),
|
||
);
|
||
}
|
||
|
||
get total(): Money {
|
||
return this.available.add(this.frozen);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3.1.9 hashpower.vo.ts
|
||
```typescript
|
||
export class Hashpower {
|
||
private constructor(public readonly value: number) {
|
||
if (value < 0) {
|
||
throw new Error('Hashpower cannot be negative');
|
||
}
|
||
}
|
||
|
||
static create(value: number): Hashpower {
|
||
return new Hashpower(value);
|
||
}
|
||
|
||
static zero(): Hashpower {
|
||
return new Hashpower(0);
|
||
}
|
||
|
||
add(other: Hashpower): Hashpower {
|
||
return new Hashpower(this.value + other.value);
|
||
}
|
||
|
||
subtract(other: Hashpower): Hashpower {
|
||
if (this.value < other.value) {
|
||
throw new Error('Insufficient hashpower');
|
||
}
|
||
return new Hashpower(this.value - other.value);
|
||
}
|
||
|
||
isZero(): boolean {
|
||
return this.value === 0;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.2 聚合根 (Aggregates)
|
||
|
||
#### 3.2.1 wallet-account.aggregate.ts
|
||
|
||
实现 `WalletAccount` 聚合根,包含:
|
||
- 余额管理 (USDT/DST/BNB/OG/RWAD/算力)
|
||
- 收益汇总 (待领取/可结算/已结算/过期)
|
||
- 核心领域行为:deposit, deduct, freeze, unfreeze, addPendingReward, movePendingToSettleable, settleReward, withdraw
|
||
|
||
#### 3.2.2 ledger-entry.aggregate.ts
|
||
|
||
实现 `LedgerEntry` 聚合根 (append-only),包含:
|
||
- 流水类型
|
||
- 金额变动
|
||
- 余额快照
|
||
- 关联引用
|
||
|
||
### 3.3 仓储接口 (Repository Interfaces)
|
||
|
||
#### 3.3.1 wallet-account.repository.interface.ts
|
||
```typescript
|
||
export interface IWalletAccountRepository {
|
||
save(wallet: WalletAccount): Promise<void>;
|
||
findById(walletId: bigint): Promise<WalletAccount | null>;
|
||
findByUserId(userId: bigint): Promise<WalletAccount | null>;
|
||
getOrCreate(userId: bigint): Promise<WalletAccount>;
|
||
findByUserIds(userIds: bigint[]): Promise<Map<string, WalletAccount>>;
|
||
}
|
||
|
||
export const WALLET_ACCOUNT_REPOSITORY = Symbol('IWalletAccountRepository');
|
||
```
|
||
|
||
#### 3.3.2 ledger-entry.repository.interface.ts
|
||
```typescript
|
||
export interface ILedgerEntryRepository {
|
||
save(entry: LedgerEntry): Promise<void>;
|
||
saveAll(entries: LedgerEntry[]): Promise<void>;
|
||
findByUserId(userId: bigint, filters?: LedgerFilters, pagination?: Pagination): Promise<LedgerEntry[]>;
|
||
findByRefOrderId(refOrderId: string): Promise<LedgerEntry[]>;
|
||
}
|
||
|
||
export const LEDGER_ENTRY_REPOSITORY = Symbol('ILedgerEntryRepository');
|
||
```
|
||
|
||
---
|
||
|
||
## 第四阶段:基础设施层实现
|
||
|
||
### 4.1 实体映射 (Entities)
|
||
|
||
创建 Prisma 实体到领域模型的映射类。
|
||
|
||
### 4.2 仓储实现 (Repository Implementations)
|
||
|
||
实现所有仓储接口,使用 Prisma Client 进行数据库操作。
|
||
|
||
---
|
||
|
||
## 第五阶段:应用层实现
|
||
|
||
### 5.1 命令对象 (Commands)
|
||
|
||
```typescript
|
||
// handle-deposit.command.ts
|
||
export class HandleDepositCommand {
|
||
constructor(
|
||
public readonly userId: string,
|
||
public readonly amount: number,
|
||
public readonly chainType: ChainType,
|
||
public readonly txHash: string,
|
||
) {}
|
||
}
|
||
|
||
// deduct-for-planting.command.ts
|
||
export class DeductForPlantingCommand {
|
||
constructor(
|
||
public readonly userId: string,
|
||
public readonly amount: number,
|
||
public readonly orderId: string,
|
||
) {}
|
||
}
|
||
```
|
||
|
||
### 5.2 应用服务 (Application Service)
|
||
|
||
实现 `WalletApplicationService`,包含所有用例:
|
||
- handleDeposit - 处理充值
|
||
- deductForPlanting - 认种扣款
|
||
- allocateFunds - 资金分配
|
||
- addRewards - 增加奖励
|
||
- movePendingToSettleable - 待领取→可结算
|
||
- settleRewards - 结算收益
|
||
- getMyWallet - 查询我的钱包
|
||
- getMyLedger - 查询我的流水
|
||
|
||
---
|
||
|
||
## 第六阶段:API层实现
|
||
|
||
### 6.1 DTO 定义
|
||
|
||
```typescript
|
||
// wallet.dto.ts
|
||
export class WalletDTO {
|
||
walletId: string;
|
||
userId: string;
|
||
balances: BalancesDTO;
|
||
rewards: RewardsDTO;
|
||
status: string;
|
||
}
|
||
|
||
export class BalancesDTO {
|
||
usdt: BalanceDTO;
|
||
dst: BalanceDTO;
|
||
bnb: BalanceDTO;
|
||
og: BalanceDTO;
|
||
rwad: BalanceDTO;
|
||
hashpower: number;
|
||
}
|
||
|
||
export class BalanceDTO {
|
||
available: number;
|
||
frozen: number;
|
||
}
|
||
```
|
||
|
||
### 6.2 控制器实现
|
||
|
||
```typescript
|
||
// wallet.controller.ts
|
||
@Controller('wallet')
|
||
export class WalletController {
|
||
constructor(private readonly walletService: WalletApplicationService) {}
|
||
|
||
@Get('my-wallet')
|
||
@UseGuards(JwtAuthGuard)
|
||
async getMyWallet(@CurrentUser() user: User): Promise<WalletDTO> {
|
||
return this.walletService.getMyWallet({ userId: user.id });
|
||
}
|
||
}
|
||
|
||
// ledger.controller.ts
|
||
@Controller('wallet/ledger')
|
||
export class LedgerController {
|
||
constructor(private readonly walletService: WalletApplicationService) {}
|
||
|
||
@Get('my-ledger')
|
||
@UseGuards(JwtAuthGuard)
|
||
async getMyLedger(
|
||
@CurrentUser() user: User,
|
||
@Query() query: GetMyLedgerQueryDTO,
|
||
): Promise<LedgerEntryDTO[]> {
|
||
return this.walletService.getMyLedger({
|
||
userId: user.id,
|
||
...query,
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 第七阶段:模块配置
|
||
|
||
### 7.1 app.module.ts
|
||
|
||
```typescript
|
||
@Module({
|
||
imports: [
|
||
ConfigModule.forRoot({
|
||
isGlobal: true,
|
||
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
|
||
}),
|
||
ApiModule,
|
||
InfrastructureModule,
|
||
],
|
||
})
|
||
export class AppModule {}
|
||
```
|
||
|
||
---
|
||
|
||
## 关键业务规则 (不变式)
|
||
|
||
1. **余额不能为负**: 任何币种的可用余额不能为负数
|
||
2. **流水表只能追加**: 账本流水表只能 INSERT,不能 UPDATE/DELETE
|
||
3. **每笔流水必须有余额快照**: 每笔流水必须记录操作后的余额快照
|
||
4. **结算金额不能超过可结算余额**: 结算金额必须 ≤ 可结算余额
|
||
|
||
---
|
||
|
||
## API 端点汇总
|
||
|
||
| 方法 | 路径 | 描述 | 认证 |
|
||
|------|------|------|------|
|
||
| GET | /wallet/my-wallet | 查询我的钱包 | 需要 |
|
||
| GET | /wallet/ledger/my-ledger | 查询我的流水 | 需要 |
|
||
| POST | /wallet/deposit | 充值入账 (内部) | 服务间 |
|
||
| POST | /wallet/withdraw | 提现申请 | 需要 |
|
||
| POST | /wallet/settle | 结算收益 | 需要 |
|
||
|
||
---
|
||
|
||
## 开发顺序建议
|
||
|
||
1. 项目初始化和 Prisma Schema
|
||
2. 值对象实现
|
||
3. 聚合根实现
|
||
4. 仓储接口定义
|
||
5. 仓储实现
|
||
6. 领域服务实现
|
||
7. 应用服务实现
|
||
8. DTO 和控制器实现
|
||
9. 模块配置和测试
|
||
|
||
---
|
||
|
||
## 注意事项
|
||
|
||
1. 所有金额使用 `Decimal(20, 8)` 存储,避免浮点数精度问题
|
||
2. 流水表是 append-only,不允许更新或删除
|
||
3. 每次余额变动都要创建对应的流水记录
|
||
4. 使用事务确保余额和流水的一致性
|
||
5. 参考 identity-service 的代码风格和命名规范
|