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

758 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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