2026-08-10-oa-overtime-auto-duration-design.md 9.2 KB

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_<durationField>;未手改自动覆盖;手改仅规范化一位小数;可恢复自动
节假日源 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 上限)。
  • 覆盖:当年 + 下一年;若某年在库中行数已 ≥ 该年天数(365/366),则跳过该年,不再请求 ailcc。
  • 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 套件属性(默认值)

{
  "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_<durationField>: 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 用例。