# 钉钉模块 - 架构设计

> **版本**: v1.0  
> **最后更新**: 2026-03-17  
> **维护者**: FFOA 后端团队

---

## 架构摘要

| 字段 | 内容 |
|------|------|
| 模块 | 钉钉模块 |
| 前端入口 | `frontend/src/app/(modules)/organization/dingtalk/` |
| 后端入口 | `backend/src/modules/organization/dingtalk/` |
| 数据存储 | `platform_automation.dingtalk_*` |
| 外部依赖 | 钉钉 OAuth / HRM / Attendance / 宜搭 API |

---

## 模块分层

```text
Dingtalk UI Pages
  -> frontend/src/services/api/dingtalk.ts
  -> DingtalkController
  -> 业务服务（Insight / PlanAdmin / EmployeeManagement / Repair / Scheduler）
  -> SDK 服务（Auth / Attendance / HRM / Yida / User）
  -> Prisma / platform_automation
  -> 钉钉开放平台
```

### 前端层

- `page.tsx`
  - 同步总览页
  - 假期余额总览页
  - 年假释放计划页
  - 员工管理页
- `services/api/dingtalk.ts`
  - 针对钉钉模块的前端 API 封装

### 控制器层

- `dingtalk.controller.ts`
  - 聚合 HTTP 入口
  - 负责参数接收和路由分发
  - 不承担核心业务计算

### 业务服务层

- `DingtalkSchedulerService`
  - 定时任务与手动触发入口
  - 时间窗口规范化
  - 任务执行封装
- `EmployeeManagementService`
  - 本地员工表同步与员工状态维护
- `DingtalkRepairService`
  - 审批撤销修复
- `AnnualLeaveInsightService`
  - 假期余额快照、余额详情、释放计划查询
- `AnnualLeavePlanAdminService`
  - 计划参数读取与更新
- `sync/*`
  - 各类具体同步实现

### SDK 层

- `DingtalkAuthService`
  - 获取和缓存 Access Token
- `DingtalkAttendanceService`
  - 调用考勤、假期、审批相关接口
- `DingtalkHrmService`
  - 调用钉钉 HRM 员工与部门接口
- `DingtalkYidaService`
  - 调用宜搭相关接口
- `DingtalkUserService`
  - 调用用户相关接口

---

## 核心数据流

### 数据流 1：同步任务执行

```text
定时器 / 页面手动触发
  -> DingtalkSchedulerService.triggerTask / Cron
  -> 对应 sync service
  -> SDK 调用钉钉 / 宜搭
  -> 写 automation execution / 本地业务表
  -> 前端刷新概览和执行记录
```

### 数据流 2：员工同步

```text
EmployeeManagementService.syncEmployeesFromDingtalk
  -> HrmService 获取在职员工 ID
  -> HrmService 获取员工详情
  -> 解析字段并 upsert 到 dingtalk_employees
  -> 标记本地已不存在于钉钉的员工为 已离职
```

### 数据流 3：假期余额洞察

```text
刷新快照
  -> AnnualLeaveInsightService.refreshQuotaSnapshot
  -> 读取本地员工 / 必要时拉假期类型
  -> AttendanceService 拉余额
  -> 写 dingtalk_leave_quota_snapshots

页面查询
  -> AnnualLeaveInsightService.getQuotaOverview / getQuotaDetail
  -> 读取本地快照和释放计划
```

### 数据流 4：年假释放计划

```text
计划重算
  -> AnnualLeaveSyncService.refreshPlan
  -> 读取 dingtalk_employees.status = 正常 的员工
  -> 基于 joinDate / workStartDate / notCountDays 计算计划
  -> 写 dingtalk_annual_leave_release_plans

计划参数更新
  -> AnnualLeavePlanAdminService.updatePlanSettings
  -> 更新 adjustmentDays / notCountDays
  -> 可选触发重算（当前前端固定为 true）
```

---

## 调度设计

### 定时任务

| 任务 | Cron | 说明 |
|------|------|------|
| 员工信息同步（新表） | `01:11` 每天 | 宜搭新表同步 |
| 员工信息同步（旧表） | `02:11` 每天 | 宜搭旧表同步 |
| 本地员工表同步 | 每 2 小时整点 | 同步到 `dingtalk_employees` |
| 假期配额快照 | 每 2 小时 + 30 分 | 刷新本地快照 |
| 年假释放 | `03:11` 每天 | 读取计划并发放 |
| 出差/外勤/加班/延期 | 每小时 `:15` / `:45` | 半小时窗口同步 |
| 假期提醒 | 每周五 `00:01` | 到期提醒 |

### 时间窗口规则

- 定时触发的考勤类任务使用固定半小时窗口
- 手动触发的考勤类任务最大回溯 60 天
- 非考勤类任务不使用时间窗口

---

## 当前架构约束

1. 代码目录仍挂在 `organization` 模块下，文档已独立
2. 员工状态属于 `dingtalk_employees.status`
3. 释放计划页只展示 `正常` 员工
4. 计划设置读取无记录时返回默认值，不在读取阶段写库
