12 KiB
12 KiB
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 克隆项目
git clone <repository-url>
cd backend/services/presence-service
2.2 安装依赖
npm install
2.3 环境配置
复制环境变量模板:
cp .env.example .env.development
编辑 .env.development:
# 应用配置
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:
# 启动开发环境依赖
docker compose -f docker-compose.dev.yml up -d
# 查看服务状态
docker compose -f docker-compose.dev.yml ps
docker-compose.dev.yml 示例:
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 数据库迁移
# 生成 Prisma Client
npx prisma generate
# 同步数据库 Schema (开发环境)
npx prisma db push
# 或使用迁移 (生产环境)
npx prisma migrate dev --name init
2.6 启动服务
# 开发模式 (热重载)
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 原则
领域层规则
- 不依赖任何框架 - 领域层只使用纯 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; // 可以被外部修改
}
聚合根设计
// ✅ 好的做法 - 聚合根保护内部状态
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 设计
// 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 设计
// 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 依赖注入
// 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 错误处理
// 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 开发命令
# 启动开发服务器
npm run start:dev
# 代码检查
npm run lint
# 代码格式化
npm run format
# 类型检查
npm run type-check
5.2 数据库命令
# 生成 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 测试命令
# 运行所有测试
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 构建命令
# 构建生产版本
npm run build
# 清理构建产物
rm -rf dist/
6. 开发流程
6.1 新增功能流程
-
领域建模
- 识别聚合、实体、值对象
- 定义领域服务
- 设计仓储接口
-
编写领域层代码
- 创建值对象 (
domain/value-objects/) - 创建实体/聚合 (
domain/entities/,domain/aggregates/) - 创建领域服务 (
domain/services/) - 定义仓储接口 (
domain/repositories/)
- 创建值对象 (
-
编写应用层代码
- 创建 Command/Query (
application/commands/,application/queries/) - 创建 Handler
- 创建 Command/Query (
-
编写基础设施层代码
- 创建 Mapper (
infrastructure/persistence/mappers/) - 实现仓储 (
infrastructure/persistence/repositories/)
- 创建 Mapper (
-
编写 API 层代码
- 创建 DTO (
api/dto/) - 创建 Controller (
api/controllers/)
- 创建 DTO (
-
编写测试
- 单元测试 (领域层)
- 集成测试 (应用层)
- E2E 测试 (API 层)
6.2 代码审查清单
- 领域层是否无框架依赖?
- 值对象是否不可变?
- 聚合根是否保护了内部状态?
- 仓储接口是否定义在领域层?
- Command/Query 是否职责单一?
- 是否有充分的测试覆盖?
- 异常处理是否完善?
- 是否遵循命名规范?
7. 调试技巧
7.1 VS Code 调试配置
.vscode/launch.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 日志调试
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 调试
# 启用查询日志
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: 修复 bugrefactor: 重构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 未生成
npx prisma generate
Q: 数据库连接失败
- 检查 Docker 容器是否运行
- 检查 DATABASE_URL 是否正确
- 检查端口是否被占用
Q: Redis 连接失败
- 检查 REDIS_HOST 和 REDIS_PORT
- 检查 Redis 容器状态
- 如有密码,检查 REDIS_PASSWORD
Q: 测试失败
- 确保测试数据库已启动
- 检查环境变量配置
- 运行
npx prisma db push同步 schema