16 KiB
16 KiB
MPC Party Service 开发指南
环境准备
系统要求
- Node.js: >= 18.x
- npm: >= 9.x
- MySQL: >= 8.0
- Redis: >= 6.x
- Docker: >= 20.x (可选,用于容器化开发)
安装步骤
1. 克隆项目
cd backend/services/mpc-service
2. 安装依赖
npm install
3. 配置环境变量
复制环境变量模板并修改:
cp .env.example .env
编辑 .env 文件:
# 应用配置
NODE_ENV=development
APP_PORT=3006
API_PREFIX=api/v1
# 数据库配置
DATABASE_URL="mysql://user:password@localhost:3306/mpc_service"
# Redis 配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
REDIS_PASSWORD=
# JWT 配置
JWT_SECRET=your-super-secret-jwt-key-at-least-32-characters
JWT_ACCESS_EXPIRES_IN=2h
JWT_REFRESH_EXPIRES_IN=7d
# MPC 配置
MPC_PARTY_ID=party-server-1
MPC_COORDINATOR_URL=http://localhost:50051
MPC_MESSAGE_ROUTER_WS_URL=ws://localhost:50052
# 加密配置
SHARE_MASTER_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
# Kafka 配置 (可选)
KAFKA_ENABLED=false
KAFKA_BROKERS=localhost:9092
KAFKA_CLIENT_ID=mpc-party-service
4. 初始化数据库
# 生成 Prisma Client
npx prisma generate
# 运行数据库迁移
npx prisma migrate dev
# (可选) 查看数据库
npx prisma studio
5. 启动开发服务器
npm run start:dev
服务将在 http://localhost:3006 启动。
开发工作流
项目脚本
{
"scripts": {
// 开发
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
// 构建
"build": "nest build",
"prebuild": "rimraf dist",
// 测试
"test": "jest --config ./tests/jest.config.js",
"test:unit": "jest --config ./tests/jest-unit.config.js",
"test:integration": "jest --config ./tests/jest-integration.config.js",
"test:e2e": "jest --config ./tests/jest-e2e.config.js",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
// 代码质量
"lint": "eslint \"{src,tests}/**/*.ts\" --fix",
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
// 数据库
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio"
}
}
开发流程
-
创建功能分支
git checkout -b feature/your-feature-name -
编写代码
- 遵循项目代码规范
- 编写相应的测试
-
运行测试
npm run test npm run lint -
提交代码
git add . git commit -m "feat: add your feature description" -
创建 Pull Request
代码规范
目录命名
- 使用 kebab-case:
party-share,mpc-party - 模块目录包含
index.ts导出
文件命名
- 实体:
party-share.entity.ts - 服务:
share-encryption.domain-service.ts - 控制器:
mpc-party.controller.ts - DTO:
participate-keygen.dto.ts - 测试:
party-share.entity.spec.ts
TypeScript 规范
// 使用接口定义数据结构
interface CreatePartyShareProps {
partyId: PartyId;
sessionId: SessionId;
shareType: PartyShareType;
shareData: ShareData;
publicKey: PublicKey;
threshold: Threshold;
}
// 使用枚举定义常量
enum PartyShareStatus {
ACTIVE = 'active',
ROTATED = 'rotated',
REVOKED = 'revoked',
}
// 使用类型别名简化复杂类型
type ShareFilters = {
partyId?: string;
status?: PartyShareStatus;
shareType?: PartyShareType;
};
// 使用 readonly 保护不可变属性
class PartyShare {
private readonly _id: ShareId;
private readonly _createdAt: Date;
}
// 使用 private 前缀
private readonly _domainEvents: DomainEvent[] = [];
// 使用 getter 暴露属性
get id(): ShareId {
return this._id;
}
错误处理
// 领域层错误
export class DomainError extends Error {
constructor(message: string) {
super(message);
this.name = 'DomainError';
}
}
// 应用层错误
export class ApplicationError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly details?: any,
) {
super(message);
this.name = 'ApplicationError';
}
}
// 使用错误
if (!share) {
throw new ApplicationError('Share not found', 'SHARE_NOT_FOUND');
}
日志规范
import { Logger } from '@nestjs/common';
@Injectable()
export class MyService {
private readonly logger = new Logger(MyService.name);
async doSomething() {
this.logger.log('Starting operation');
this.logger.debug(`Processing with params: ${JSON.stringify(params)}`);
this.logger.warn('Potential issue detected');
this.logger.error('Operation failed', error.stack);
}
}
添加新功能指南
1. 添加新的 API 端点
Step 1: 创建 DTO
// src/api/dto/request/new-feature.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class NewFeatureDto {
@ApiProperty({ description: 'Feature parameter' })
@IsString()
@IsNotEmpty()
param: string;
}
Step 2: 创建 Command/Query
// src/application/commands/new-feature/new-feature.command.ts
export class NewFeatureCommand {
constructor(
public readonly param: string,
) {}
}
Step 3: 创建 Handler
// src/application/commands/new-feature/new-feature.handler.ts
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { Injectable } from '@nestjs/common';
import { NewFeatureCommand } from './new-feature.command';
@Injectable()
@CommandHandler(NewFeatureCommand)
export class NewFeatureHandler implements ICommandHandler<NewFeatureCommand> {
async execute(command: NewFeatureCommand): Promise<any> {
// 实现业务逻辑
}
}
Step 4: 更新 Application Service
// src/application/services/mpc-party-application.service.ts
async newFeature(params: NewFeatureParams): Promise<ResultDto> {
const command = new NewFeatureCommand(params.param);
return this.commandBus.execute(command);
}
Step 5: 添加 Controller 端点
// src/api/controllers/mpc-party.controller.ts
@Post('new-feature')
@ApiOperation({ summary: '新功能' })
@ApiResponse({ status: 200, description: 'Success' })
async newFeature(@Body() dto: NewFeatureDto) {
return this.mpcPartyService.newFeature(dto);
}
Step 6: 编写测试
// tests/unit/application/new-feature.handler.spec.ts
describe('NewFeatureHandler', () => {
// ... 单元测试
});
// tests/integration/new-feature.spec.ts
describe('NewFeature (Integration)', () => {
// ... 集成测试
});
2. 添加新的领域实体
Step 1: 定义实体
// src/domain/entities/new-entity.entity.ts
import { DomainEvent } from '../events';
export class NewEntity {
private readonly _id: EntityId;
private readonly _domainEvents: DomainEvent[] = [];
private constructor(props: NewEntityProps) {
this._id = props.id;
}
static create(props: CreateNewEntityProps): NewEntity {
const entity = new NewEntity({
id: EntityId.generate(),
...props,
});
entity.addDomainEvent(new NewEntityCreatedEvent(entity.id.value));
return entity;
}
static reconstruct(props: NewEntityProps): NewEntity {
return new NewEntity(props);
}
// Getters
get id(): EntityId {
return this._id;
}
get domainEvents(): DomainEvent[] {
return [...this._domainEvents];
}
// Business methods
doSomething(): void {
// 业务逻辑
this.addDomainEvent(new SomethingDoneEvent(this._id.value));
}
// Private methods
private addDomainEvent(event: DomainEvent): void {
this._domainEvents.push(event);
}
clearDomainEvents(): void {
this._domainEvents.length = 0;
}
}
Step 2: 定义值对象
// src/domain/value-objects/entity-id.ts
export class EntityId {
private constructor(private readonly _value: string) {}
static create(value: string): EntityId {
if (!this.isValid(value)) {
throw new DomainError('Invalid EntityId format');
}
return new EntityId(value);
}
static generate(): EntityId {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 15);
return new EntityId(`entity_${timestamp}_${random}`);
}
private static isValid(value: string): boolean {
return /^entity_\d+_[a-z0-9]+$/.test(value);
}
get value(): string {
return this._value;
}
equals(other: EntityId): boolean {
return this._value === other._value;
}
}
Step 3: 定义仓储接口
// src/domain/repositories/new-entity.repository.interface.ts
import { NewEntity } from '../entities/new-entity.entity';
import { EntityId } from '../value-objects';
export const NEW_ENTITY_REPOSITORY = Symbol('NEW_ENTITY_REPOSITORY');
export interface NewEntityRepository {
save(entity: NewEntity): Promise<void>;
findById(id: EntityId): Promise<NewEntity | null>;
findMany(filters?: any): Promise<NewEntity[]>;
delete(id: EntityId): Promise<void>;
}
Step 4: 实现仓储
// src/infrastructure/persistence/repositories/new-entity.repository.impl.ts
@Injectable()
export class NewEntityRepositoryImpl implements NewEntityRepository {
constructor(
private readonly prisma: PrismaService,
private readonly mapper: NewEntityMapper,
) {}
async save(entity: NewEntity): Promise<void> {
const data = this.mapper.toPersistence(entity);
await this.prisma.newEntity.create({ data });
}
async findById(id: EntityId): Promise<NewEntity | null> {
const record = await this.prisma.newEntity.findUnique({
where: { id: id.value },
});
return record ? this.mapper.toDomain(record) : null;
}
}
3. 添加新的外部服务集成
// src/infrastructure/external/new-service/new-service.client.ts
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class NewServiceClient {
private readonly logger = new Logger(NewServiceClient.name);
private readonly baseUrl: string;
constructor(private readonly configService: ConfigService) {
this.baseUrl = this.configService.get<string>('NEW_SERVICE_URL');
}
async callExternalService(params: any): Promise<any> {
this.logger.log(`Calling external service: ${JSON.stringify(params)}`);
try {
// 实现外部服务调用
const response = await fetch(`${this.baseUrl}/endpoint`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`External service error: ${response.status}`);
}
return response.json();
} catch (error) {
this.logger.error(`External service failed: ${error.message}`, error.stack);
throw error;
}
}
}
调试技巧
1. VSCode 调试配置
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug MPC Service",
"runtimeArgs": [
"-r",
"ts-node/register",
"-r",
"tsconfig-paths/register"
],
"args": ["${workspaceFolder}/src/main.ts"],
"cwd": "${workspaceFolder}",
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true
},
{
"type": "node",
"request": "launch",
"name": "Debug Tests",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"--config",
"${workspaceFolder}/tests/jest.config.js",
"--runInBand",
"--testPathPattern",
"${file}"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
2. 日志调试
// 启用详细日志
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
});
// 在代码中添加调试日志
this.logger.debug(`Debug info: ${JSON.stringify(data)}`);
this.logger.verbose(`Verbose details: ${details}`);
3. 数据库调试
# 查看 Prisma 生成的 SQL
DEBUG=prisma:query npm run start:dev
# 使用 Prisma Studio 查看数据
npx prisma studio
4. API 调试
- 使用 Swagger UI:
http://localhost:3006/api/docs - 使用 Postman 或 Insomnia
- 查看请求/响应日志
性能优化
1. 数据库优化
// 使用索引
@@index([partyId, status])
@@index([publicKey])
// 使用 select 限制字段
const share = await this.prisma.partyShare.findUnique({
where: { id },
select: {
id: true,
status: true,
publicKey: true,
// 不选择大字段如 shareData
},
});
// 使用分页
const shares = await this.prisma.partyShare.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
});
2. 缓存优化
// 使用 Redis 缓存
@Injectable()
export class CachedShareService {
constructor(private readonly redis: Redis) {}
async getShareInfo(shareId: string): Promise<ShareInfo> {
const cacheKey = `share:${shareId}`;
// 尝试从缓存获取
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 从数据库获取
const share = await this.repository.findById(shareId);
// 存入缓存
await this.redis.setex(cacheKey, 3600, JSON.stringify(share));
return share;
}
}
3. 异步处理
// 使用异步端点处理长时间操作
@Post('keygen/participate')
@HttpCode(HttpStatus.ACCEPTED)
async participateInKeygen(@Body() dto: ParticipateKeygenDto) {
// 立即返回,后台异步处理
this.mpcPartyService.participateInKeygen(dto).catch(error => {
this.logger.error(`Keygen failed: ${error.message}`);
});
return {
message: 'Keygen participation started',
sessionId: dto.sessionId,
};
}
常见问题解决
1. Prisma 相关
问题: Prisma Client 未生成
npx prisma generate
问题: 数据库连接失败
检查 DATABASE_URL 环境变量格式:
mysql://user:password@host:port/database
2. 测试相关
问题: 测试超时
// 增加超时时间
jest.setTimeout(60000);
问题: 模拟不生效
确保模拟在正确的位置:
beforeEach(() => {
jest.clearAllMocks();
mockService.method.mockResolvedValue(expectedValue);
});
3. TypeScript 相关
问题: 路径别名不工作
检查 tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
}
}
}
问题: 类型错误
# 重新生成类型
npm run build
npx prisma generate
4. 运行时相关
问题: 端口已被占用
# 查找占用端口的进程
lsof -i :3006
# 或在 Windows 上
netstat -ano | findstr :3006
# 终止进程
kill <PID>
问题: 内存不足
# 增加 Node.js 内存限制
NODE_OPTIONS=--max-old-space-size=4096 npm run start:dev
代码审查清单
在提交 PR 前,请检查:
- 代码遵循项目规范
- 所有测试通过
- 新功能有相应的测试
- 更新了相关文档
- 没有硬编码的敏感信息
- 日志级别适当
- 错误处理完善
- 没有引入新的安全漏洞