From 2f02a2d0f2e2d5aeb83aa8b318949d616507a99b Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Mon, 10 Aug 2026 22:49:56 +0800 Subject: [PATCH] docs: add OA overtime auto-duration and holiday cache design Capture agreed rules for overtime kit auto/manual hours, ailcc sync via in-process midnight loop, and dual-end calculation aligned with leave kit. Co-authored-by: Cursor --- ...-08-10-oa-overtime-auto-duration-design.md | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-10-oa-overtime-auto-duration-design.md diff --git a/docs/superpowers/specs/2026-08-10-oa-overtime-auto-duration-design.md b/docs/superpowers/specs/2026-08-10-oa-overtime-auto-duration-design.md new file mode 100644 index 00000000..cbfbd20d --- /dev/null +++ b/docs/superpowers/specs/2026-08-10-oa-overtime-auto-duration-design.md @@ -0,0 +1,237 @@ +# OA 加班套件 · 自动计算时长 + 中国节假日缓存 设计文档 + +**日期**: 2026-08-10 +**状态**: 待实现 +**范围**: `web-admin` 加班套件运行时时长联动;`backend` 时长重算/校验;全国节假日日历同步与落库 +**前置**: 既有加班套件(手动时长);请假套件手改联动模式(`oa-leave-kit` / `oa_leave_hours`) +**界面模式**: 对齐请假套件发起态交互;视觉 Fresh Mint + +--- + +## 1. 背景与目标 + +当前「加班」套件含加班人、开始时间、结束时间、时长(h),时长仅手动填写。客户要求: + +- 按规则**自动计算**时长,并仍支持**手动修改**; +- 工作日 / 周末与节假日规则不同; +- 节假日数据来自第三方免费接口,并缓存到数据库。 + +### 1.1 目标 + +- 开始/结束变化时自动写入时长;手改后断开联动,可「恢复自动计算」(对齐请假套件)。 +- 前后端同一套算法:发起页即时算;提交时后端 `apply` + `validate`。 +- 每天本地 **00:00** 及**服务启动时**从 `holiday.ailcc.com` 拉取并 upsert 到本地表;计算只读本地库。 +- 同步跑在**后端进程内后台线程**(非 APScheduler cron);进程退出则任务一并结束。 + +### 1.2 非目标 + +- 管理端可视化维护节假日日历。 +- 薪资倍数 / 三倍工资展示。 +- 租户级全局考勤配置中心(时段挂在**套件实例**属性)。 +- 多实例分布式锁(本期 upsert 幂等即可)。 + +--- + +## 2. 产品决策(已确认) + +| 项 | 决策 | +|----|------| +| 实现方案 | 前后端双端计算 + 本地节假日缓存 | +| 小数 | 按分钟累计,四舍五入 **1** 位小数(同请假) | +| 工作日窗口 | 只统计与 `[workdayOvertimeStart, 次日 workdayOvertimeEnd)` 的交集;白天正常工时不算加班 | +| 默认工作日窗口 | `17:30`~次日 `08:30` | +| 休息日 | 结束−开始,扣除与 `[restBreakStart, restBreakEnd)` 的交集 | +| 默认午休 | `11:30`~`13:30` | +| 跨天 | 按自然日切段,分段套用当日规则后相加 | +| 调休补班(ailcc type=4) | 按**工作日**规则 | +| 法定放假 / 周末 / 调休放假(1/2/3) | 按**休息日**规则 | +| 时段配置 | 套件属性可配,默认值如上 | +| 手改 | `__manual_`;未手改自动覆盖;手改仅规范化一位小数;可恢复自动 | +| 节假日源 | `https://holiday.ailcc.com` | +| 同步时机 | 启动拉一次 + 进程内循环 sleep 至次日 00:00 再拉;**不注册 cron** | +| 库无日历行 | 回退:周一~五工作日,周六日休息日 | + +--- + +## 3. 日类型与时长算法 + +### 3.1 日类型判定 + +读 `sys_cn_holiday_day`: + +| `day_type` | 含义 | 加班规则 | +|------------|------|----------| +| 0 | 工作日 | 工作日 | +| 4 | 补班 | 工作日 | +| 1 | 周末 | 休息日 | +| 2 | 节日 | 休息日 | +| 3 | 调休放假 | 休息日 | +| 无记录 | — | 回退周末规则 | + +`is_rest_day` 冗余列:type ∈ {1,2,3} → 1,否则 0。 + +### 3.2 算法步骤(唯一实现约定) + +输入:`start`、`end`(`YYYY-MM-DD HH:mm`)、套件四时段、`is_rest_day(date)`。 + +1. 若无法解析或 `end <= start` → 返回 `null`(不写时长)。 +2. 对半开区间 `[start, end)` 内每一时刻 `t`(实现可用区间合并,语义等同逐分钟): + - **若**存在锚点日 `A`:`A` 为工作日(含补班),且 `t ∈ [A + workdayOvertimeStart, (A+1) + workdayOvertimeEnd)` → **计入**; + - **否则若** `t` 所在日历日为休息日,且 `t` 不在该日 `[restBreakStart, restBreakEnd)` → **计入**; + - **否则**不计。 +3. 同一时刻只计一次(先匹配工作日窗口,再匹配休息日)。 +4. 总分钟 / 60,四舍五入到 1 位小数。 + +说明:工作日窗口允许跨到次日早晨;即使次日是休息日,落在「上一工作日窗口」内的凌晨时段仍按加班计入(不再扣次日午休,因已走工作日窗口分支)。 + +### 3.3 示例(默认时段;周五=工作日,周六=休息日) + +| 场景 | 期望 | +|------|------| +| 工作日 18:00–20:00 | 2.0 | +| 工作日 14:00–18:00 | 0.5(仅 17:30 后) | +| 工作日 17:30–次日 08:30 | 15.0 | +| 周六 09:00–15:00 | 4.0(扣 11:30–13:30) | +| 周五 18:00–周六 12:00 | **17.5** | + +跨天拆解(周五 18:00–周六 12:00): + +- 周五 18:00–周六 08:30:落在周五工作日窗口 → 14.5h +- 周六 08:30–11:30:休息日非午休 → 3.0h +- 周六 11:30–12:00:午休 → 0 +- 合计 **17.5h** + +--- + +## 4. 节假日缓存 + +### 4.1 表 `sys_cn_holiday_day` + +全国共用参考数据,**无 `tenant_no`**(系统日历豁免租户隔离)。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | VARCHAR(64) | UUID | +| `day_date` | DATE | 唯一 | +| `day_type` | TINYINT | 0/1/2/3/4,对齐 ailcc | +| `day_name` | VARCHAR(64) | 展示名 | +| `week` | TINYINT | 1–7 | +| `is_rest_day` | TINYINT | 0/1 | +| `raw_json` | TEXT | 原始片段 | +| `sync_time` | DATETIME | 最近同步 | +| `create_time` / `update_time` | DATETIME | | + +唯一键:`uk_day_date(day_date)`。DDL 写入 `docs/sql/`。 + +### 4.2 同步策略 + +- 数据源:`CN_HOLIDAY_API_BASE`(默认 `https://holiday.ailcc.com`)。 +- **不要只用** `/api/holiday/year/{year}`(文档明确**不返回补班**)。 +- 使用 `GET /api/holiday/batch?d=日期列表&type=Y`,按月拆批(每月 ≤31,低于 50 上限)。 +- 覆盖:**当年**;若当前月为 12 月则同时拉**下一年**(或始终拉当年+下一年,实现任选其一并在计划中写死)。 +- Upsert by `day_date`;失败打日志并**保留旧缓存**。 +- 超时与有限次重试(如 2 次)在服务层配置。 + +### 4.3 进程内后台循环(替代 cron) + +不在 APScheduler 注册 Job。 + +在 FastAPI `lifespan`: + +1. 启动后台 **daemon 线程**(或等价任务); +2. **立即**执行一次 `sync_cn_holidays()`(不阻塞 `yield` 前的就绪:可先起线程再 `yield`); +3. 循环:`sleep` 至下一个本地 **00:00** → 再 sync → 直到 stop 事件; +4. 应用关闭:set stop、join 短超时;线程随进程退出。 + +同步互斥锁:避免启动同步与 00:00 重叠双写。 + +多实例:各进程各自同步,upsert 幂等;本期不加分布式锁。 + +### 4.4 对内 API + +- 服务:`is_rest_day(date) -> bool`;`list_days(start, end)`。 +- HTTP:`GET /api/public/cn-holiday/range?start=YYYY-MM-DD&end=YYYY-MM-DD` + 返回区间内每日 `dayType` / `isRestDay`(或仅返回有行的日期 + 前端回退)。无敏感数据,可走 public。 + +--- + +## 5. 加班套件 Schema / 运行时 + +### 5.1 套件属性(默认值) + +```json +{ + "type": "overtimeKit", + "props": { + "personField": "...", + "startField": "...", + "endField": "...", + "durationField": "...", + "workdayOvertimeStart": "17:30", + "workdayOvertimeEnd": "08:30", + "restBreakStart": "11:30", + "restBreakEnd": "13:30" + }, + "children": [ "...既有四字段..." ] +} +``` + +设计器右侧可选编辑上述四时段;时长字段 `info` 改为提示可自动计算、可手改。 + +### 5.2 form_data 手改标记 + +与请假一致:`__manual_: true`。 + +### 5.3 前端模块 + +| 文件 | 职责 | +|------|------| +| `oa-overtime-kit-rule.ts` | 默认属性 + DragRule props | +| `oa-overtime-hours.ts` | 纯函数计算(注入休息日判断 map/回调) | +| `oa-overtime-kit-runtime.ts` | binding、apply、manual、对齐样式 | +| `OaStartFormDialog.vue` | 联动与「恢复自动计算」(对齐 leave) | +| `OaOvertimeKit.vue` | 容器;可声明新 props | + +发起时:按起止预取 holiday range 到内存,再算时长。 + +### 5.4 后端模块 + +| 文件 | 职责 | +|------|------| +| model + SQL | `sys_cn_holiday_day` | +| `cn_holiday_sync.py` | 拉 ailcc、upsert、后台循环入口 | +| `cn_holiday.py` | 只读判定 | +| `oa_overtime_hours.py` | 与前端对齐的 calc / apply / validate | +| `oa_engine` 提交路径 | 在 leave/trip 旁调用 overtime apply + validate | +| `public` API | range 查询 | +| `main.py` lifespan | 启动/停止后台同步线程 | + +`apply`:未手改则覆盖时长;已手改则仅 `_round1`。 +`validate`:结束晚于开始;手改时长合法。 + +--- + +## 6. 验收标准 + +1. 启动后库中有当年(及约定的下一年)日历,且含补班日(type=4)。 +2. 进程内到 00:00 会再同步;**无** APScheduler overtime/holiday cron 注册;关停服务后面台循环停止。 +3. 第 3.3 节示例时长与单测一致。 +4. 手改断开 / 恢复自动计算行为与请假一致。 +5. 提交时未手改时长由后端按规则写回。 +6. ailcc 失败时仍可用旧缓存或周末回退完成计算。 + +--- + +## 7. 测试计划 + +- 前端:`oa-overtime-hours.test.ts`(假日 map 内存注入,不访问外网)。 +- 后端:`test_oa_overtime_hours.py`、`test_cn_holiday_sync.py`(mock HTTP)。 +- 可选:lifespan 启动路径断言调用一次 sync(mock)。 + +--- + +## 8. 风险与备注 + +- ailcc 免费限流:按月 batch,启动 + 每日一次足够。 +- 服务长时间停机:重启时启动同步补齐。 +- 算法跨午夜语义以 §3.2 为准,前后端单测共用同一组 §3.3 用例。