# 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
