20 KiB
20 KiB
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 {}
关键业务规则 (不变式)
- 余额不能为负: 任何币种的可用余额不能为负数
- 流水表只能追加: 账本流水表只能 INSERT,不能 UPDATE/DELETE
- 每笔流水必须有余额快照: 每笔流水必须记录操作后的余额快照
- 结算金额不能超过可结算余额: 结算金额必须 ≤ 可结算余额
API 端点汇总
| 方法 | 路径 | 描述 | 认证 |
|---|---|---|---|
| GET | /wallet/my-wallet | 查询我的钱包 | 需要 |
| GET | /wallet/ledger/my-ledger | 查询我的流水 | 需要 |
| POST | /wallet/deposit | 充值入账 (内部) | 服务间 |
| POST | /wallet/withdraw | 提现申请 | 需要 |
| POST | /wallet/settle | 结算收益 | 需要 |
开发顺序建议
- 项目初始化和 Prisma Schema
- 值对象实现
- 聚合根实现
- 仓储接口定义
- 仓储实现
- 领域服务实现
- 应用服务实现
- DTO 和控制器实现
- 模块配置和测试
注意事项
- 所有金额使用
Decimal(20, 8)存储,避免浮点数精度问题 - 流水表是 append-only,不允许更新或删除
- 每次余额变动都要创建对应的流水记录
- 使用事务确保余额和流水的一致性
- 参考 identity-service 的代码风格和命名规范