479 lines
9.8 KiB
Markdown
479 lines
9.8 KiB
Markdown
# 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>
|
||
```
|