545 lines
12 KiB
Markdown
545 lines
12 KiB
Markdown
# 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 */ });
|
||
```
|