rwadurian/backend/services/wallet-service/docs/DEVELOPMENT.md

9.8 KiB
Raw Blame History

Wallet Service 开发指南

环境要求

  • Node.js 20.x
  • npm 10.x
  • PostgreSQL 15.x
  • Docker (用于本地数据库)
  • WSL2 (Windows 开发者)

快速开始

1. 克隆项目

git clone <repository_url>
cd wallet-service

2. 安装依赖

npm install

3. 配置环境变量

创建 .env.development 文件:

# 数据库连接
DATABASE_URL="postgresql://wallet:wallet123@localhost:5432/wallet_dev?schema=public"

# JWT 配置
JWT_SECRET="your-development-jwt-secret"

# 应用配置
NODE_ENV=development
PORT=3000

4. 启动数据库

使用 Docker 启动 PostgreSQL

docker run -d \
  --name wallet-postgres-dev \
  -e POSTGRES_USER=wallet \
  -e POSTGRES_PASSWORD=wallet123 \
  -e POSTGRES_DB=wallet_dev \
  -p 5432:5432 \
  postgres:15-alpine

5. 初始化数据库

# 生成 Prisma Client
npx prisma generate

# 推送数据库结构
npx prisma db push

# (可选) 打开 Prisma Studio 查看数据
npx prisma studio

6. 启动开发服务器

npm run start:dev

服务将在 http://localhost:3000 启动。

Swagger 文档: http://localhost:3000/api-docs


项目脚本

命令 描述
npm run start 启动生产模式
npm run start:dev 启动开发模式 (热重载)
npm run start:debug 启动调试模式
npm run build 构建项目
npm test 运行单元测试
npm run test:watch 监听模式运行测试
npm run test:cov 运行测试并生成覆盖率报告
npm run test:e2e 运行 E2E 测试
npm run lint 代码检查
npm run format 代码格式化
npm run prisma:generate 生成 Prisma Client
npm run prisma:migrate 运行数据库迁移
npm run prisma:studio 启动 Prisma Studio

代码结构

添加新功能的标准流程

1. 定义值对象 (如需要)

// src/domain/value-objects/new-value.vo.ts
export class NewValue {
  private readonly _value: number;

  private constructor(value: number) {
    this._value = value;
  }

  static create(value: number): NewValue {
    // 验证逻辑
    if (value < 0) {
      throw new DomainError('Value cannot be negative');
    }
    return new NewValue(value);
  }

  get value(): number {
    return this._value;
  }
}

2. 定义领域事件 (如需要)

// src/domain/events/new-action.event.ts
export class NewActionEvent extends DomainEvent {
  constructor(public readonly payload: {
    userId: string;
    amount: string;
  }) {
    super('NewActionEvent');
  }
}

3. 在聚合中添加业务方法

// src/domain/aggregates/wallet-account.aggregate.ts
newAction(amount: Money): void {
  this.ensureActive();
  // 业务逻辑
  this._updatedAt = new Date();
  this.addDomainEvent(new NewActionEvent({
    userId: this._userId.toString(),
    amount: amount.value.toString(),
  }));
}

4. 定义命令/查询

// src/application/commands/new-action.command.ts
export class NewActionCommand {
  constructor(
    public readonly userId: string,
    public readonly amount: number,
  ) {}
}

5. 在应用服务中实现

// src/application/services/wallet-application.service.ts
async newAction(command: NewActionCommand): Promise<void> {
  const wallet = await this.walletRepo.findByUserId(BigInt(command.userId));
  if (!wallet) {
    throw new WalletNotFoundError(`userId: ${command.userId}`);
  }
  wallet.newAction(Money.USDT(command.amount));
  await this.walletRepo.save(wallet);
  // 记录流水等...
}

6. 添加 DTO

// src/api/dto/request/new-action.dto.ts
export class NewActionDTO {
  @ApiProperty({ description: '金额' })
  @IsNumber()
  @Min(0)
  amount: number;
}

7. 添加控制器端点

// src/api/controllers/wallet.controller.ts
@Post('new-action')
@ApiOperation({ summary: '新操作' })
async newAction(
  @CurrentUser() user: CurrentUserPayload,
  @Body() dto: NewActionDTO,
): Promise<{ message: string }> {
  await this.walletService.newAction(
    new NewActionCommand(user.userId, dto.amount)
  );
  return { message: 'Success' };
}

值对象规范

Money (金额)

// 创建
const usdt = Money.USDT(100);        // 100 USDT
const bnb = Money.BNB(0.5);          // 0.5 BNB
const custom = Money.create(50, 'DST'); // 50 DST

