# 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; findById(walletId: bigint): Promise; findByUserId(userId: bigint): Promise; getOrCreate(userId: bigint): Promise; findByUserIds(userIds: bigint[]): Promise>; } export const WALLET_ACCOUNT_REPOSITORY = Symbol('IWalletAccountRepository'); ``` #### 3.3.2 ledger-entry.repository.interface.ts ```typescript export interface ILedgerEntryRepository { save(entry: LedgerEntry): Promise; saveAll(entries: LedgerEntry[]): Promise; findByUserId(userId: bigint, filters?: LedgerFilters, pagination?: Pagination): Promise; findByRefOrderId(refOrderId: string): Promise; } 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 { 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 { 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 的代码风格和命名规范