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 <cursoragent@cursor.com>
This commit is contained in:
1 parent
efd7efbc69
commit
2f02a2d0f2
1 file changed
+237
@@ -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_<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 上限)。
|
||||
- 覆盖:**当年**;若当前月为 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_<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 用例。
|
||||
Reference in new issue
Block a user