9.3 KiB
9.3 KiB
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>