---
date: 2026-05-06
module: dingtalk / employee-management
type: pattern
status: superseded
superseded_by: 2026-05-08-dingtalk-tenure-confirm-source-with-hr.md
superseded_reason: 该 learning 假设"停薪要扣司龄"。2026-05-08 与 HR 确认：停薪计入司龄，本系统口径与钉钉自带字段不同。已通过 fix/dingtalk-tenure-revert-suspension-deduction 回退。
---

# [SUPERSEDED] 钉钉员工司龄：周期表写入必须前后端双触发重算

## 现象

用户报告：在「在职历史」对话框里**新增停薪留职区间后，司龄数字不变**。
对照：新增/编辑「在职段」时，司龄正常变化。

## 根因（双层）

1. **后端 service 漏调重算**
   - [employee-management.service.ts](../backend/src/modules/organization/dingtalk/employee-management.service.ts) 里 `addSuspensionPeriod` / `updateSuspensionPeriod` / `deleteSuspensionPeriod` 三个方法**没有调用** `calculateTenureDays(userId)` 把缓存写回 `dingtalk_employees.tenure_days`。
   - 同文件里的 `addEmploymentPeriod` / `updateEmploymentPeriod` / `deleteEmploymentPeriod` 都正确调了。属于增加并行表时漏接的"半成品"模式。

2. **`calculateTenureDays` 公式根本没读 `SuspensionPeriod` 表**
   - 即使后端 1 修了，重算结果也不变 — 因为停薪留职从未参与扣减。
   - 这是更深的设计漏洞：`SuspensionPeriod` 表存在但在司龄逻辑里是"摆设"。

3. **前端 handler 没刷新司龄**
   - [employees/page.tsx](../frontend/src/app/(modules)/sync-center/dingtalk/employees/page.tsx) 的 `handleAddSuspension` / `handleUpdateSuspension` / `handleDeleteSuspension` 没有在成功后调用 `refreshTenure()`。employment 段的三个 handler 都调了。
   - 即使后端缓存已正确更新，UI 不重新拉接口也看不到新值。

## 项目级模式（沉淀）

在 dingtalk 模块里，**任何会改变累计司龄的表（当前是 `employment_periods` 与 `suspension_periods`），CRUD 必须做到三件事**：

1. **后端 service 层**：写入操作返回前 `await calculateTenureDays(userId)`，把缓存写回 `dingtalk_employees.tenure_days`。
2. **后端 `calculateTenureDays` 公式必须真把这张表读进去并参与计算**——否则触发了重算也只是空跑。
3. **前端 handler**：CRUD 成功后调用 `refreshTenure()`，否则用户看到的是 dialog 内陈旧数据。

下次再加一张"会影响司龄"的表（例如未来若引入"实习期不计司龄段"等），必须按这三件事自检。

## 隐藏的口径冲突要先确认

修这个 bug 时还发现：[annual-leave-sync.service.ts:379](../backend/src/modules/organization/dingtalk/sync/annual-leave-sync.service.ts#L379) 的注释明确写"停薪留职期间不释放年假**但计入司龄**"，而前端帮助文档把停薪留职当成应该从司龄扣的。两边业务假设相反。

**经验**：动业务公式（司龄/工龄/年假天数等）前，**至少要 grep 一遍模块里所有提到这个概念的注释和帮助文案**，把口径冲突挑出来交给业务方拍板，不要按其中一边的假设直接改。否则修了 A 处砸了 B 处。

本次确认口径：**司龄扣除停薪留职**（用户决策），同步把年假反推用的 `accumulatedDaysAsOf` 也改成扣除版，并更新了帮助文案 + `docs/modules/dingtalk/06-data-model.md`。
