rwadurian/backend/services/reward-service/docs/DEVELOPMENT.md

12 KiB
Raw Blame History

Reward Service 开发指南

环境准备

系统要求

  • Node.js: >= 20.x LTS
  • npm: >= 10.x
  • Docker: >= 24.x (用于本地开发环境)
  • Git: >= 2.x

开发环境设置

1. 克隆项目

git clone <repository-url>
cd backend/services/reward-service

2. 安装依赖

npm install

3. 配置环境变量

创建 .env 文件:

cp .env.example .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

docker compose -f docker-compose.test.yml up -d

5. 数据库迁移

# 生成 Prisma Client
npx prisma generate

# 运行数据库迁移
npx prisma migrate dev

6. 启动开发服务器

# 开发模式 (热重载)
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 进行代码规范检查:

# 运行 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)

领域层是系统核心,不依赖任何其他层

// ✅ 正确:领域层只使用领域概念
import { Money } from '../value-objects/money.vo';
import { RewardStatus } from '../value-objects/reward-status.enum';

// ❌ 错误:领域层不应依赖基础设施
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';

应用层 (Application)

应用层协调领域层和基础设施层,实现用例。

@Injectable()
export class RewardApplicationService {
  constructor(
    // 注入领域服务
    private readonly rewardCalculationService: RewardCalculationService,
    // 通过接口注入仓储
    @Inject(REWARD_LEDGER_ENTRY_REPOSITORY)
    private readonly repository: IRewardLedgerEntryRepository,
  ) {}
}

基础设施层 (Infrastructure)

基础设施层实现领域层定义的接口。

// 仓储实现
@Injectable()
export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryRepository {
  constructor(private readonly prisma: PrismaService) {}

  async save(entry: RewardLedgerEntry): Promise<void> {
    // 实现持久化逻辑
  }
}

添加新功能

1. 添加新的值对象

// 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. 添加新的领域事件

// 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. 添加新的聚合根方法

// 在聚合根中添加新行为
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 端点

// 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);
  }
}

依赖注入

定义仓储接口

// src/domain/repositories/new.repository.interface.ts
export interface INewRepository {
  findById(id: bigint): Promise<Entity | null>;
  save(entity: Entity): Promise<void>;
}

export const NEW_REPOSITORY = Symbol('INewRepository');

实现仓储

// src/infrastructure/persistence/repositories/new.repository.impl.ts
@Injectable()
export class NewRepositoryImpl implements INewRepository {
  constructor(private readonly prisma: PrismaService) {}

  async findById(id: bigint): Promise<Entity | null> {
    const data = await this.prisma.entity.findUnique({ where: { id } });
    return data ? EntityMapper.toDomain(data) : null;
  }

  async save(entity: Entity): Promise<void> {
    const data = EntityMapper.toPersistence(entity);
    await this.prisma.entity.upsert({
      where: { id: data.id },
      create: data,
      update: data,
    });
  }
}

注册依赖

// src/infrastructure/infrastructure.module.ts
@Module({
  providers: [
    {
      provide: NEW_REPOSITORY,
      useClass: NewRepositoryImpl,
    },
  ],
  exports: [NEW_REPOSITORY],
})
export class InfrastructureModule {}

常用命令

开发命令

# 启动开发服务器 (热重载)
npm run start:dev

# 启动调试模式
npm run start:debug

# 构建生产版本
npm run build

# 启动生产服务器
npm run start:prod

数据库命令

# 生成 Prisma Client
npx prisma generate

# 创建新迁移
npx prisma migrate dev --name <migration-name>

# 应用迁移
npx prisma migrate deploy

# 重置数据库
npx prisma migrate reset --force

# 打开 Prisma Studio
npx prisma studio

代码质量

# ESLint 检查
npm run lint

# 代码格式化
npm run format

测试命令

# 运行所有测试
npm test

# 运行单元测试
make test-unit

# 运行集成测试
make test-integration

# 运行 E2E 测试
make test-e2e

# 测试覆盖率
npm run test:cov

调试技巧

VS Code 调试配置

创建 .vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Attach NestJS",
      "port": 9229,
      "restart": true,
      "skipFiles": ["<node_internals>/**"]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Jest Tests",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": ["--runInBand", "--watchAll=false"],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen"
    }
  ]
}

日志调试

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 序列化问题?

// 在 JSON 序列化时转换为字符串
return {
  id: entity.id?.toString(),
  userId: entity.userId.toString(),
};

Q: 如何添加新的外部服务依赖?

  1. src/domain/services/ 中定义接口 (防腐层)
  2. src/infrastructure/external/ 中实现客户端
  3. 在模块中注册依赖注入

Q: 如何处理数据库事务?

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: 如何测试私有方法?

不要直接测试私有方法。通过公共接口测试私有方法的行为:

// ❌ 错误:直接测试私有方法
expect(service['privateMethod']()).toBe(expected);

// ✅ 正确:通过公共接口测试
const result = await service.publicMethod();
expect(result).toMatchObject({ /* expected behavior */ });