rwadurian/backend/services/wallet-service/DEVELOPMENT_GUIDE.md

20 KiB
Raw Blame History

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 项目

cd backend/services
npx @nestjs/cli new wallet-service --skip-git --package-manager npm
cd wallet-service

1.2 安装依赖

npm install @nestjs/config @prisma/client class-validator class-transformer uuid
npm install -D prisma @types/uuid

1.3 配置环境变量

创建 .env.development:

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwadurian_wallet?schema=public"
NODE_ENV=development
PORT=3002

创建 .env.example:

DATABASE_URL="postgresql://user:password@host:5432/database?schema=public"
NODE_ENV=development
PORT=3002

第二阶段:数据库设计 (Prisma Schema)

2.1 创建 prisma/schema.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 初始化数据库

npx prisma migrate dev --name init
npx prisma generate

第三阶段:领域层实现

3.1 值对象 (Value Objects)

3.1.1 asset-type.enum.ts

export enum AssetType {
  USDT = 'USDT',
  DST = 'DST',
  BNB = 'BNB',
  OG = 'OG',
  RWAD = 'RWAD',
  HASHPOWER = 'HASHPOWER',
}

3.1.2 chain-type.enum.ts

export enum ChainType {
  KAVA = 'KAVA',
  DST = 'DST',
  BSC = 'BSC',
}

3.1.3 wallet-status.enum.ts

export enum WalletStatus {
  ACTIVE = 'ACTIVE',
  FROZEN = 'FROZEN',
  CLOSED = 'CLOSED',
}

3.1.4 ledger-entry-type.enum.ts

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

export enum DepositStatus {
  PENDING = 'PENDING',
  CONFIRMED = 'CONFIRMED',
  FAILED = 'FAILED',
}

3.1.6 settlement-status.enum.ts

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

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

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

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

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

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)

// 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 定义

// 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 控制器实现

// 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

@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 的代码风格和命名规范