# 钉钉模块 - API 文档

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

---

## API 摘要

### 最小必填项

- Base URL: `/api/v1/organization/dingtalk`
- 认证: Bearer Token (JWT)
- 统一封装: 成功响应由 `TransformInterceptor` 统一包装

### 统一约定

- 成功响应: `{ "success": true, "data": {...}, "message": "success", "timestamp": "...", "path": "/api/v1/..." }`
- 当前文档示例如无特殊说明，仅展示 `data` 字段

### 接口数量汇总

- 总计: 16 个主要端点
- 统计口径: 总览、任务、配置、员工、审批修复、年假洞察

---

## 接口清单（按域）

### 1) 同步总览与任务

| 序号 | 方法 | 路径 | 权限 |
|------|------|------|------|
| 001 | GET | `/overview` | `organization:sync` |
| 002 | GET | `/tasks` | `organization:sync` |
| 003 | POST | `/tasks/:code/trigger` | `organization:sync` |
| 004 | GET | `/tasks/:code/trigger-stream` | `organization:sync` |
| 005 | GET | `/tasks/:code/executions` | `organization:sync` |
| 006 | GET | `/executions/recent` | `organization:sync` |
| 007 | GET | `/config` | `organization:sync` |
| 008 | PATCH | `/config` | `organization:sync` |
| 009 | PATCH | `/tasks/:code` | `organization:sync` |

### 2) 员工管理

| 序号 | 方法 | 路径 | 权限 |
|------|------|------|------|
| 010 | GET | `/employees` | `organization:sync` |
| 011 | GET | `/employees/list` | `organization:sync` |
| 012 | PUT | `/employees/:userId` | `organization:sync` |
| 013 | POST | `/employees/sync` | `organization:sync` |

### 3) 审批撤销修复

| 序号 | 方法 | 路径 | 权限 |
|------|------|------|------|
| 014 | POST | `/approval-cancellations` | `organization:sync` |

### 4) 年假洞察

| 序号 | 方法 | 路径 | 权限 |
|------|------|------|------|
| 015 | GET | `/annual-leave/plan-settings` | `organization:sync` |
| 016 | PATCH | `/annual-leave/plan-settings` | `organization:sync` |
| 017 | GET | `/annual-leave/quotas` | `organization:sync` |
| 018 | GET | `/annual-leave/details` | `organization:sync` |
| 019 | GET | `/annual-leave/records` | `organization:sync` |
| 020 | POST | `/annual-leave/leave-types/refresh` | `organization:sync` |
| 021 | POST | `/annual-leave/quotas/refresh` | `organization:sync` |
| 022 | GET | `/annual-leave/release-plan` | `organization:sync` |
| 023 | GET | `/annual-leave/release-plan/export` | `organization:sync` |
| 024 | POST | `/annual-leave/release-plan/refresh` | `organization:sync` |
| 025 | POST | `/annual-leave/release` | `organization:sync` |

---

## 业务实体与 DTO

### ApprovalCancellationDto

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `taskCode` | string | ✅ | `DINGTALK_BUSINESS_TRIP` / `DINGTALK_FIELD_APPLICATION` / `DINGTALK_OVERTIME` |
| `userId` | string | ✅ | 员工钉钉 `userId` |
| `approveId` | string | ✅ | 审批号，支持精确匹配和 `*` 通配 |
| `fromTime` | string | ❌ | 开始日期，`YYYY-MM-DD` |
| `toTime` | string | ❌ | 结束日期，`YYYY-MM-DD` |
| `dryRun` | boolean | ❌ | 默认 `true`，仅预览不执行 |

### AnnualLeavePlanSettings

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `userId` | string | ✅ | 员工钉钉 `userId` |
| `year` | number | ✅ | 年度 |
| `adjustmentDays` | number | ✅ | 额度扣减天数 |
| `notCountDays` | number | ✅ | 释放计划偏移天数 |
| `totalDays` | number | ✅ | 当前计划总天数 |
| `releaseSchedule` | Array<{ dayIndex, releaseDate }> | ✅ | 当前计划明细 |
| `lastCalculatedAt` | string \| null | ✅ | 最近一次实际重算时间 |

