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

479 lines
9.8 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 开发指南
## 环境要求
- Node.js 20.x
- npm 10.x
- PostgreSQL 15.x
- Docker (用于本地数据库)
- WSL2 (Windows 开发者)
## 快速开始
### 1. 克隆项目
```bash
git clone <repository_url>
cd wallet-service
```
### 2. 安装依赖
```bash
npm install
```
### 3. 配置环境变量
创建 `.env.development` 文件:
```bash
# 数据库连接
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
```bash
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. 初始化数据库
```bash
# 生成 Prisma Client
npx prisma generate
# 推送数据库结构
npx prisma db push
# (可选) 打开 Prisma Studio 查看数据
npx prisma studio
```
### 6. 启动开发服务器
```bash
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. 定义值对象 (如需要)
```typescript
// 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. 定义领域事件 (如需要)
```typescript
// src/domain/events/new-action.event.ts
export class NewActionEvent extends DomainEvent {
constructor(public readonly payload: {
userId: string;
amount: string;
}) {
super('NewActionEvent');
}
}
```
#### 3. 在聚合中添加业务方法
```typescript
// 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. 定义命令/查询
```typescript
// src/application/commands/new-action.command.ts
export class NewActionCommand {
constructor(
public readonly userId: string,
public readonly amount: number,
) {}
}
```
#### 5. 在应用服务中实现
```typescript
// 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
```typescript
// src/api/dto/request/new-action.dto.ts
export class NewActionDTO {
@ApiProperty({ description: '金额' })
@IsNumber()
@Min(0)
amount: number;
}
```
#### 7. 添加控制器端点
```typescript
// 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 (金额)
```typescript
// 创建
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 (余额)
```typescript
// 创建
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 (算力)
```typescript
const hp = Hashpower.create(500);
const sum = hp.add(Hashpower.create(100)); // 600
const value = hp.value; // 500
```
---
## 仓储模式
### 接口定义
```typescript
// 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>;
}
```
### 实现
```typescript
// 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(/* ... */);
}
}
```
### 依赖注入
```typescript
// src/infrastructure/infrastructure.module.ts
@Module({
providers: [
{
provide: WALLET_ACCOUNT_REPOSITORY,
useClass: WalletAccountRepositoryImpl,
},
],
exports: [WALLET_ACCOUNT_REPOSITORY],
})
export class InfrastructureModule {}
```
---
## 异常处理
### 领域异常
```typescript
// 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}`);
}
}
```
### 异常过滤器
```typescript
// 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`:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug NestJS",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start:debug"],
"console": "integratedTerminal",
"restart": true
}
]
}
```
### 日志调试
```typescript
// 在代码中添加
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);
```
### 数据库调试
```bash
# 查看数据库
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 未生成
```bash
npx prisma generate
```
### 2. 数据库连接失败
检查 `.env` 文件中的 `DATABASE_URL` 是否正确。
### 3. WSL2 性能问题
参考 [E2E-TESTING-WSL2.md](./E2E-TESTING-WSL2.md) 将项目放在 WSL2 原生文件系统中。
### 4. 端口被占用
```bash
# Windows
netstat -ano | findstr :3000
taskkill /PID <pid> /F
# Linux/Mac
lsof -i :3000
kill -9 <pid>
```