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

545 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Reward Service 开发指南
## 环境准备
### 系统要求
- **Node.js**: >= 20.x LTS
- **npm**: >= 10.x
- **Docker**: >= 24.x (用于本地开发环境)
- **Git**: >= 2.x
### 开发环境设置
#### 1. 克隆项目
```bash
git clone <repository-url>
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<void> {
// 实现持久化逻辑
}
}
```
---
## 添加新功能
### 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<Entity | null>;
save(entity: Entity): Promise<void>;
}
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<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,
});
}
}
```
### 注册依赖
```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 <migration-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": ["<node_internals>/**"]
},
{
"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 */ });
```