# ScheduleModule 使用规范

> **核心原则**: `ScheduleModule.forRoot()` 只能在 `AppModule` 中调用一次

---

## 📋 问题背景

### 历史 Bug 案例

**问题**: 2025-12-16 周一早上，用户收到了 4 封相同的工时提醒邮件

**根本原因**: `ScheduleModule.forRoot()` 被调用了 4 次，导致同一个定时任务被注册了 4 次

```typescript
// ❌ 错误做法：在多个模块中调用 forRoot()
@Module({
  imports: [ScheduleModule.forRoot()],  // work-record.module.ts
})
@Module({
  imports: [ScheduleModule.forRoot()],  // overtime.module.ts
})
@Module({
  imports: [ScheduleModule.forRoot()],  // audit.module.ts
})
@Module({
  imports: [ScheduleModule.forRoot()],  // logging.module.ts
})

// 结果：创建了 4 个调度器，每个任务执行 4 次 🐛
```

**数据证据**:
- 93 个用户都收到了正好 4 封邮件
- 所有邮件在同一秒内发送（并发特征）
- 时间间隔：1ms, 1ms, 79ms（典型的并发执行）

---

## ✅ 正确用法

### 规范 1: 只在 AppModule 调用一次

```typescript
// backend/src/app.module.ts

import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';

@Module({
  imports: [
    ScheduleModule.forRoot(),  // ✅ 只在这里调用一次
    LoggingModule,
    AuditModule,
    // ... 其他模块
  ],
})
export class AppModule {}
```

### 规范 2: 其他模块不要导入 ScheduleModule

```typescript
// backend/src/modules/logging/logging.module.ts

import { Module } from '@nestjs/common';
// ❌ 不要导入 ScheduleModule

@Module({
  // ❌ 不要在 imports 中添加 ScheduleModule.forRoot()
  providers: [
    LogCleanupService,  // ✅ 直接使用 @Cron 装饰器即可
  ],
})
export class LoggingModule {}
```

### 规范 3: Service 中正常使用 @Cron 装饰器

```typescript
// backend/src/modules/logging/services/log-cleanup.service.ts

import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class LogCleanupService {
  @Cron(CronExpression.EVERY_DAY_AT_3AM)  // ✅ 自动被识别
  async cleanupLogs() {
    // 清理逻辑
  }
}
```

---

## 🔍 工作原理

### forRoot() 的作用

```typescript
ScheduleModule.forRoot()  // 做了什么？
  ↓
创建 SchedulerRegistry 实例
  ↓
扫描整个应用，查找所有 @Cron、@Interval、@Timeout 装饰器
  ↓
注册并启动所有定时任务
```

### 多次调用的后果

```
第1次 forRoot(): 创建调度器 #1 → 注册所有任务
第2次 forRoot(): 创建调度器 #2 → 再次注册所有任务
第3次 forRoot(): 创建调度器 #3 → 又注册一遍
第4次 forRoot(): 创建调度器 #4 → 继续注册

结果：每个任务被注册 4 次 = 执行 4 次 🐛
```

---

## 🚨 常见错误

### 错误 1: 在功能模块中调用 forRoot()

```typescript
// ❌ 错误
@Module({
  imports: [
    ScheduleModule.forRoot(),  // ❌ 不要这样做
  ],
})
export class WorkRecordModule {}
```

**后果**: 每个调用 forRoot() 的模块都会创建一个新的调度器

### 错误 2: 误以为"每个模块调用一次"

```typescript
// ❌ 错误理解
// "每个模块只调用一次 forRoot()"

// ✅ 正确理解
// "整个应用只调用一次 forRoot()"
```

### 错误 3: 认为需要在使用 @Cron 的模块中导入

```typescript
// ❌ 不需要
@Module({
  imports: [ScheduleModule.forRoot()],  // ❌ 不需要
  providers: [MySchedulerService],       // ✅ 直接提供即可
})

// ✅ 正确
@Module({
  providers: [MySchedulerService],  // ✅ @Cron 会自动被识别
})
```

---

## 📊 检查清单

在添加或修改模块时，请检查：

- [ ] ✅ `ScheduleModule.forRoot()` 只在 `AppModule` 中调用
- [ ] ✅ 其他模块**不导入** `ScheduleModule`
- [ ] ✅ 使用 `@Cron`、`@Interval`、`@Timeout` 的 Service 正常提供
- [ ] ✅ 没有在多个地方调用 `forRoot()`

---

## 🔧 如何检查现有代码

### 方法 1: 搜索所有 forRoot() 调用

```bash
cd backend
grep -r "ScheduleModule.forRoot" src/
```

**预期结果**: 只有一条记录，指向 `app.module.ts`

**如果发现多个**: 立即修复！移除除 AppModule 外的所有调用

### 方法 2: 检查 imports 语句

```bash
cd backend
grep -r "import.*ScheduleModule" src/
```

查看每个导入 ScheduleModule 的文件，确认它们：
- 如果是 `app.module.ts`: ✅ 应该调用 `forRoot()`
- 如果是其他文件: ❌ 不应该调用 `forRoot()`

---

## 📚 相关文档

- [NestJS Task Scheduling 官方文档](https://docs.nestjs.com/techniques/task-scheduling)
- [定时任务开发指南](./CRON_JOBS_GUIDE.md)
- [模块开发规范](./MODULE_DEVELOPMENT.md)

---

## 🎯 总结

| 规范 | 说明 |
|------|------|
| **核心原则** | `ScheduleModule.forRoot()` 只在 `AppModule` 调用一次 |
| **其他模块** | 不需要导入 `ScheduleModule` |
| **Service 使用** | 直接使用 `@Cron`、`@Interval`、`@Timeout` 装饰器 |
| **调度器数量** | 应该只有 1 个 `SchedulerRegistry` 实例 |
| **任务注册** | 每个任务只注册 1 次 |

---

**最后更新**: 2025-12-16  
**版本**: v1.0  
**维护者**: FFOA 开发团队  

---

> ⚠️ **重要提醒**: 如果发现多个 `forRoot()` 调用，会导致定时任务重复执行！请立即修复。

