12 KiB
12 KiB
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: 如何添加新的外部服务依赖?
- 在
src/domain/services/中定义接口 (防腐层) - 在
src/infrastructure/external/中实现客户端 - 在模块中注册依赖注入
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 */ });