// 运算
const sum = usdt.add(Money.USDT(50));       // 150 USDT
const diff = usdt.subtract(Money.USDT(30)); // 70 USDT

// 比较
usdt.equals(Money.USDT(100));  // true
usdt.lessThan(Money.USDT(200)); // true
usdt.isZero(); // false

// 获取值
usdt.value;     // 100 (number)
usdt.currency;  // 'USDT'

Balance (余额)

// 创建
const balance = Balance.create(
  Money.USDT(1000),  // available
  Money.USDT(100)    // frozen
);

// 操作
const afterDeposit = balance.add(Money.USDT(200));  // available + 200
const afterDeduct = balance.deduct(Money.USDT(50)); // available - 50
const afterFreeze = balance.freeze(Money.USDT(100)); // available -> frozen
const afterUnfreeze = balance.unfreeze(Money.USDT(50)); // frozen -> available

Hashpower (算力)

const hp = Hashpower.create(500);
const sum = hp.add(Hashpower.create(100)); // 600
const value = hp.value; // 500

仓储模式

接口定义

// src/domain/repositories/wallet-account.repository.interface.ts
export interface IWalletAccountRepository {
  findByUserId(userId: bigint): Promise<WalletAccount | null>;
  getOrCreate(userId: bigint): Promise<WalletAccount>;
  save(wallet: WalletAccount): Promise<WalletAccount>;
}

实现

// src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts
@Injectable()
export class WalletAccountRepositoryImpl implements IWalletAccountRepository {
  constructor(private readonly prisma: PrismaService) {}

  async findByUserId(userId: bigint): Promise<WalletAccount | null> {
    const record = await this.prisma.walletAccount.findUnique({
      where: { userId },
    });
    if (!record) return null;
    return WalletAccount.reconstruct(/* ... */);
  }
}

依赖注入

// src/infrastructure/infrastructure.module.ts
@Module({
  providers: [
    {
      provide: WALLET_ACCOUNT_REPOSITORY,
      useClass: WalletAccountRepositoryImpl,
    },
  ],
  exports: [WALLET_ACCOUNT_REPOSITORY],
})
export class InfrastructureModule {}

异常处理

领域异常

// src/shared/exceptions/domain.exception.ts
export class DomainError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'DomainError';
  }
}

export class InsufficientBalanceError extends DomainError {
  public readonly code = 'INSUFFICIENT_BALANCE';
  constructor(assetType: string, required: string, available: string) {
    super(`Insufficient ${assetType} balance: required ${required}, available ${available}`);
  }
}

异常过滤器

// src/shared/filters/domain-exception.filter.ts
@Catch(DomainError)
export class DomainExceptionFilter implements ExceptionFilter {
  catch(exception: DomainError, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = this.getHttpStatus(exception);

    response.status(status).json({
      success: false,
      code: (exception as any).code || 'DOMAIN_ERROR',
      message: exception.message,
      timestamp: new Date().toISOString(),
    });
  }
}

调试技巧

VS Code 调试配置

.vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug NestJS",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run", "start:debug"],
      "console": "integratedTerminal",
      "restart": true
    }
  ]
}

日志调试

// 在代码中添加
console.log('DEBUG:', JSON.stringify(data, null, 2));

// 使用 NestJS Logger
import { Logger } from '@nestjs/common';
const logger = new Logger('WalletService');
logger.log('Processing deposit...');
logger.debug('Wallet state:', wallet);

数据库调试

# 查看数据库
npx prisma studio

# 查看 SQL 日志 (在 prisma.service.ts 中)
this.$on('query', (e) => {
  console.log('Query:', e.query);
  console.log('Params:', e.params);
});

Git 工作流

分支命名

  • feature/xxx - 新功能
  • fix/xxx - Bug 修复
  • refactor/xxx - 重构
  • docs/xxx - 文档

提交信息格式

<type>(<scope>): <subject>

<body>

<footer>

类型:

  • feat - 新功能
  • fix - Bug 修复
  • docs - 文档
  • style - 格式
  • refactor - 重构
  • test - 测试
  • chore - 构建/工具

示例:

feat(wallet): add settle rewards feature

- Add SettleRewardsCommand
- Implement settlement logic in WalletAccount
- Add POST /settle endpoint

Closes #123

常见问题

1. Prisma Client 未生成

npx prisma generate

2. 数据库连接失败

检查 .env 文件中的 DATABASE_URL 是否正确。

3. WSL2 性能问题

参考 E2E-TESTING-WSL2.md 将项目放在 WSL2 原生文件系统中。

4. 端口被占用

# Windows
netstat -ano | findstr :3000
taskkill /PID <pid> /F

# Linux/Mac
lsof -i :3000
kill -9 <pid>