# Reward Service 开发指南 ## 环境准备 ### 系统要求 - **Node.js**: >= 20.x LTS - **npm**: >= 10.x - **Docker**: >= 24.x (用于本地开发环境) - **Git**: >= 2.x ### 开发环境设置 #### 1. 克隆项目 ```bash git clone cd backend/services/reward-service ``` #### 2. 安装依赖 ```bash npm install ``` #### 3. 配置环境变量 创建 `.env` 文件: ```bash cp .env.example .env ``` 编辑 `.env` 文件: ```env # 应用配置 NODE_ENV=development PORT=3000 # 数据库配置 DATABASE_URL="postgresql://postgres:password@localhost:5432/reward_db" # Redis配置 REDIS_HOST=localhost REDIS_PORT=6379 # Kafka配置 KAFKA_BROKERS=localhost:9092 KAFKA_CLIENT_ID=reward-service KAFKA_GROUP_ID=reward-service-group # JWT配置 JWT_SECRET=your-jwt-secret-key # 外部服务配置 REFERRAL_SERVICE_URL=http://localhost:3001 AUTHORIZATION_SERVICE_URL=http://localhost:3002 WALLET_SERVICE_URL=http://localhost:3003 ``` #### 4. 启动基础设施 使用 Docker Compose 启动 PostgreSQL、Redis 和 Kafka: ```bash docker compose -f docker-compose.test.yml up -d ``` #### 5. 数据库迁移 ```bash # 生成 Prisma Client npx prisma generate # 运行数据库迁移 npx prisma migrate dev ``` #### 6. 启动开发服务器 ```bash # 开发模式 (热重载) npm run start:dev # 调试模式 npm run start:debug ``` --- ## 项目结构 ``` reward-service/ ├── src/ │ ├── api/ # API层 │ │ ├── controllers/ # 控制器 │ │ ├── dto/ # 数据传输对象 │ │ │ ├── request/ # 请求DTO │ │ │ └── response/ # 响应DTO │ │ └── api.module.ts │ │ │ ├── application/ # 应用层 │ │ ├── services/ # 应用服务 │ │ ├── schedulers/ # 定时任务 │ │ └── application.module.ts │ │ │ ├── domain/ # 领域层 (核心) │ │ ├── aggregates/ # 聚合根 │ │ ├── value-objects/ # 值对象 │ │ ├── events/ # 领域事件 │ │ ├── services/ # 领域服务 │ │ ├── repositories/ # 仓储接口 │ │ └── domain.module.ts │ │ │ ├── infrastructure/ # 基础设施层 │ │ ├── persistence/ # 持久化 │ │ │ ├── prisma/ # Prisma配置 │ │ │ ├── repositories/ # 仓储实现 │ │ │ └── mappers/ # 对象映射 │ │ ├── external/ # 外部服务客户端 │ │ ├── kafka/ # Kafka集成 │ │ ├── redis/ # Redis集成 │ │ └── infrastructure.module.ts │ │ │ ├── shared/ # 共享模块 │ │ ├── guards/ # 守卫 │ │ └── strategies/ # 认证策略 │ │ │ ├── config/ # 配置 │ ├── app.module.ts # 根模块 │ └── main.ts # 入口文件 │ ├── test/ # 测试 │ ├── integration/ # 集成测试 │ └── app.e2e-spec.ts # E2E测试 │ ├── prisma/ # Prisma配置 │ └── schema.prisma │ ├── docs/ # 文档 ├── Makefile # Make命令 └── docker-compose.test.yml # Docker配置 ``` --- ## 开发规范 ### 代码风格 项目使用 ESLint 和 Prettier 进行代码规范检查: ```bash # 运行 ESLint 检查并自动修复 npm run lint # 运行 Prettier 格式化 npm run format ``` ### 命名规范 | 类型 | 规范 | 示例 | |------|------|------| | 文件名 | kebab-case | `reward-ledger-entry.aggregate.ts` | | 类名 | PascalCase | `RewardLedgerEntry` | | 接口名 | I + PascalCase | `IRewardLedgerEntryRepository` | | 方法名 | camelCase | `calculateRewards` | | 常量 | UPPER_SNAKE_CASE | `HEADQUARTERS_COMMUNITY_USER_ID` | | 枚举值 | UPPER_SNAKE_CASE | `SHARE_RIGHT` | ### DDD 分层规范 #### 领域层 (Domain) 领域层是系统核心,**不依赖任何其他层**。 ```typescript // ✅ 正确:领域层只使用领域概念 import { Money } from '../value-objects/money.vo'; import { RewardStatus } from '../value-objects/reward-status.enum'; // ❌ 错误:领域层不应依赖基础设施 import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; ``` #### 应用层 (Application) 应用层协调领域层和基础设施层,实现用例。 ```typescript @Injectable() export class RewardApplicationService { constructor( // 注入领域服务 private readonly rewardCalculationService: RewardCalculationService, // 通过接口注入仓储 @Inject(REWARD_LEDGER_ENTRY_REPOSITORY) private readonly repository: IRewardLedgerEntryRepository, ) {} } ``` #### 基础设施层 (Infrastructure) 基础设施层实现领域层定义的接口。 ```typescript // 仓储实现 @Injectable() export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryRepository { constructor(private readonly prisma: PrismaService) {} async save(entry: RewardLedgerEntry): Promise { // 实现持久化逻辑 } } ``` --- ## 添加新功能 ### 1. 添加新的值对象 ```typescript // src/domain/value-objects/new-value.vo.ts export class NewValue { private readonly _value: number; private constructor(value: number) { if (value < 0) { throw new Error('Value must be non-negative'); } this._value = value; } static create(value: number): NewValue { return new NewValue(value); } get value(): number { return this._value; } equals(other: NewValue): boolean { return this._value === other._value; } } ``` ### 2. 添加新的领域事件 ```typescript // src/domain/events/new.event.ts import { DomainEvent } from './domain-event.base'; export class NewEvent extends DomainEvent { constructor( public readonly data: { id: string; userId: string; // ... 其他字段 }, ) { super('NewEvent'); } } ``` ### 3. 添加新的聚合根方法 ```typescript // 在聚合根中添加新行为 export class RewardLedgerEntry { // ... 现有代码 /** * 新的领域行为 */ newBehavior(): void { // 1. 检查不变式 if (!this.canPerformNewBehavior()) { throw new Error('Cannot perform behavior in current state'); } // 2. 修改状态 this._someField = newValue; // 3. 发布领域事件 this._domainEvents.push(new NewEvent({ id: this._id?.toString() || '', userId: this._userId.toString(), })); } private canPerformNewBehavior(): boolean { // 业务规则检查 return true; } } ``` ### 4. 添加新的 API 端点 ```typescript // src/api/controllers/new.controller.ts @ApiTags('New') @Controller('new') @UseGuards(JwtAuthGuard) @ApiBearerAuth() export class NewController { constructor(private readonly service: RewardApplicationService) {} @Get() @ApiOperation({ summary: '新接口描述' }) @ApiResponse({ status: 200, description: '成功' }) async newEndpoint(@Request() req) { const userId = BigInt(req.user.sub); return this.service.newMethod(userId); } } ``` --- ## 依赖注入 ### 定义仓储接口 ```typescript // src/domain/repositories/new.repository.interface.ts export interface INewRepository { findById(id: bigint): Promise; save(entity: Entity): Promise; } export const NEW_REPOSITORY = Symbol('INewRepository'); ``` ### 实现仓储 ```typescript // src/infrastructure/persistence/repositories/new.repository.impl.ts @Injectable() export class NewRepositoryImpl implements INewRepository { constructor(private readonly prisma: PrismaService) {} async findById(id: bigint): Promise { const data = await this.prisma.entity.findUnique({ where: { id } }); return data ? EntityMapper.toDomain(data) : null; } async save(entity: Entity): Promise { const data = EntityMapper.toPersistence(entity); await this.prisma.entity.upsert({ where: { id: data.id }, create: data, update: data, }); } } ``` ### 注册依赖 ```typescript // src/infrastructure/infrastructure.module.ts @Module({ providers: [ { provide: NEW_REPOSITORY, useClass: NewRepositoryImpl, }, ], exports: [NEW_REPOSITORY], }) export class InfrastructureModule {} ``` --- ## 常用命令 ### 开发命令 ```bash # 启动开发服务器 (热重载) npm run start:dev # 启动调试模式 npm run start:debug # 构建生产版本 npm run build # 启动生产服务器 npm run start:prod ``` ### 数据库命令 ```bash # 生成 Prisma Client npx prisma generate # 创建新迁移 npx prisma migrate dev --name # 应用迁移 npx prisma migrate deploy # 重置数据库 npx prisma migrate reset --force # 打开 Prisma Studio npx prisma studio ``` ### 代码质量 ```bash # ESLint 检查 npm run lint # 代码格式化 npm run format ``` ### 测试命令 ```bash # 运行所有测试 npm test # 运行单元测试 make test-unit # 运行集成测试 make test-integration # 运行 E2E 测试 make test-e2e # 测试覆盖率 npm run test:cov ``` --- ## 调试技巧 ### VS Code 调试配置 创建 `.vscode/launch.json`: ```json { "version": "0.2.0", "configurations": [ { "type": "node", "request": "attach", "name": "Attach NestJS", "port": 9229, "restart": true, "skipFiles": ["/**"] }, { "type": "node", "request": "launch", "name": "Debug Jest Tests", "program": "${workspaceFolder}/node_modules/.bin/jest", "args": ["--runInBand", "--watchAll=false"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen" } ] } ``` ### 日志调试 ```typescript import { Logger } from '@nestjs/common'; @Injectable() export class SomeService { private readonly logger = new Logger(SomeService.name); async someMethod() { this.logger.log('Processing started'); this.logger.debug('Debug information', { data }); this.logger.warn('Warning message'); this.logger.error('Error occurred', error.stack); } } ``` --- ## 常见问题 ### Q: 如何处理 BigInt 序列化问题? ```typescript // 在 JSON 序列化时转换为字符串 return { id: entity.id?.toString(), userId: entity.userId.toString(), }; ``` ### Q: 如何添加新的外部服务依赖? 1. 在 `src/domain/services/` 中定义接口 (防腐层) 2. 在 `src/infrastructure/external/` 中实现客户端 3. 在模块中注册依赖注入 ### Q: 如何处理数据库事务? ```typescript await this.prisma.$transaction(async (tx) => { await tx.rewardLedgerEntry.create({ data: entry1 }); await tx.rewardLedgerEntry.create({ data: entry2 }); await tx.rewardSummary.update({ where: { userId }, data: summary }); }); ``` ### Q: 如何测试私有方法? 不要直接测试私有方法。通过公共接口测试私有方法的行为: ```typescript // ❌ 错误:直接测试私有方法 expect(service['privateMethod']()).toBe(expected); // ✅ 正确:通过公共接口测试 const result = await service.publicMethod(); expect(result).toMatchObject({ /* expected behavior */ }); ```