### DingtalkEmployeeUpdateResponse

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `success` | boolean | ✅ | 是否成功 |
| `employee` | object | ✅ | 更新后的员工对象 |

---

## 统一格式 + 简短示例

### [014] POST `/approval-cancellations`

- 说明: 按同步类型、员工、审批号扫描并撤销钉钉考勤审批记录
- 请求 Body: `ApprovalCancellationDto`
- 业务规则:
  - 手动触发窗口最大收敛到最近 60 天
  - 仅处理真实存在且类型匹配的审批记录
  - 同一员工 + 同一审批号去重后再执行

请求示例:

```json
{
  "taskCode": "DINGTALK_FIELD_APPLICATION",
  "userId": "0155050738753347",
  "approveId": "undefined-*",
  "fromTime": "2026-01-01",
  "toTime": "2026-12-31",
  "dryRun": true
}
```

### [015] GET `/annual-leave/plan-settings`

- 说明: 获取员工年度年假计划参数
- 请求 Query:
  - `userId`: 员工钉钉 `userId`
  - `year`: 年度，默认当前年
- 当前实现说明:
  - 读取接口不再写入占位记录
  - 无现存计划记录时返回默认值结构

### [016] PATCH `/annual-leave/plan-settings`

- 说明: 更新员工年度年假计划参数
- 请求 Body:

```json
{
  "userId": "0155050738753347",
  "year": 2026,
  "adjustmentDays": 1,
  "notCountDays": 5,
  "recalculate": true
}
```

- 当前实现说明:
  - 当前前端入口只编辑 `adjustmentDays` 与 `notCountDays`
  - 当前前端保存动作固定为保存并重算
  - 不在此接口维护员工状态

### [017] GET `/annual-leave/quotas`

- 说明: 获取本地快照中的假期余额总览
- 请求 Query:
  - `userId`: 指定员工 `userId`
  - `keyword`: 员工姓名、工号或 `userId` 模糊匹配
  - `hideZero`: 是否隐藏总额和剩余都为 0 的记录
  - `includeAllStatuses`: 默认 `false`，仅返回 `status=正常` 员工的余额；设为 `true` 时返回包含 顾问 / 停薪留职 / 已离职 在内的所有员工余额，用于 HR 清查残留余额场景
- 响应 items 关键字段:
  - `status`: 员工状态（`正常` / `停薪留职` / `顾问` / `已离职` / `未知`）。`未知` 出现在快照引用的 `userId` 在 `dingtalk_employees` 表中不存在的数据漂移场景
- 响应 `summary.employeeCount` 语义:
  - `includeAllStatuses=false`（默认）: 仅计正常员工
  - `includeAllStatuses=true`: 计入所有可见员工（含离职 / 顾问 / 停薪留职），与 items 行数对应
- 响应 `allEmployees`（用于「按员工筛选」下拉）始终仅含 `status=正常` 员工，不随 `includeAllStatuses` 变化

### [018] GET `/annual-leave/details`

- 说明: 获取指定员工某个假期类型的余额详情、释放/使用记录、累计司龄、首次工作日期等
- 响应关键字段:
  - `tenureDays`: 累计司龄天数（缓存值，来自 `dingtalk_employees.tenure_days`）
  - `workStartDate`: 首次参加工作日期（`YYYY-MM-DD`），缺失为 `null`；前端按 `today - workStartDate` 计算并展示「累计工龄」

### [022] GET `/annual-leave/release-plan`

- 说明: 获取本地数据库中的年假释放计划
- 请求 Query:
  - `year`: 年度，默认当前年
  - `userId`: 指定员工 `userId`
  - `keyword`: 员工姓名、工号或 `userId` 模糊匹配
  - `upcomingOnly`: 默认 `true`
- 当前实现说明:
  - 默认且仅展示 `dingtalk_employees.status = 正常` 的员工
  - 当前接口不支持 `includeConsultant`、`includeLeaveWithoutPay`、`includeTerminated`

### [012] PUT `/employees/:userId`

- 说明: 更新员工信息
- 当前实现说明:
  - 仅支持更新 `status`
  - 响应结构为 `{ success, employee }`
