597 lines
12 KiB
Markdown
597 lines
12 KiB
Markdown
# Presence Service 开发指南
|
||
|
||
## 1. 环境要求
|
||
|
||
### 1.1 必需软件
|
||
|
||
| 软件 | 版本 | 说明 |
|
||
|-----|------|------|
|
||
| Node.js | 20.x LTS | 运行时环境 |
|
||
| npm | 10.x | 包管理器 |
|
||
| Docker | 24.x+ | 容器运行时 |
|
||
| Docker Compose | 2.x | 容器编排 |
|
||
| PostgreSQL | 15.x | 关系数据库 |
|
||
| Redis | 7.x | 缓存数据库 |
|
||
|
||
### 1.2 推荐工具
|
||
|
||
| 工具 | 用途 |
|
||
|-----|------|
|
||
| VS Code | IDE |
|
||
| Prisma Extension | Prisma 语法高亮 |
|
||
| ESLint Extension | 代码检查 |
|
||
| Prettier Extension | 代码格式化 |
|
||
| REST Client | API 测试 |
|
||
|
||
---
|
||
|
||
## 2. 项目设置
|
||
|
||
### 2.1 克隆项目
|
||
|
||
```bash
|
||
git clone <repository-url>
|
||
cd backend/services/presence-service
|
||
```
|
||
|
||
### 2.2 安装依赖
|
||
|
||
```bash
|
||
npm install
|
||
```
|
||
|
||
### 2.3 环境配置
|
||
|
||
复制环境变量模板:
|
||
|
||
```bash
|
||
cp .env.example .env.development
|
||
```
|
||
|
||
编辑 `.env.development`:
|
||
|
||
```env
|
||
# 应用配置
|
||
NODE_ENV=development
|
||
PORT=3000
|
||
|
||
# 数据库配置
|
||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/presence_dev?schema=public
|
||
|
||
# Redis 配置
|
||
REDIS_HOST=localhost
|
||
REDIS_PORT=6379
|
||
REDIS_PASSWORD=
|
||
REDIS_DB=0
|
||
|
||
# Kafka 配置
|
||
KAFKA_BROKERS=localhost:9092
|
||
KAFKA_CLIENT_ID=presence-service-dev
|
||
|
||
# JWT 配置 (从 identity-service 获取)
|
||
JWT_SECRET=your-jwt-secret-key
|
||
```
|
||
|
||
### 2.4 启动基础设施
|
||
|
||
使用 Docker Compose 启动 PostgreSQL 和 Redis:
|
||
|
||
```bash
|
||
# 启动开发环境依赖
|
||
docker compose -f docker-compose.dev.yml up -d
|
||
|
||
# 查看服务状态
|
||
docker compose -f docker-compose.dev.yml ps
|
||
```
|
||
|
||
`docker-compose.dev.yml` 示例:
|
||
|
||
```yaml
|
||
services:
|
||
postgres:
|
||
image: postgres:15-alpine
|
||
environment:
|
||
POSTGRES_USER: postgres
|
||
POSTGRES_PASSWORD: postgres
|
||
POSTGRES_DB: presence_dev
|
||
ports:
|
||
- "5432:5432"
|
||
volumes:
|
||
- postgres-data:/var/lib/postgresql/data
|
||
|
||
redis:
|
||
image: redis:7-alpine
|
||
ports:
|
||
- "6379:6379"
|
||
|
||
volumes:
|
||
postgres-data:
|
||
```
|
||
|
||
### 2.5 数据库迁移
|
||
|
||
```bash
|
||
# 生成 Prisma Client
|
||
npx prisma generate
|
||
|
||
# 同步数据库 Schema (开发环境)
|
||
npx prisma db push
|
||
|
||
# 或使用迁移 (生产环境)
|
||
npx prisma migrate dev --name init
|
||
```
|
||
|
||
### 2.6 启动服务
|
||
|
||
```bash
|
||
# 开发模式 (热重载)
|
||
npm run start:dev
|
||
|
||
# 调试模式
|
||
npm run start:debug
|
||
|
||
# 生产模式
|
||
npm run start:prod
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 项目结构约定
|
||
|
||
### 3.1 命名规范
|
||
|
||
| 类型 | 命名规范 | 示例 |
|
||
|-----|---------|------|
|
||
| 文件名 | kebab-case | `event-log.entity.ts` |
|
||
| 类名 | PascalCase | `EventLogEntity` |
|
||
| 接口名 | PascalCase + I 前缀 | `IEventLogRepository` |
|
||
| 方法名 | camelCase | `findByTimeRange` |
|
||
| 常量 | UPPER_SNAKE_CASE | `EVENT_LOG_REPOSITORY` |
|
||
| 枚举值 | UPPER_SNAKE_CASE | `EventType.APP_SESSION_START` |
|
||
|
||
### 3.2 目录约定
|
||
|
||
```
|
||
src/
|
||
├── domain/ # 领域层 - 纯业务逻辑,无框架依赖
|
||
├── application/ # 应用层 - 用例编排,CQRS 处理器
|
||
├── infrastructure/ # 基础设施层 - 技术实现
|
||
├── api/ # API 层 - HTTP 控制器
|
||
└── shared/ # 共享模块 - 工具、过滤器、守卫
|
||
```
|
||
|
||
### 3.3 模块组织
|
||
|
||
每个功能模块遵循以下结构:
|
||
|
||
```
|
||
feature/
|
||
├── feature.module.ts # 模块定义
|
||
├── feature.controller.ts # 控制器 (可选)
|
||
├── feature.service.ts # 服务 (可选)
|
||
├── dto/ # 数据传输对象
|
||
│ ├── request/
|
||
│ └── response/
|
||
└── __tests__/ # 单元测试 (可选)
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 编码规范
|
||
|
||
### 4.1 DDD 原则
|
||
|
||
#### 领域层规则
|
||
|
||
1. **不依赖任何框架** - 领域层只使用纯 TypeScript
|
||
2. **聚合根保护内部状态** - 通过方法修改状态,不直接暴露属性
|
||
3. **值对象不可变** - 创建后不能修改
|
||
4. **领域服务无状态** - 不持有任何状态
|
||
|
||
```typescript
|
||
// ✅ 好的做法 - 值对象不可变
|
||
export class InstallId {
|
||
private constructor(private readonly _value: string) {}
|
||
|
||
static fromString(value: string): InstallId {
|
||
// 校验逻辑
|
||
return new InstallId(value);
|
||
}
|
||
|
||
get value(): string {
|
||
return this._value;
|
||
}
|
||
}
|
||
|
||
// ❌ 不好的做法 - 值对象可变
|
||
export class InstallId {
|
||
public value: string; // 可以被外部修改
|
||
}
|
||
```
|
||
|
||
#### 聚合根设计
|
||
|
||
```typescript
|
||
// ✅ 好的做法 - 聚合根保护内部状态
|
||
export class DailyActiveStats {
|
||
private _dauCount: number;
|
||
|
||
updateStats(count: number): void {
|
||
if (count < 0) {
|
||
throw new InvalidDauCountException();
|
||
}
|
||
this._dauCount = count;
|
||
this.incrementVersion();
|
||
}
|
||
|
||
get dauCount(): number {
|
||
return this._dauCount;
|
||
}
|
||
}
|
||
|
||
// ❌ 不好的做法 - 直接暴露内部状态
|
||
export class DailyActiveStats {
|
||
public dauCount: number; // 外部可以直接修改
|
||
}
|
||
```
|
||
|
||
### 4.2 CQRS 规范
|
||
|
||
#### Command 设计
|
||
|
||
```typescript
|
||
// Command 只包含执行操作所需的数据
|
||
export class RecordHeartbeatCommand {
|
||
constructor(
|
||
public readonly userId: bigint,
|
||
public readonly installId: string,
|
||
public readonly appVersion: string,
|
||
public readonly clientTs: number,
|
||
) {}
|
||
}
|
||
|
||
// Handler 负责业务编排
|
||
@CommandHandler(RecordHeartbeatCommand)
|
||
export class RecordHeartbeatHandler implements ICommandHandler<RecordHeartbeatCommand> {
|
||
async execute(command: RecordHeartbeatCommand): Promise<void> {
|
||
// 1. 验证
|
||
// 2. 执行业务逻辑
|
||
// 3. 持久化
|
||
// 4. 发布事件
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Query 设计
|
||
|
||
```typescript
|
||
// Query 只包含查询条件
|
||
export class GetOnlineCountQuery {
|
||
constructor(
|
||
public readonly windowSeconds: number = 180,
|
||
) {}
|
||
}
|
||
|
||
// Handler 返回查询结果
|
||
@QueryHandler(GetOnlineCountQuery)
|
||
export class GetOnlineCountHandler implements IQueryHandler<GetOnlineCountQuery, OnlineCountResult> {
|
||
async execute(query: GetOnlineCountQuery): Promise<OnlineCountResult> {
|
||
// 直接读取数据,不修改状态
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.3 依赖注入
|
||
|
||
```typescript
|
||
// 1. 定义接口和 Token (domain/repositories/)
|
||
export const EVENT_LOG_REPOSITORY = Symbol('EVENT_LOG_REPOSITORY');
|
||
export interface IEventLogRepository {
|
||
insert(log: EventLog): Promise<EventLog>;
|
||
}
|
||
|
||
// 2. 实现接口 (infrastructure/persistence/repositories/)
|
||
@Injectable()
|
||
export class EventLogRepositoryImpl implements IEventLogRepository {
|
||
async insert(log: EventLog): Promise<EventLog> {
|
||
// 具体实现
|
||
}
|
||
}
|
||
|
||
// 3. 配置绑定 (infrastructure/infrastructure.module.ts)
|
||
@Module({
|
||
providers: [
|
||
{
|
||
provide: EVENT_LOG_REPOSITORY,
|
||
useClass: EventLogRepositoryImpl,
|
||
},
|
||
],
|
||
})
|
||
|
||
// 4. 使用 (application/commands/)
|
||
@CommandHandler(SomeCommand)
|
||
export class SomeHandler {
|
||
constructor(
|
||
@Inject(EVENT_LOG_REPOSITORY)
|
||
private readonly repo: IEventLogRepository,
|
||
) {}
|
||
}
|
||
```
|
||
|
||
### 4.4 错误处理
|
||
|
||
```typescript
|
||
// 1. 定义领域异常 (domain/exceptions/)
|
||
export class InvalidInstallIdException extends DomainException {
|
||
constructor(value: string) {
|
||
super(`Invalid install ID: ${value}`);
|
||
}
|
||
|
||
get code(): string {
|
||
return 'INVALID_INSTALL_ID';
|
||
}
|
||
}
|
||
|
||
// 2. 在值对象中使用
|
||
export class InstallId {
|
||
static fromString(value: string): InstallId {
|
||
if (!this.isValid(value)) {
|
||
throw new InvalidInstallIdException(value);
|
||
}
|
||
return new InstallId(value);
|
||
}
|
||
}
|
||
|
||
// 3. GlobalExceptionFilter 自动处理
|
||
// DomainException -> 400 Bad Request
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 常用命令
|
||
|
||
### 5.1 开发命令
|
||
|
||
```bash
|
||
# 启动开发服务器
|
||
npm run start:dev
|
||
|
||
# 代码检查
|
||
npm run lint
|
||
|
||
# 代码格式化
|
||
npm run format
|
||
|
||
# 类型检查
|
||
npm run type-check
|
||
```
|
||
|
||
### 5.2 数据库命令
|
||
|
||
```bash
|
||
# 生成 Prisma Client
|
||
npx prisma generate
|
||
|
||
# 同步 Schema (开发)
|
||
npx prisma db push
|
||
|
||
# 创建迁移
|
||
npx prisma migrate dev --name <migration-name>
|
||
|
||
# 应用迁移 (生产)
|
||
npx prisma migrate deploy
|
||
|
||
# 打开 Prisma Studio
|
||
npx prisma studio
|
||
|
||
# 重置数据库
|
||
npx prisma migrate reset
|
||
```
|
||
|
||
### 5.3 测试命令
|
||
|
||
```bash
|
||
# 运行所有测试
|
||
npm test
|
||
|
||
# 运行单元测试
|
||
npm run test:unit
|
||
|
||
# 运行集成测试
|
||
npm run test:integration
|
||
|
||
# 运行 E2E 测试
|
||
npm run test:e2e
|
||
|
||
# 生成覆盖率报告
|
||
npm run test:cov
|
||
|
||
# 监视模式
|
||
npm run test:watch
|
||
```
|
||
|
||
### 5.4 构建命令
|
||
|
||
```bash
|
||
# 构建生产版本
|
||
npm run build
|
||
|
||
# 清理构建产物
|
||
rm -rf dist/
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 开发流程
|
||
|
||
### 6.1 新增功能流程
|
||
|
||
1. **领域建模**
|
||
- 识别聚合、实体、值对象
|
||
- 定义领域服务
|
||
- 设计仓储接口
|
||
|
||
2. **编写领域层代码**
|
||
- 创建值对象 (`domain/value-objects/`)
|
||
- 创建实体/聚合 (`domain/entities/`, `domain/aggregates/`)
|
||
- 创建领域服务 (`domain/services/`)
|
||
- 定义仓储接口 (`domain/repositories/`)
|
||
|
||
3. **编写应用层代码**
|
||
- 创建 Command/Query (`application/commands/`, `application/queries/`)
|
||
- 创建 Handler
|
||
|
||
4. **编写基础设施层代码**
|
||
- 创建 Mapper (`infrastructure/persistence/mappers/`)
|
||
- 实现仓储 (`infrastructure/persistence/repositories/`)
|
||
|
||
5. **编写 API 层代码**
|
||
- 创建 DTO (`api/dto/`)
|
||
- 创建 Controller (`api/controllers/`)
|
||
|
||
6. **编写测试**
|
||
- 单元测试 (领域层)
|
||
- 集成测试 (应用层)
|
||
- E2E 测试 (API 层)
|
||
|
||
### 6.2 代码审查清单
|
||
|
||
- [ ] 领域层是否无框架依赖?
|
||
- [ ] 值对象是否不可变?
|
||
- [ ] 聚合根是否保护了内部状态?
|
||
- [ ] 仓储接口是否定义在领域层?
|
||
- [ ] Command/Query 是否职责单一?
|
||
- [ ] 是否有充分的测试覆盖?
|
||
- [ ] 异常处理是否完善?
|
||
- [ ] 是否遵循命名规范?
|
||
|
||
---
|
||
|
||
## 7. 调试技巧
|
||
|
||
### 7.1 VS Code 调试配置
|
||
|
||
`.vscode/launch.json`:
|
||
|
||
```json
|
||
{
|
||
"version": "0.2.0",
|
||
"configurations": [
|
||
{
|
||
"type": "node",
|
||
"request": "launch",
|
||
"name": "Debug NestJS",
|
||
"runtimeExecutable": "npm",
|
||
"runtimeArgs": ["run", "start:debug"],
|
||
"console": "integratedTerminal",
|
||
"restart": true,
|
||
"autoAttachChildProcesses": true
|
||
},
|
||
{
|
||
"type": "node",
|
||
"request": "launch",
|
||
"name": "Debug Jest Tests",
|
||
"runtimeExecutable": "npm",
|
||
"runtimeArgs": ["run", "test:debug"],
|
||
"console": "integratedTerminal"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 7.2 日志调试
|
||
|
||
```typescript
|
||
import { Logger } from '@nestjs/common';
|
||
|
||
const logger = new Logger('MyService');
|
||
|
||
logger.log('Info message');
|
||
logger.warn('Warning message');
|
||
logger.error('Error message', error.stack);
|
||
logger.debug('Debug message'); // 需要设置 LOG_LEVEL=debug
|
||
```
|
||
|
||
### 7.3 Prisma 调试
|
||
|
||
```bash
|
||
# 启用查询日志
|
||
DEBUG="prisma:query" npm run start:dev
|
||
|
||
# 或在 schema.prisma 中配置
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
previewFeatures = ["tracing"]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Git 工作流
|
||
|
||
### 8.1 分支命名
|
||
|
||
| 类型 | 格式 | 示例 |
|
||
|-----|------|------|
|
||
| 功能 | `feature/<description>` | `feature/add-online-history-api` |
|
||
| 修复 | `fix/<description>` | `fix/heartbeat-validation` |
|
||
| 重构 | `refactor/<description>` | `refactor/redis-repository` |
|
||
| 文档 | `docs/<description>` | `docs/api-documentation` |
|
||
|
||
### 8.2 提交信息格式
|
||
|
||
```
|
||
<type>(<scope>): <description>
|
||
|
||
[optional body]
|
||
|
||
[optional footer]
|
||
```
|
||
|
||
类型:
|
||
- `feat`: 新功能
|
||
- `fix`: 修复 bug
|
||
- `refactor`: 重构
|
||
- `docs`: 文档
|
||
- `test`: 测试
|
||
- `chore`: 构建/工具
|
||
|
||
示例:
|
||
```
|
||
feat(presence): add online history query API
|
||
|
||
- Add GetOnlineHistoryQuery and handler
|
||
- Add OnlineHistoryResponseDto
|
||
- Add endpoint GET /api/v1/presence/online-history
|
||
|
||
Closes #123
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 常见问题
|
||
|
||
### Q: Prisma Client 未生成
|
||
|
||
```bash
|
||
npx prisma generate
|
||
```
|
||
|
||
### Q: 数据库连接失败
|
||
|
||
1. 检查 Docker 容器是否运行
|
||
2. 检查 DATABASE_URL 是否正确
|
||
3. 检查端口是否被占用
|
||
|
||
### Q: Redis 连接失败
|
||
|
||
1. 检查 REDIS_HOST 和 REDIS_PORT
|
||
2. 检查 Redis 容器状态
|
||
3. 如有密码,检查 REDIS_PASSWORD
|
||
|
||
### Q: 测试失败
|
||
|
||
1. 确保测试数据库已启动
|
||
2. 检查环境变量配置
|
||
3. 运行 `npx prisma db push` 同步 schema
|