# MPC Party Service 开发指南 ## 环境准备 ### 系统要求 - **Node.js**: >= 18.x - **npm**: >= 9.x - **MySQL**: >= 8.0 - **Redis**: >= 6.x - **Docker**: >= 20.x (可选,用于容器化开发) ### 安装步骤 #### 1. 克隆项目 ```bash cd backend/services/mpc-service ``` #### 2. 安装依赖 ```bash npm install ``` #### 3. 配置环境变量 复制环境变量模板并修改: ```bash cp .env.example .env ``` 编辑 `.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. 初始化数据库 ```bash # 生成 Prisma Client npx prisma generate # 运行数据库迁移 npx prisma migrate dev # (可选) 查看数据库 npx prisma studio ``` #### 5. 启动开发服务器 ```bash npm run start:dev ``` 服务将在 `http://localhost:3006` 启动。 --- ## 开发工作流 ### 项目脚本 ```json { "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" } } ``` ### 开发流程 1. **创建功能分支** ```bash git checkout -b feature/your-feature-name ``` 2. **编写代码** - 遵循项目代码规范 - 编写相应的测试 3. **运行测试** ```bash npm run test npm run lint ``` 4. **提交代码** ```bash git add . git commit -m "feat: add your feature description" ``` 5. **创建 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 规范 ```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; } ``` ### 错误处理 ```typescript // 领域层错误 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'); } ``` ### 日志规范 ```typescript 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 ```typescript // 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 ```typescript // src/application/commands/new-feature/new-feature.command.ts export class NewFeatureCommand { constructor( public readonly param: string, ) {} } ``` #### Step 3: 创建 Handler ```typescript // 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 { async execute(command: NewFeatureCommand): Promise { // 实现业务逻辑 } } ``` #### Step 4: 更新 Application Service ```typescript // src/application/services/mpc-party-application.service.ts async newFeature(params: NewFeatureParams): Promise { const command = new NewFeatureCommand(params.param); return this.commandBus.execute(command); } ``` #### Step 5: 添加 Controller 端点 ```typescript // 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: 编写测试 ```typescript // tests/unit/application/new-feature.handler.spec.ts describe('NewFeatureHandler', () => { // ... 单元测试 }); // tests/integration/new-feature.spec.ts describe('NewFeature (Integration)', () => { // ... 集成测试 }); ``` ### 2. 添加新的领域实体 #### Step 1: 定义实体 ```typescript // 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: 定义值对象 ```typescript // 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: 定义仓储接口 ```typescript // 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; findById(id: EntityId): Promise; findMany(filters?: any): Promise; delete(id: EntityId): Promise; } ``` #### Step 4: 实现仓储 ```typescript // 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 { const data = this.mapper.toPersistence(entity); await this.prisma.newEntity.create({ data }); } async findById(id: EntityId): Promise { const record = await this.prisma.newEntity.findUnique({ where: { id: id.value }, }); return record ? this.mapper.toDomain(record) : null; } } ``` ### 3. 添加新的外部服务集成 ```typescript // 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('NEW_SERVICE_URL'); } async callExternalService(params: any): Promise { 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`: ```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. 日志调试 ```typescript // 启用详细日志 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. 数据库调试 ```bash # 查看 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. 数据库优化 ```typescript // 使用索引 @@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. 缓存优化 ```typescript // 使用 Redis 缓存 @Injectable() export class CachedShareService { constructor(private readonly redis: Redis) {} async getShareInfo(shareId: string): Promise { 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. 异步处理 ```typescript // 使用异步端点处理长时间操作 @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 未生成 ```bash npx prisma generate ``` **问题**: 数据库连接失败 检查 `DATABASE_URL` 环境变量格式: ``` mysql://user:password@host:port/database ``` ### 2. 测试相关 **问题**: 测试超时 ```javascript // 增加超时时间 jest.setTimeout(60000); ``` **问题**: 模拟不生效 确保模拟在正确的位置: ```typescript beforeEach(() => { jest.clearAllMocks(); mockService.method.mockResolvedValue(expectedValue); }); ``` ### 3. TypeScript 相关 **问题**: 路径别名不工作 检查 `tsconfig.json`: ```json { "compilerOptions": { "paths": { "@/*": ["src/*"] } } } ``` **问题**: 类型错误 ```bash # 重新生成类型 npm run build npx prisma generate ``` ### 4. 运行时相关 **问题**: 端口已被占用 ```bash # 查找占用端口的进程 lsof -i :3006 # 或在 Windows 上 netstat -ano | findstr :3006 # 终止进程 kill ``` **问题**: 内存不足 ```bash # 增加 Node.js 内存限制 NODE_OPTIONS=--max-old-space-size=4096 npm run start:dev ``` --- ## 代码审查清单 在提交 PR 前,请检查: - [ ] 代码遵循项目规范 - [ ] 所有测试通过 - [ ] 新功能有相应的测试 - [ ] 更新了相关文档 - [ ] 没有硬编码的敏感信息 - [ ] 日志级别适当 - [ ] 错误处理完善 - [ ] 没有引入新的安全漏洞