feat(hrm): 打通请假审批与考勤,请假当天可不打卡也不算异常
## 一、修复请假审批无法提交(BPM 配置缺失)
现象:请假申请「提交审批」报 1030001006,前端拿不到可选流程。
根因:
1. 代码里 HRM 四个审批模块各要求一个分类码:
hrm_leave_approve / overtime_approve / hrm_resignation_approve / hrm_transfer_approve
而 bpm_category 里这四个分类一个都没有。
2. 库里已部署的请假流程(key=qingjia v1/v2)被误挂在
「考勤异常审批 hrm_attendance_approve」分类下,按 hrm_leave_approve 过滤查不到。
修复:补建 4 个分类;为 qingjia 各版本复制一份 bpm_process_definition_info
记录到 hrm_leave_approve,保留原有 model_id / 表单 / 流程配置;原绑定不动。
验证:approve-process-definition-list 返回 qingjia v2(已激活)。
## 二、考勤与请假打通
改造前的问题:
- 请假当天照样可以打卡、照样算迟到
- 请假当天不打卡会被算成「缺卡」,反而扣分
- 考勤记录里没有任何字段能体现「这天请假了」
- 月度统计的 leaveHours 写死为 BigDecimal.ZERO,从未计算
改动:
1. hrm_attendance_record 新增 leave_flag / leave_hours / leave_types 三列
2. HrmAttendanceClockTypeEnum 新增 LEAVE(5, "请假")
3. 新增 HrmLeaveQueryService:只读查某员工某天的请假单
HrmLeaveApplicationMapper 新增 selectListByUserIdAndOverlapDate
(按时间区间【交集】查,支持跨天请假;原有的 selectApprovedListByUserIdAndTimeRange
是【全包含】语义,不适用于按天判断)
4. getMyStatus:返回 onLeave / leaveHours / leaveTypeNames,
请假当天提示「今日已请假(xx),如需上班可正常打卡」,但打卡按钮保持可用
5. clockIn / clockOut:请假当天打卡一律记为正常,不判迟到/早退;
下班打卡时若上班卡为「缺卡」则改为「请假」
6. 月度统计:按考勤规则工作时段与请假单求交集累计 leaveHours(精确到 0.01 小时);
请假当天不计迟到/早退/缺卡;补算「有请假单但当天没打卡」的日子
7. 修正字典 hrm_clock_type:原字典 2=迟到/早退、3=缺卡,与枚举
(2=迟到 3=早退 4=缺卡)不一致,导致早退被显示成缺卡、缺卡显示为空
## 三、顺带修复的 ID 语义混乱
clockIn/clockOut 的入参实际是「员工档案 ID」,但考勤记录表和请假表存的是
「系统用户 ID」。原代码用入参直接查记录,导致多员工数据可能错位。
现在统一用 employee.getUserId() 查记录与请假;getMyStatus 改用 loginUserId。
## 验证(本地 IDEA + 生产环境)
本地:
onLeave=true leaveHours=5(10:00-15:00 与 10:00-19:30 交集)
leaveTypeNames=事假 可打卡=true
打卡后 clockInType=1(正常) leaveFlag=true leaveHours=5 leaveTypes=1
月度统计 请假工时=5 缺卡=0
生产(宋春歌,请假 10-13~12-31 覆盖今天):
onLeave=True leaveHours=4.0 leaveTypeNames=产假
可打卡=True 提示「今日已请假(产假),如需上班可正常打卡」
(4.0 是因为部门规则已禁用,回退到全局「长辉2026下午」15:30-19:30,
与请假 09:30-19:30 交集正好 4 小时,计算无误)
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | #!/usr/bin/env node |
| | | /** |
| | | * åæ®µä¸è´æ§æ£æ¥èæ¬ |
| | | * |
| | | * ç¨éï¼æ¹å®ä»£ç åè·ä¸æ¬¡ï¼æ£æ¥ãå端è½å¡«ãå端åä¸ä¸ / 读ä¸åºãçé¾è·¯æè£ã |
| | | * |
| | | * ç¨æ³ï¼ |
| | | * node scripts/field-consistency-check.mjs [å端路å¾] [schema.json] |
| | | * |
| | | * schema.json å¯ä»ç产åºå¯¼åºï¼ |
| | | * SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.columns |
| | | * WHERE table_schema='mom-chdl' ORDER BY TABLE_NAME, ORDINAL_POSITION; |
| | | * 导åºä¸º {"表å": ["col1","col2"]} ç JSON |
| | | * |
| | | * éåºç ï¼0 = æ é®é¢ï¼1 = åç°é®é¢ |
| | | */ |
| | | |
| | | import { readdirSync, readFileSync, statSync } from 'node:fs'; |
| | | import { basename, join } from 'node:path'; |
| | | |
| | | const BACKEND = process.argv[2] || '.'; |
| | | const SCHEMA_FILE = process.argv[3] || '.local-test/db-schema.json'; |
| | | |
| | | const COMMON = new Set(['id', 'creator', 'createTime', 'updater', 'updateTime', 'deleted', 'tenantId']); |
| | | const camel2snake = (s) => s.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, ''); |
| | | const isColl = (t) => /List|Set|Collection|\[\]|Map/.test(t); |
| | | |
| | | function listJava(dir) { |
| | | const out = []; |
| | | const walk = (d) => { |
| | | let ents = []; |
| | | try { ents = readdirSync(d, { withFileTypes: true }); } catch { return; } |
| | | for (const e of ents) { |
| | | const p = join(d, e.name); |
| | | if (e.isDirectory()) { |
| | | if (!['target', 'node_modules', '.git'].includes(e.name)) walk(p); |
| | | } else if (e.name.endsWith('.java')) out.push(p); |
| | | } |
| | | }; |
| | | walk(dir); |
| | | return out; |
| | | } |
| | | |
| | | /** åªåé¡¶å±å段ï¼è·³è¿å
é¨ç±»ï¼ï¼å¹¶è®°å½ @TableField æå®ççå®åå */ |
| | | function topFields(file) { |
| | | const t = readFileSync(file, 'utf8'); |
| | | const out = []; |
| | | let depth = 0; |
| | | let anno = ''; |
| | | for (const line of t.split(/\r?\n/)) { |
| | | const trimmed = line.trim(); |
| | | if (/^@(com\.baomidou\.mybatisplus\.annotation\.)?TableField/.test(trimmed)) { |
| | | anno += trimmed; |
| | | continue; |
| | | } |
| | | if (depth === 1) { |
| | | const m = trimmed.match(/^private\s+(?:static\s+|final\s+)?([\w<>\[\].?, ]+?)\s+(\w+)\s*;/); |
| | | if (m) { |
| | | if (/exist\s*=\s*false/.test(anno)) { anno = ''; } |
| | | else { |
| | | const colM = anno.match(/@(?:com\.baomidou\.mybatisplus\.annotation\.)?TableField\(\s*(?:value\s*=\s*)?"`?([^"`)]+)`?"/); |
| | | out.push({ type: m[1].trim(), name: m[2], col: colM ? colM[1] : null }); |
| | | anno = ''; |
| | | } |
| | | continue; |
| | | } |
| | | } |
| | | const clean = trimmed.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '').replace(/"[^"]*"/g, '""'); |
| | | for (const ch of clean) { if (ch === '{') depth++; else if (ch === '}') depth--; } |
| | | if (trimmed && !trimmed.startsWith('@')) anno = ''; |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | const javaFiles = readdirSync(BACKEND, { withFileTypes: true }) |
| | | .filter((e) => e.isDirectory() && e.name.startsWith('yudao-module-')) |
| | | .flatMap((e) => listJava(join(BACKEND, e.name, 'src', 'main', 'java'))); |
| | | |
| | | let schema = {}; |
| | | try { schema = JSON.parse(readFileSync(SCHEMA_FILE, 'utf8')); } |
| | | catch { console.warn('â æªæ¾å° schema æä»¶ï¼è·³è¿æ£æ¥ Cï¼DO vs æ°æ®åºåï¼'); } |
| | | |
| | | const doMap = new Map(); // base -> { cls, table, fields } |
| | | const saveMap = new Map(); // base -> { cls, fields } |
| | | const respMap = new Map(); // base -> { cls, fields } |
| | | const srcTexts = []; |
| | | |
| | | for (const f of javaFiles) { |
| | | const bn = basename(f); |
| | | if (bn.endsWith('DO.java')) { |
| | | const base = bn.replace(/DO\.java$/, ''); |
| | | const txt = readFileSync(f, 'utf8'); |
| | | const tm = txt.match(/@TableName\(\s*"([^"]+)"/); |
| | | const d = doMap.get(base) || { cls: bn.replace(/\.java$/, ''), table: tm ? tm[1] : null, fields: [] }; |
| | | d.fields.push(...topFields(f)); |
| | | doMap.set(base, d); |
| | | } else if (bn.endsWith('SaveReqVO.java')) { |
| | | const base = bn.replace(/SaveReqVO\.java$/, ''); |
| | | const s = saveMap.get(base) || { cls: bn.replace(/\.java$/, ''), fields: [] }; |
| | | s.fields.push(...topFields(f)); |
| | | saveMap.set(base, s); |
| | | } else if (bn.endsWith('RespVO.java')) { |
| | | const base = bn.replace(/RespVO\.java$/, ''); |
| | | const r = respMap.get(base) || { cls: bn.replace(/\.java$/, ''), fields: [] }; |
| | | r.fields.push(...topFields(f)); |
| | | respMap.set(base, r); |
| | | } |
| | | if (/Controller\.java$|ServiceImpl\.java$|Convert\.java$|Service\.java$/.test(bn) && !bn.includes('Api')) { |
| | | srcTexts.push(readFileSync(f, 'utf8')); |
| | | } |
| | | } |
| | | const allSrc = srcTexts.join('\n'); |
| | | const assigned = (name) => { |
| | | const cap = name[0].toUpperCase() + name.slice(1); |
| | | return allSrc.includes('set' + cap + '(') || allSrc.includes('::set' + cap); |
| | | }; |
| | | |
| | | const problems = []; |
| | | |
| | | // A. SaveReqVO æ / RespVO 没æ |
| | | for (const [base, s] of saveMap) { |
| | | const r = respMap.get(base); |
| | | if (!r) continue; |
| | | const rf = new Set(r.fields.map((x) => x.name)); |
| | | for (const f of s.fields) { |
| | | if (COMMON.has(f.name) || isColl(f.type) || rf.has(f.name)) continue; |
| | | problems.push({ kind: 'A', msg: `${base}: SaveReqVO æ ${f.name}ï¼RespVO 没æ â åå¾è¿ä½è¯»ä¸åº` }); |
| | | } |
| | | } |
| | | |
| | | // B. SaveReqVO æ / DO 没æ / Service 乿²¡æå·¥æ å° |
| | | for (const [base, s] of saveMap) { |
| | | const d = doMap.get(base); |
| | | if (!d) continue; |
| | | const df = new Set(d.fields.map((x) => x.name)); |
| | | for (const f of s.fields) { |
| | | if (COMMON.has(f.name) || isColl(f.type) || df.has(f.name)) continue; |
| | | if (assigned(f.name)) continue; |
| | | problems.push({ kind: 'B', msg: `${d.cls}: SaveReqVO æ ${f.name}ï¼DO 没æ â æäº¤è¢«éé»ä¸¢å¼` }); |
| | | } |
| | | } |
| | | |
| | | // C. DO æ / æ°æ®åºè¡¨æ²¡è¿å |
| | | for (const [, d] of doMap) { |
| | | if (!d.table || !schema[d.table]) continue; |
| | | const cols = new Set(schema[d.table]); |
| | | for (const f of d.fields) { |
| | | const col = f.col || camel2snake(f.name); |
| | | if (!cols.has(col)) problems.push({ kind: 'C', msg: `${d.cls}: åæ®µ ${f.name} 对åºå ${col} å¨è¡¨ ${d.table} ä¸åå¨ â SQL æ¥é` }); |
| | | } |
| | | } |
| | | |
| | | // D. RespVO æ / DO 没æ / å
¨ä»£ç åºä»æªèµå¼ |
| | | for (const [base, r] of respMap) { |
| | | const d = doMap.get(base); |
| | | if (!d) continue; |
| | | const df = new Set(d.fields.map((x) => x.name)); |
| | | for (const f of r.fields) { |
| | | if (COMMON.has(f.name) || isColl(f.type) || df.has(f.name)) continue; |
| | | if (assigned(f.name)) continue; |
| | | problems.push({ kind: 'D', msg: `${r.cls}: ${f.name} 仿ªè¢«èµå¼ â åç«¯æ°¸è¿æ¾ç¤ºç©º` }); |
| | | } |
| | | } |
| | | |
| | | const byKind = { A: [], B: [], C: [], D: [] }; |
| | | for (const p of problems) byKind[p.kind].push(p.msg); |
| | | |
| | | const titles = { |
| | | A: 'A. SaveReqVO æ / RespVO 没æï¼åå¾è¿ã读ä¸åºï¼', |
| | | B: 'B. SaveReqVO æ / DO 没æï¼æäº¤è¢«ä¸¢å¼ï¼', |
| | | C: 'C. DO æ / æ°æ®åºè¡¨æ²¡è¿åï¼SQL æ¥éï¼', |
| | | D: 'D. RespVO æ / 仿ªèµå¼ï¼æ°¸è¿æ¾ç¤ºç©ºï¼', |
| | | }; |
| | | |
| | | let total = 0; |
| | | for (const k of ['A', 'B', 'C', 'D']) { |
| | | console.log('\n=== ' + titles[k] + ' ==='); |
| | | if (byKind[k].length === 0) console.log(' (æ )'); |
| | | else { console.log(' â ' + byKind[k].length + ' 项'); byKind[k].forEach((m) => console.log(' - ' + m)); } |
| | | total += byKind[k].length; |
| | | } |
| | | |
| | | console.log('\nå计: ' + total + ' 项'); |
| | | if (total > 0) { |
| | | console.log('\næç¤ºï¼A/B/C é常æ¯çé®é¢ï¼D éæ´¾çå±ç¤ºå段è¾å¤ï¼é人工确认ã'); |
| | | process.exit(1); |
| | | } |
| | | console.log('â
æªåç°å段é¾è·¯æè£'); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ============================================================ |
| | | -- èå¤è®°å½è¡¨å¢å 请åç¸å
³åæ®µï¼æéã请å â èå¤ã |
| | | -- çææ¶é´ï¼2026-10-30 |
| | | -- |
| | | -- èæ¯ï¼è¯·ååä¸èå¤å®å
¨è±èââ |
| | | -- · 请åå½å¤©ç
§æ ·å¯ä»¥æå¡ãç
§æ ·ç®è¿å° |
| | | -- · 请åå½å¤©ä¸æå¡ä¼è¢«ç®æã缺å¡ã |
| | | -- · èå¤è®°å½é没æä»»ä½å段è½ä½ç°ãè¿å¤©è¯·åäºã |
| | | -- · æåº¦ç»è®¡ç leaveHours åæ®µåæ»ä¸º 0ï¼ä»æªè®¡ç® |
| | | -- |
| | | -- åæ³ï¼å¨ hrm_attendance_record ä¸å¢å ä¸ä¸ªå段ââ |
| | | -- leave_flag å½å¤©æ¯å¦è¯·åï¼æå®¡æ¹éè¿ç请ååè¦çå½å¤©å³ä¸º 1ï¼ |
| | | -- leave_hours å½å¤©è¯·åå·¥æ¶ï¼æè¯·ååçèµ·æ¢æ¶é´ä¸èå¤è§åç工使¶æ®µæ±äº¤éï¼ç²¾ç¡®å°åéï¼ |
| | | -- leave_types å½å¤©æ¶åç请åç±»åï¼éå·åéï¼æ¯æä¸å¤©å
夿®µè¯·åãä¸åç±»åï¼ |
| | | -- |
| | | -- ç²åº¦ï¼æå°æ¶ï¼0.01 å°æ¶ = 36 ç§ï¼ï¼ä¸ hrm_leave_application.duration 精度ä¸è´ |
| | | -- å¹çï¼å
æ¥ information_schema å ALTER |
| | | -- ============================================================ |
| | | |
| | | DROP PROCEDURE IF EXISTS sp_attendance_add_leave_col; |
| | | DELIMITER $$ |
| | | CREATE PROCEDURE sp_attendance_add_leave_col(IN p_col VARCHAR(64), IN p_def VARCHAR(255), |
| | | IN p_comment VARCHAR(255), IN p_after VARCHAR(64)) |
| | | BEGIN |
| | | IF NOT EXISTS (SELECT 1 FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() |
| | | AND table_name = 'hrm_attendance_record' |
| | | AND column_name = p_col) THEN |
| | | SET @s = CONCAT('ALTER TABLE `hrm_attendance_record` ADD COLUMN `', p_col, '` ', p_def, |
| | | ' COMMENT ''', p_comment, ''' AFTER `', p_after, '`'); |
| | | PREPARE st FROM @s; EXECUTE st; DEALLOCATE PREPARE st; |
| | | END IF; |
| | | END$$ |
| | | DELIMITER ; |
| | | |
| | | CALL sp_attendance_add_leave_col('leave_flag', 'bit(1) NOT NULL DEFAULT b''0''', |
| | | 'å½å¤©æ¯å¦è¯·åï¼æå®¡æ¹éè¿ç请ååè¦çå½å¤©ï¼', 'overtime_hours'); |
| | | CALL sp_attendance_add_leave_col('leave_hours', 'decimal(6,2) NOT NULL DEFAULT 0.00', |
| | | 'å½å¤©è¯·åå·¥æ¶ï¼æè¯·ååä¸å·¥ä½æ¶æ®µäº¤é计ç®ï¼ç²¾ç¡®å° 0.01 å°æ¶ï¼', 'leave_flag'); |
| | | CALL sp_attendance_add_leave_col('leave_types', 'varchar(64) DEFAULT NULL', |
| | | 'å½å¤©è¯·åç±»åï¼éå·åéï¼å¦ 1,2 è¡¨ç¤ºæ¢æäºååæç
åï¼', 'leave_hours'); |
| | | |
| | | DROP PROCEDURE IF EXISTS sp_attendance_add_leave_col; |
| | | |
| | | -- ---------- ç´¢å¼ï¼ææ¥æ+è¯·åæ è®°æ¥è¯¢æ´å¿« ---------- |
| | | SET @idx = (SELECT COUNT(*) FROM information_schema.statistics |
| | | WHERE table_schema = DATABASE() AND table_name = 'hrm_attendance_record' |
| | | AND index_name = 'idx_leave_flag'); |
| | | SET @s2 = IF(@idx = 0, |
| | | 'CREATE INDEX `idx_leave_flag` ON `hrm_attendance_record` (`leave_flag`)', |
| | | 'SELECT 1'); |
| | | PREPARE st2 FROM @s2; EXECUTE st2; DEALLOCATE PREPARE st2; |
| | | |
| | | -- ---------- æ ¡éª ---------- |
| | | SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT |
| | | FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'hrm_attendance_record' |
| | | AND COLUMN_NAME IN ('leave_flag', 'leave_hours', 'leave_types') |
| | | ORDER BY ORDINAL_POSITION; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ============================================================ |
| | | -- ä¿®æ£åå
¸ hrm_clock_typeï¼ä¸å端æä¸¾ HrmAttendanceClockTypeEnum å¯¹é½ |
| | | -- çææ¶é´ï¼2026-10-30 |
| | | -- |
| | | -- é®é¢ï¼åå
¸ä¸æä¸¾ä¸ä¸è´ï¼å¯¼è´å端çé/æ ç¾æ¾ç¤ºéä¹± |
| | | -- |
| | | -- æä¸¾å®ä¹ï¼å端å¯ä¸çç¸ï¼ï¼ |
| | | -- NORMAL(1, "æ£å¸¸") LATE(2, "è¿å°") EARLY(3, "æ©é") |
| | | -- MISSING(4, "缺å¡") LEAVE(5, "请å") â LEAVE ä¸ºæ¬æ¬¡æ°å¢ |
| | | -- |
| | | -- ååå
¸ï¼éçï¼ï¼ |
| | | -- 1=æ£å¸¸ 2=è¿å°/æ©é 3=ç¼ºå¡ |
| | | -- â å¼ä¸º 3ï¼æ©éï¼çæå¡è®°å½ä¼è¢«æ¾ç¤ºæã缺å¡ã |
| | | -- â å¼ä¸º 4ï¼ç¼ºå¡ï¼çæå¡è®°å½å¨åå
¸éæ¾ä¸å°ï¼æ¾ç¤ºç©º |
| | | -- |
| | | -- åæ³ï¼ææä¸¾é建åå
¸æ°æ®ï¼å¹çï¼å
å åæï¼åªå½±å hrm_clock_typeï¼ |
| | | -- ============================================================ |
| | | |
| | | -- ---------- 1. å
çä¿®æ£å ---------- |
| | | SELECT sort, label, value FROM system_dict_data |
| | | WHERE dict_type = 'hrm_clock_type' AND deleted = b'0' ORDER BY sort; |
| | | |
| | | -- ---------- 2. è½¯å æ§æ°æ®ï¼ä¿çå¯åæ»ï¼ ---------- |
| | | UPDATE system_dict_data |
| | | SET deleted = b'1', updater = '1', update_time = NOW() |
| | | WHERE dict_type = 'hrm_clock_type' AND deleted = b'0'; |
| | | |
| | | -- ---------- 3. ææä¸¾éæ°æå
¥ 5 项 ---------- |
| | | INSERT INTO `system_dict_data` |
| | | (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, |
| | | `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | VALUES |
| | | (1, 'æ£å¸¸', '1', 'hrm_clock_type', 0, 'success', '', 'ææ¶æå¡', '1', NOW(), '1', NOW(), b'0'), |
| | | (2, 'è¿å°', '2', 'hrm_clock_type', 0, 'warning', '', 'ä¸çæå¡æäºè§å®æ¶é´', '1', NOW(), '1', NOW(), b'0'), |
| | | (3, 'æ©é', '3', 'hrm_clock_type', 0, 'warning', '', 'ä¸çæå¡æ©äºè§å®æ¶é´', '1', NOW(), '1', NOW(), b'0'), |
| | | (4, '缺å¡', '4', 'hrm_clock_type', 0, 'danger', '', 'åºæå¡ä½æªæå¡', '1', NOW(), '1', NOW(), b'0'), |
| | | (5, '请å', '5', 'hrm_clock_type', 0, 'info', '', 'å½å¤©æå®¡æ¹éè¿ç请ååï¼ä¸è®¡èå¤å¼å¸¸', '1', NOW(), '1', NOW(), b'0'); |
| | | |
| | | -- ---------- 4. æ ¡éª ---------- |
| | | SELECT sort, label, value, color_type FROM system_dict_data |
| | | WHERE dict_type = 'hrm_clock_type' AND deleted = b'0' ORDER BY sort; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ============================================================ |
| | | -- HRM å®¡æ¹æµç¨åç±»è¡¥é½ + è¯·åæµç¨ç»å®å°æ£ç¡®åç±» |
| | | -- çææ¶é´ï¼2026-10-30 |
| | | -- |
| | | -- é®é¢ï¼è¯·åç³è¯·ãæäº¤å®¡æ¹ãæ¥ 1030001006ï¼å端æ¿ä¸å°å¯éæµç¨ |
| | | -- |
| | | -- æ ¹å ï¼ |
| | | -- 1. 代ç é HRM åä¸ªå®¡æ¹æ¨¡ååè¦æ±ä¸ä¸ªåç±»ç ï¼ |
| | | -- hrm_leave_approve / overtime_approve / hrm_resignation_approve / hrm_transfer_approve |
| | | -- è bpm_category éè¿å个åç±»ä¸ä¸ªé½æ²¡æ |
| | | -- 2. åºéå·²é¨ç½²çè¯·åæµç¨ï¼key=qingjiaï¼è¢«æå¨ |
| | | -- ãèå¤å¼å¸¸å®¡æ¹ hrm_attendance_approveãåç±»ä¸ï¼æ hrm_leave_approve è¿æ»¤æ¥ä¸å° |
| | | -- |
| | | -- åæ³ï¼ |
| | | -- 1. 补建 4 个åç±»ï¼å¹çï¼ |
| | | -- 2. 为 qingjia çæ¯ä¸ªå·²é¨ç½²çæ¬ï¼å¤å¶ä¸ä»½ bpm_process_definition_info |
| | | -- è®°å½å° hrm_leave_approve åç±»ï¼ä¿çåè®°å½ç model_id / 表å / æµç¨é
ç½® |
| | | -- 3. åæ hrm_attendance_approve ç»å®ä¿çä¸å¨ï¼ä¸å½±åå
¶å®åè½ï¼ |
| | | -- ============================================================ |
| | | |
| | | -- ---------- 1. 补建 4 个åç±» ---------- |
| | | INSERT INTO `bpm_category` (`name`, `code`, `description`, `status`, `sort`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT '请å审æ¹', 'hrm_leave_approve', 'å工请åç³è¯·çå®¡æ¹æµç¨åç±»', 0, 15, '1', NOW(), '1', NOW(), b'0' |
| | | WHERE NOT EXISTS (SELECT 1 FROM `bpm_category` WHERE `code` = 'hrm_leave_approve' AND `deleted` = b'0'); |
| | | |
| | | INSERT INTO `bpm_category` (`name`, `code`, `description`, `status`, `sort`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT 'å ç审æ¹', 'overtime_approve', 'åå·¥å çç³è¯·çå®¡æ¹æµç¨åç±»', 0, 16, '1', NOW(), '1', NOW(), b'0' |
| | | WHERE NOT EXISTS (SELECT 1 FROM `bpm_category` WHERE `code` = 'overtime_approve' AND `deleted` = b'0'); |
| | | |
| | | INSERT INTO `bpm_category` (`name`, `code`, `description`, `status`, `sort`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT '离è审æ¹', 'hrm_resignation_approve', 'å工离èç³è¯·çå®¡æ¹æµç¨åç±»', 0, 17, '1', NOW(), '1', NOW(), b'0' |
| | | WHERE NOT EXISTS (SELECT 1 FROM `bpm_category` WHERE `code` = 'hrm_resignation_approve' AND `deleted` = b'0'); |
| | | |
| | | INSERT INTO `bpm_category` (`name`, `code`, `description`, `status`, `sort`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT 'è°å²å®¡æ¹', 'hrm_transfer_approve', 'åå·¥è°å²ç³è¯·çå®¡æ¹æµç¨åç±»', 0, 18, '1', NOW(), '1', NOW(), b'0' |
| | | WHERE NOT EXISTS (SELECT 1 FROM `bpm_category` WHERE `code` = 'hrm_transfer_approve' AND `deleted` = b'0'); |
| | | |
| | | -- ---------- 2. å¤å¶ qingjia çæµç¨å®ä¹ä¿¡æ¯å° hrm_leave_approve ---------- |
| | | -- ç¨åè®°å½å模æ¿ï¼ä¿è¯è¡¨å/æµç¨é
ç½®å®æ´å¸¦è¿å»ï¼model_id å åç¼é¿å
䏿ºè®°å½å²çª |
| | | INSERT INTO `bpm_process_definition_info` |
| | | (`process_definition_id`, `model_id`, `model_type`, `category`, `icon`, `description`, |
| | | `form_type`, `form_id`, `form_conf`, `form_fields`, `form_custom_create_path`, `form_custom_view_path`, |
| | | `simple_model`, `visible`, `sort`, `start_user_ids`, `start_dept_ids`, `manager_user_ids`, |
| | | `allow_cancel_running_process`, `allow_withdraw_task`, `process_id_rule`, `auto_approval_type`, |
| | | `title_setting`, `summary_setting`, |
| | | `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT p.`ID_`, |
| | | CONCAT(src.`model_id`, '-leave'), |
| | | src.`model_type`, |
| | | 'hrm_leave_approve', |
| | | src.`icon`, src.`description`, |
| | | src.`form_type`, src.`form_id`, src.`form_conf`, src.`form_fields`, |
| | | src.`form_custom_create_path`, src.`form_custom_view_path`, |
| | | src.`simple_model`, src.`visible`, src.`sort`, src.`start_user_ids`, src.`start_dept_ids`, src.`manager_user_ids`, |
| | | src.`allow_cancel_running_process`, src.`allow_withdraw_task`, src.`process_id_rule`, src.`auto_approval_type`, |
| | | src.`title_setting`, src.`summary_setting`, |
| | | '1', NOW(), '1', NOW(), b'0' |
| | | FROM `act_re_procdef` p |
| | | JOIN `bpm_process_definition_info` src |
| | | ON src.`process_definition_id` = p.`ID_` AND src.`deleted` = b'0' |
| | | WHERE p.`key_` = 'qingjia' |
| | | AND NOT EXISTS ( |
| | | SELECT 1 FROM `bpm_process_definition_info` x |
| | | WHERE x.`process_definition_id` = p.`ID_` |
| | | AND x.`category` = 'hrm_leave_approve' AND x.`deleted` = b'0' |
| | | ); |
| | | |
| | | -- ---------- 3. æ ¡éª ---------- |
| | | SELECT id, name, code, status, sort FROM `bpm_category` |
| | | WHERE code IN ('hrm_leave_approve','overtime_approve','hrm_resignation_approve','hrm_transfer_approve') |
| | | AND deleted = b'0' ORDER BY sort; |
| | | |
| | | SELECT info.`category`, info.`process_definition_id`, p.`name_`, p.`version_`, |
| | | p.`suspension_state_` AS ç¶æ_1æ¿æ´», |
| | | CASE WHEN info.`form_fields` IS NULL THEN 'æ 表å' ELSE 'æè¡¨å' END AS 表å |
| | | FROM `bpm_process_definition_info` info |
| | | JOIN `act_re_procdef` p ON p.`ID_` = info.`process_definition_id` |
| | | WHERE info.`category` = 'hrm_leave_approve' AND info.`deleted` = b'0' |
| | | ORDER BY p.`version_`; |
| | |
| | | NORMAL(1, "æ£å¸¸"), |
| | | LATE(2, "è¿å°"), |
| | | EARLY(3, "æ©é"), |
| | | MISSING(4, "缺å¡"); |
| | | MISSING(4, "缺å¡"), |
| | | /** |
| | | * 请å |
| | | * |
| | | * å½å¤©æå®¡æ¹éè¿ç请ååæ¶ä¸ºè¯¥ç±»åã请åå½å¤©ä»å
许æå¡ï¼ |
| | | * 使å¡ä¸ä¼è¢«å¤å®ä¸ºè¿å°/æ©éï¼æªæå¡ä¹ä¸ä¼å¤å®ä¸ºç¼ºå¡ã |
| | | */ |
| | | LEAVE(5, "请å"); |
| | | |
| | | private final Integer type; |
| | | private final String name; |
| | |
| | | @Schema(description = "æ°æ®æ¥æºï¼å
¼å®¹æ§ç客æ·ç«¯ï¼") |
| | | private Integer dataSource; |
| | | |
| | | @Schema(description = "å½å¤©æ¯å¦è¯·å", example = "false") |
| | | private Boolean onLeave; |
| | | |
| | | @Schema(description = "å½å¤©è¯·åå·¥æ¶ï¼å°æ¶ï¼ï¼æªè¯·å为 0", example = "3.5") |
| | | private BigDecimal leaveHours; |
| | | |
| | | @Schema(description = "å½å¤©è¯·åç±»ååç§°ï¼å¤æ®µç¨é¡¿å·åé", example = "äºåãç
å") |
| | | private String leaveTypeNames; |
| | | |
| | | @Schema(description = "å½å坿å¡ç¶æ") |
| | | private Clockable clockable; |
| | | |
| | |
| | | @Schema(description = "ä¸çæå¡ç±»å", example = "1") |
| | | private Integer clockOutType; |
| | | |
| | | @Schema(description = "æ¯å¦è¯·åï¼true-已请åï¼false-æªè¯·åï¼ä¸ä¼ åä¸è¿æ»¤", example = "true") |
| | | private Boolean leaveFlag; |
| | | |
| | | } |
| | |
| | | @DictFormat("hrm_data_source") |
| | | private Integer dataSource; |
| | | |
| | | @Schema(description = "å½å¤©æ¯å¦è¯·å", example = "false") |
| | | @ExcelProperty("æ¯å¦è¯·å") |
| | | private Boolean leaveFlag; |
| | | |
| | | @Schema(description = "å½å¤©è¯·åå·¥æ¶ï¼å°æ¶ï¼", example = "3.5") |
| | | @ExcelProperty("请åå·¥æ¶") |
| | | private BigDecimal leaveHours; |
| | | |
| | | @Schema(description = "å½å¤©è¯·åç±»åï¼éå·åé", example = "1,2") |
| | | private String leaveTypes; |
| | | |
| | | @Schema(description = "å建æ¶é´") |
| | | @ExcelProperty("å建æ¶é´") |
| | | private LocalDateTime createTime; |
| | |
| | | private BigDecimal overtimeHours; |
| | | |
| | | /** |
| | | * å½å¤©æ¯å¦è¯·å |
| | | * |
| | | * å½å¤©åå¨ã审æ¹éè¿ãç请ååä¸ä¸å·¥ä½æ¶æ®µæéå æ¶ä¸º trueã |
| | | * 请åå½å¤©ä»å
许æå¡ï¼ä½ä¸ç®è¿å°/æ©éï¼ä¹ä¸ç®ç¼ºå¡ã |
| | | */ |
| | | private Boolean leaveFlag; |
| | | |
| | | /** |
| | | * å½å¤©è¯·åå·¥æ¶ï¼å°æ¶ï¼ |
| | | * |
| | | * æè¯·ååèµ·æ¢æ¶é´ä¸èå¤è§å工使¶æ®µæ±äº¤é计ç®ï¼ç²¾ç¡®å° 0.01 å°æ¶ã |
| | | */ |
| | | private BigDecimal leaveHours; |
| | | |
| | | /** |
| | | * å½å¤©è¯·åç±»åï¼éå·åé |
| | | * |
| | | * å¯¹åº HrmLeaveTypeEnum ç typeï¼å¦ "1,2" 表示å½å¤©æ¢è¯·äºäºåå请äºç
åã |
| | | */ |
| | | private String leaveTypes; |
| | | |
| | | /** |
| | | * æå¡å°ç¹ |
| | | */ |
| | | private String location; |
| | |
| | | * @param endTime ç»ææ¶é´ |
| | | * @return 请åè®°å½å表 |
| | | */ |
| | | /** |
| | | * æ¥è¯¢ä¸æå®æ¶é´åºé´ãæäº¤éãç请åå |
| | | * |
| | | * <p>è¯ä¹ä¸ selectApprovedListByUserIdAndTimeRange ä¸åï¼é£è¾¹è¦æ±è¯·åå宿´è½å¨åºé´å
ï¼ |
| | | * è¿éåªè¦æéå å°±ç®å½ä¸ãèå¤å¤æãæå¤©æ¯å¦è¯·åãå¿
é¡»ç¨è¿ä¸ªï¼ |
| | | * 跨天ç请ååï¼å¦ 9/24 18:00 ~ 9/25 18:00ï¼å¨ 9/25 è¿å¤©ä¹ç®è¯·åã</p> |
| | | * |
| | | * <p>äº¤éæ¡ä»¶ï¼start_time < rangeEnd ä¸ end_time >= rangeStart</p> |
| | | * |
| | | * @param userId åå·¥IDï¼hrm_leave_application.user_idï¼ |
| | | * @param status 审æ¹ç¶æï¼ä¼ null ä¸è¿æ»¤ |
| | | * @param rangeStart åºé´å¼å§ï¼å«ï¼ |
| | | * @param rangeEnd åºé´ç»æï¼ä¸å«ï¼ |
| | | * @return ä¸åºé´æäº¤éç请åå |
| | | */ |
| | | default List<HrmLeaveApplicationDO> selectListByUserIdAndOverlapDate( |
| | | Long userId, Integer status, LocalDateTime rangeStart, LocalDateTime rangeEnd) { |
| | | return selectList(new LambdaQueryWrapperX<HrmLeaveApplicationDO>() |
| | | .eq(HrmLeaveApplicationDO::getUserId, userId) |
| | | .eqIfPresent(HrmLeaveApplicationDO::getStatus, status) |
| | | .lt(HrmLeaveApplicationDO::getStartTime, rangeEnd) |
| | | .ge(HrmLeaveApplicationDO::getEndTime, rangeStart) |
| | | .orderByAsc(HrmLeaveApplicationDO::getStartTime)); |
| | | } |
| | | |
| | | default List<HrmLeaveApplicationDO> selectApprovedListByUserIdAndTimeRange(Long userId, LocalDateTime startTime, LocalDateTime endTime) { |
| | | return selectList(new LambdaQueryWrapperX<HrmLeaveApplicationDO>() |
| | | .eq(HrmLeaveApplicationDO::getUserId, userId) |
| | |
| | | .leIfPresent(HrmAttendanceRecordDO::getDate, reqVO.getEndDate()) |
| | | .eqIfPresent(HrmAttendanceRecordDO::getClockInType, reqVO.getClockInType()) |
| | | .eqIfPresent(HrmAttendanceRecordDO::getClockOutType, reqVO.getClockOutType()) |
| | | .eqIfPresent(HrmAttendanceRecordDO::getLeaveFlag, reqVO.getLeaveFlag()) |
| | | .orderByDesc(HrmAttendanceRecordDO::getDate)); |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.hrm.service.approval; |
| | | |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO; |
| | | import java.time.LocalDate; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * è¯·åæ¥è¯¢æå¡ï¼ä¾è夿¨¡å夿æåå·¥æå¤©æ¯å¦è¯·å |
| | | * |
| | | * <p>ç¬ç«äº {@link HrmLeaveApplicationService}ï¼é¿å
è夿¨¡åä¾èµå®¡æ¹æ¨¡åçåæä½ï¼ |
| | | * åªæ´é²åªè¯»æ¥è¯¢ï¼ä¾¿äºåæµåå¤ç¨ã</p> |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | public interface HrmLeaveQueryService { |
| | | |
| | | /** |
| | | * æ¥è¯¢æå工卿宿¥æå
ã审æ¹éè¿ç请åå |
| | | * |
| | | * <p>åªè¿å status = 审æ¹éè¿(20) çåæ®ï¼é©³åãæ¤éã审æ¹ä¸é½ä¸ç®ã</p> |
| | | * |
| | | * @param userId åå·¥ç¼å·ï¼å¯¹åº hrm_leave_application.user_idï¼ |
| | | * @param date æ¥æ |
| | | * @return è¦çè¯¥æ¥æç请åååè¡¨ï¼æ å空å表 |
| | | */ |
| | | List<HrmLeaveApplicationDO> getApprovedLeaveList(Long userId, LocalDate date); |
| | | |
| | | /** |
| | | * 夿æåå·¥æå¤©æ¯å¦è¯·åï¼æ¯å¦æå®¡æ¹éè¿ç请ååè¦çè¯¥å¤©ï¼ |
| | | */ |
| | | default boolean isOnLeave(Long userId, LocalDate date) { |
| | | return !getApprovedLeaveList(userId, date).isEmpty(); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.hrm.service.approval; |
| | | |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper; |
| | | import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum; |
| | | import jakarta.annotation.Resource; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * è¯·åæ¥è¯¢æå¡å®ç° |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Service |
| | | public class HrmLeaveQueryServiceImpl implements HrmLeaveQueryService { |
| | | |
| | | @Resource |
| | | private HrmLeaveApplicationMapper leaveApplicationMapper; |
| | | |
| | | @Override |
| | | public List<HrmLeaveApplicationDO> getApprovedLeaveList(Long userId, LocalDate date) { |
| | | if (userId == null || date == null) { |
| | | return List.of(); |
| | | } |
| | | // 请ååä¸å½å¤©æäº¤éå³ä¸ºå½ä¸ï¼start_time < æ¬¡æ¥ 00:00 ä¸ end_time >= å½å¤© 00:00 |
| | | LocalDateTime dayStart = date.atStartOfDay(); |
| | | LocalDateTime dayEnd = date.plusDays(1).atStartOfDay(); |
| | | return leaveApplicationMapper.selectListByUserIdAndOverlapDate( |
| | | userId, HrmAuditStatusEnum.APPROVE.getStatus(), dayStart, dayEnd); |
| | | } |
| | | |
| | | } |
| | |
| | | private HrmAttendanceHolidayService attendanceHolidayService; |
| | | @Resource |
| | | private DeptApi deptApi; |
| | | @Resource |
| | | private cn.iocoder.yudao.module.hrm.service.approval.HrmLeaveQueryService leaveQueryService; |
| | | |
| | | @Override |
| | | public PageResult<HrmAttendanceRecordDO> getAttendanceRecordPage(HrmAttendanceRecordPageReqVO pageReqVO) { |
| | |
| | | } |
| | | |
| | | // 4. 彿¥èå¤è®°å½ |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(employee.getId(), queryDate); |
| | | // èå¤è®°å½è¡¨åçæ¯ç³»ç»ç¨æ· IDï¼è¿éç¨ loginUserId è䏿¯åå·¥æ¡£æ¡ ID |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(loginUserId, queryDate); |
| | | if (record != null) { |
| | | respVO.setRecord(buildRecordRespVO(record, employee)); |
| | | fillCompatibleRecordFields(respVO, record); |
| | | } |
| | | |
| | | // 4.5 å½å¤©è¯·åæ
åµï¼è¯·åä»å
许æå¡ï¼ä½ä¸ç®è¿å°/æ©é/缺å¡ï¼ |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves = |
| | | leaveQueryService.getApprovedLeaveList(loginUserId, queryDate); |
| | | boolean onLeave = !leaves.isEmpty(); |
| | | java.math.BigDecimal leaveHours = onLeave ? calcLeaveHours(leaves, rule, queryDate) : java.math.BigDecimal.ZERO; |
| | | respVO.setOnLeave(onLeave); |
| | | respVO.setLeaveHours(leaveHours); |
| | | if (onLeave) { |
| | | List<String> typeNames = leaves.stream() |
| | | .map(l -> cn.iocoder.yudao.module.hrm.enums.HrmLeaveTypeEnum.getNameByStatus(l.getLeaveType())) |
| | | .distinct().toList(); |
| | | respVO.setLeaveTypeNames(String.join("ã", typeNames)); |
| | | } |
| | | |
| | | // 5. å½å坿å¡ç¶æ |
| | | HrmAttendanceMyStatusRespVO.Clockable clockable = buildClockable(workDay, rule, record, queryDate); |
| | | HrmAttendanceMyStatusRespVO.Clockable clockable = buildClockable(workDay, rule, record, queryDate, onLeave); |
| | | |
| | | // 5.1 请åå½å¤©ï¼æã坿å¡ãçæç¤ºæ¢æè¯·å说æï¼ä½ä¾ç¶å
许æå¡ |
| | | if (onLeave) { |
| | | clockable.setMessage("仿¥å·²è¯·åï¼" + respVO.getLeaveTypeNames() + "ï¼ï¼å¦éä¸ç坿£å¸¸æå¡"); |
| | | } |
| | | respVO.setClockable(clockable); |
| | | respVO.setClockInDone(record != null && record.getClockInTime() != null); |
| | | respVO.setClockOutDone(record != null && record.getClockOutTime() != null); |
| | |
| | | respVO.setOvertimeHours(record.getOvertimeHours()); |
| | | respVO.setLocation(record.getLocation()); |
| | | respVO.setDataSource(record.getDataSource()); |
| | | } |
| | | |
| | | /** |
| | | * æè¯·åä¿¡æ¯åå
¥èå¤è®°å½ï¼leave_flag / leave_hours / leave_typesï¼ |
| | | * |
| | | * @param record èå¤è®°å½ |
| | | * @param leaves å½å¤©å®¡æ¹éè¿ç请åå |
| | | * @param rule èå¤è§åï¼ç¨äºè®¡ç®å·¥ä½æ¶æ®µäº¤éï¼ |
| | | * @param date è夿¥æ |
| | | */ |
| | | private void applyLeaveInfo(HrmAttendanceRecordDO record, |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves, |
| | | HrmAttendanceRuleDO rule, LocalDate date) { |
| | | if (leaves == null || leaves.isEmpty()) { |
| | | record.setLeaveFlag(false); |
| | | record.setLeaveHours(BigDecimal.ZERO); |
| | | record.setLeaveTypes(null); |
| | | return; |
| | | } |
| | | record.setLeaveFlag(true); |
| | | record.setLeaveHours(calcLeaveHours(leaves, rule, date)); |
| | | record.setLeaveTypes(leaves.stream() |
| | | .map(l -> String.valueOf(l.getLeaveType())) |
| | | .distinct() |
| | | .collect(java.util.stream.Collectors.joining(","))); |
| | | } |
| | | |
| | | /** |
| | | * è®¡ç®æå¤©ç请åå·¥æ¶ |
| | | * |
| | | * <p>ææ¯å¼ è¯·ååç [start_time, end_time] ä¸èå¤è§åç工使¶æ®µæ±äº¤éåç´¯å ï¼ |
| | | * ç²¾ç¡®å° 0.01 å°æ¶ï¼36 ç§ï¼ãæ è§åæ¶ææ´å¤©çè¯·åæ¶é¿æç®ã</p> |
| | | * |
| | | * <p>ä¾ï¼è§å 08:30-17:30ï¼è¯·åå 14:00-18:00 â 交é 14:00-17:30 = 3.5 å°æ¶ã</p> |
| | | */ |
| | | private java.math.BigDecimal calcLeaveHours( |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves, |
| | | HrmAttendanceRuleDO rule, LocalDate date) { |
| | | LocalDateTime dayStart = date.atStartOfDay(); |
| | | LocalDateTime dayEnd = date.plusDays(1).atStartOfDay(); |
| | | // 工使¥æ¶æ®µï¼ä¼å
ç¨èå¤è§åï¼æ²¡é
è§åå°±ææ´å¤© |
| | | LocalDateTime workStart; |
| | | LocalDateTime workEnd; |
| | | if (rule != null && rule.getWorkStartTime() != null && rule.getWorkEndTime() != null) { |
| | | workStart = LocalDateTime.of(date, rule.getWorkStartTime()); |
| | | workEnd = LocalDateTime.of(date, rule.getWorkEndTime()); |
| | | // è·¨é¶ç¹ç次ï¼å¦ 20:00-æ¬¡æ¥ 04:00ï¼ |
| | | if (!workEnd.isAfter(workStart)) { |
| | | workEnd = workEnd.plusDays(1); |
| | | } |
| | | } else { |
| | | workStart = dayStart; |
| | | workEnd = dayEnd; |
| | | } |
| | | java.math.BigDecimal total = java.math.BigDecimal.ZERO; |
| | | for (var leave : leaves) { |
| | | if (leave.getStartTime() == null || leave.getEndTime() == null) { |
| | | continue; |
| | | } |
| | | LocalDateTime from = leave.getStartTime().isAfter(workStart) ? leave.getStartTime() : workStart; |
| | | LocalDateTime to = leave.getEndTime().isBefore(workEnd) ? leave.getEndTime() : workEnd; |
| | | if (!to.isAfter(from)) { |
| | | continue; |
| | | } |
| | | long minutes = java.time.Duration.between(from, to).toMinutes(); |
| | | total = total.add(java.math.BigDecimal.valueOf(minutes) |
| | | .divide(java.math.BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP)); |
| | | } |
| | | return total; |
| | | } |
| | | |
| | | /** |
| | |
| | | * æ ¹æ®å·¥ä½æ¥ãèå¤è§åä¸å½æ¥è®°å½è®¡ç®å¯æå¡ç¶æ |
| | | */ |
| | | private HrmAttendanceMyStatusRespVO.Clockable buildClockable(boolean workDay, HrmAttendanceRuleDO rule, |
| | | HrmAttendanceRecordDO record, LocalDate date) { |
| | | HrmAttendanceRecordDO record, LocalDate date, |
| | | boolean onLeave) { |
| | | HrmAttendanceMyStatusRespVO.Clockable clockable = new HrmAttendanceMyStatusRespVO.Clockable(); |
| | | if (!workDay) { |
| | | clockable.setClockIn(false); |
| | |
| | | clockable.setClockOut(false); |
| | | clockable.setNextAction("in"); |
| | | clockable.setMessage("å¯ä¸çæå¡"); |
| | | // ä»
å½å¤©æ¥è¯¢æ¶æç¤ºå·²è¿å°æ¶é¿ |
| | | // ä»
å½å¤©æ¥è¯¢æ¶æç¤ºå·²è¿å°æ¶é¿ï¼è¯·åå½å¤©ä¸æç¤ºè¿å° |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | if (rule.getWorkStartTime() != null && date.equals(now.toLocalDate())) { |
| | | if (!onLeave && rule.getWorkStartTime() != null && date.equals(now.toLocalDate())) { |
| | | LocalDateTime workStart = LocalDateTime.of(date, rule.getWorkStartTime()); |
| | | int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0; |
| | | if (now.isAfter(workStart.plusMinutes(lateAllowMinutes))) { |
| | |
| | | throw exception(ATTENDANCE_CLOCK_FAIL_NOT_WORKDAY); |
| | | } |
| | | // 2. è·åå工信æ¯åèå¤è§å |
| | | // 注æï¼å
¥å userId å®é
ä¼ çæ¯ãåå·¥æ¡£æ¡ IDãï¼ |
| | | // èèå¤è®°å½è¡¨ / 请å表ç user_id åçæ¯ãç³»ç»ç¨æ· IDãï¼å¿
é¡»åºåã |
| | | HrmEmployeeDO employee = employeeService.getEmployee(userId); |
| | | if (employee == null) { |
| | | throw exception(EMPLOYEE_NOT_EXISTS); |
| | | } |
| | | Long systemUserId = employee.getUserId(); |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId()); |
| | | if (rule == null) { |
| | | throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE); |
| | | } |
| | | // 3. æ¥è¯¢å½æ¥èå¤è®°å½ |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate); |
| | | // 3. æ¥è¯¢å½æ¥èå¤è®°å½ï¼ç¨ç³»ç»ç¨æ· IDï¼ |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(systemUserId, clockDate); |
| | | if (record != null && record.getClockInTime() != null) { |
| | | throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED); |
| | | } |
| | | // å¨ä»»ä½ååºæä½å宿å®ä½æ ¡éªï¼æ§çå°ç¹å符串ä¸è½ç»è¿èå´éå¶ã |
| | | ClockLocation clockLocation = validateClockLocation(rule, reqVO); |
| | | // 3.1 å½å¤©è¯·åæ
åµï¼è¯·åå½å¤©å
许æå¡ï¼ä½ä¸å¤å®ä¸ºè¿å° |
| | | // 注æï¼æ¹æ³å
¥å userId å®ä¸ºãåå·¥æ¡£æ¡ IDãï¼è请å表åçæ¯ãç³»ç»ç¨æ· IDãï¼ |
| | | // å¿
é¡»ç¨ employee.getUserId() æè½å¯¹ä¸ã |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves = |
| | | leaveQueryService.getApprovedLeaveList(employee.getUserId(), clockDate); |
| | | boolean onLeave = !leaves.isEmpty(); |
| | | // 4. 夿æå¡ç±»å |
| | | LocalTime workStartTime = rule.getWorkStartTime(); |
| | | int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0; |
| | | LocalTime lateThreshold = workStartTime.plusMinutes(lateAllowMinutes); |
| | | LocalTime clockTimeOnly = clockTime.toLocalTime(); |
| | | Integer clockInType = clockTimeOnly.isBefore(lateThreshold) || clockTimeOnly.equals(lateThreshold) |
| | | ? HrmAttendanceClockTypeEnum.NORMAL.getType() |
| | | : HrmAttendanceClockTypeEnum.LATE.getType(); |
| | | Integer clockInType; |
| | | if (onLeave) { |
| | | // 请åå½å¤©ï¼åªè¦ææå¡å°±ç®æ£å¸¸ï¼è¿å°ä¸å¦ä¸å½±åèå¤ |
| | | clockInType = HrmAttendanceClockTypeEnum.NORMAL.getType(); |
| | | } else { |
| | | clockInType = clockTimeOnly.isBefore(lateThreshold) || clockTimeOnly.equals(lateThreshold) |
| | | ? HrmAttendanceClockTypeEnum.NORMAL.getType() |
| | | : HrmAttendanceClockTypeEnum.LATE.getType(); |
| | | } |
| | | // 5. åå»ºææ´æ°èå¤è®°å½ |
| | | if (record == null) { |
| | | record = HrmAttendanceRecordDO.builder() |
| | | .userId(userId) |
| | | .userId(systemUserId) |
| | | .deptId(employee.getDeptId()) |
| | | .date(clockDate) |
| | | .clockInTime(clockTime) |
| | | .clockInType(clockInType) |
| | | .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType()) |
| | | .build(); |
| | | applyLeaveInfo(record, leaves, rule, clockDate); |
| | | applyClockLocation(record, rule, clockLocation, legacyLocation, true); |
| | | attendanceRecordMapper.insert(record); |
| | | } else { |
| | | record.setClockInTime(clockTime); |
| | | record.setClockInType(clockInType); |
| | | applyLeaveInfo(record, leaves, rule, clockDate); |
| | | applyClockLocation(record, rule, clockLocation, legacyLocation, true); |
| | | attendanceRecordMapper.updateById(record); |
| | | } |
| | |
| | | if (employee == null) { |
| | | throw exception(EMPLOYEE_NOT_EXISTS); |
| | | } |
| | | Long systemUserId = employee.getUserId(); |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId()); |
| | | if (rule == null) { |
| | | throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE); |
| | | } |
| | | // 3. æ¥è¯¢å½æ¥èå¤è®°å½ |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate); |
| | | // 3. æ¥è¯¢å½æ¥èå¤è®°å½ï¼ç³»ç»ç¨æ· IDï¼ |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(systemUserId, clockDate); |
| | | if (record != null && record.getClockOutTime() != null) { |
| | | throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED); |
| | | } |
| | |
| | | ClockLocation clockLocation = validateClockLocation(rule, reqVO); |
| | | if (record == null) { |
| | | record = HrmAttendanceRecordDO.builder() |
| | | .userId(userId) |
| | | .userId(systemUserId) |
| | | .deptId(employee.getDeptId()) |
| | | .date(clockDate) |
| | | .clockInType(HrmAttendanceClockTypeEnum.MISSING.getType()) |
| | | .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType()) |
| | | .build(); |
| | | } |
| | | // 3.1 å½å¤©è¯·åæ
åµï¼è¯·åå½å¤©å
许æå¡ï¼ä¸å¤å®ä¸ºæ©éï¼ä¹ä¸ç®ç¼ºä¸çå¡ |
| | | // åä¸ï¼ç¨ç³»ç»ç¨æ· ID æ¥è¯·å表 |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves = |
| | | leaveQueryService.getApprovedLeaveList(employee.getUserId(), clockDate); |
| | | boolean onLeave = !leaves.isEmpty(); |
| | | if (onLeave && HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType())) { |
| | | // 请åå½å¤©æ²¡æä¸çå¡å±äºæ£å¸¸ï¼äººæ²¡æ¥ï¼ï¼ä¸åºè®°ç¼ºå¡ |
| | | record.setClockInType(HrmAttendanceClockTypeEnum.LEAVE.getType()); |
| | | } |
| | | // 4. 夿æå¡ç±»å |
| | | LocalTime workEndTime = rule.getWorkEndTime(); |
| | | int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0; |
| | | LocalTime earlyThreshold = workEndTime.minusMinutes(earlyAllowMinutes); |
| | | LocalTime clockTimeOnly = clockTime.toLocalTime(); |
| | | Integer clockOutType = clockTimeOnly.isAfter(earlyThreshold) || clockTimeOnly.equals(earlyThreshold) |
| | | ? HrmAttendanceClockTypeEnum.NORMAL.getType() |
| | | : HrmAttendanceClockTypeEnum.EARLY.getType(); |
| | | Integer clockOutType; |
| | | if (onLeave) { |
| | | // 请åå½å¤©ï¼ææå¡å°±ç®æ£å¸¸ |
| | | clockOutType = HrmAttendanceClockTypeEnum.NORMAL.getType(); |
| | | } else { |
| | | clockOutType = clockTimeOnly.isAfter(earlyThreshold) || clockTimeOnly.equals(earlyThreshold) |
| | | ? HrmAttendanceClockTypeEnum.NORMAL.getType() |
| | | : HrmAttendanceClockTypeEnum.EARLY.getType(); |
| | | } |
| | | // 5. æ´æ°èå¤è®°å½ |
| | | record.setClockOutTime(clockTime); |
| | | record.setClockOutType(clockOutType); |
| | | applyLeaveInfo(record, leaves, rule, clockDate); |
| | | applyClockLocation(record, rule, clockLocation, legacyLocation, false); |
| | | // 6. 计ç®å·¥ä½æ¶é¿ |
| | | if (record.getClockInTime() != null) { |
| | |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceSummaryPageReqVO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceSummaryDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceRecordMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceSummaryMapper; |
| | | import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceClockTypeEnum; |
| | | import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum; |
| | | import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.YearMonth; |
| | | import java.util.List; |
| | | |
| | |
| | | private HrmEmployeeService employeeService; |
| | | @Resource |
| | | private HrmAttendanceHolidayService attendanceHolidayService; |
| | | @Resource |
| | | private cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper leaveApplicationMapper; |
| | | @Resource |
| | | private HrmAttendanceRuleService attendanceRuleService; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | // 3. æ¥è¯¢è¯¥æææèå¤è®°å½ |
| | | List<HrmAttendanceRecordDO> records = attendanceRecordMapper.selectListByUserIdAndDateBetween(userId, startDate, endDate); |
| | | |
| | | // 3.1 æ¥è¯¢è¯¥æææå®¡æ¹éè¿ç请åå |
| | | // èå¤è®°å½åªå¨ãæå¡ãæ¶æå建ï¼è¯·åå½å¤©æ²¡æ¥ä¸çå°±ä¸ä¼æè®°å½ï¼ |
| | | // æä»¥å¿
é¡»åç¬æ¥è¯·ååï¼å¦å leaveHours 伿¼ç®ã |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> monthLeaves = |
| | | leaveApplicationMapper.selectListByUserIdAndOverlapDate( |
| | | userId, HrmAuditStatusEnum.APPROVE.getStatus(), |
| | | startDate.atStartOfDay(), endDate.plusDays(1).atStartOfDay()); |
| | | |
| | | // 4. 计ç®åºåºå¤å¤©æ°ï¼å·¥ä½æ¥æ°éï¼ |
| | | int workDays = calculateWorkDays(startDate, endDate); |
| | | |
| | |
| | | int earlyCount = 0; |
| | | int absentCount = 0; |
| | | BigDecimal overtimeHours = BigDecimal.ZERO; |
| | | BigDecimal leaveHours = BigDecimal.ZERO; |
| | | int actualDays = 0; |
| | | |
| | | for (HrmAttendanceRecordDO record : records) { |
| | | // ç»è®¡è¿å° |
| | | if (HrmAttendanceClockTypeEnum.LATE.getType().equals(record.getClockInType())) { |
| | | lateCount++; |
| | | // è¯·åæ è®°ï¼å®¡æ¹éè¿ç请ååè½åºæ¶å·²åå
¥ record.leaveFlag |
| | | boolean onLeave = Boolean.TRUE.equals(record.getLeaveFlag()); |
| | | // 累计请åå·¥æ¶ |
| | | if (onLeave && record.getLeaveHours() != null) { |
| | | leaveHours = leaveHours.add(record.getLeaveHours()); |
| | | } |
| | | // ç»è®¡æ©é |
| | | if (HrmAttendanceClockTypeEnum.EARLY.getType().equals(record.getClockOutType())) { |
| | | earlyCount++; |
| | | } |
| | | // ç»è®¡ç¼ºå¡ |
| | | if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType())) { |
| | | absentCount++; |
| | | } |
| | | if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockOutType())) { |
| | | absentCount++; |
| | | // 请åå½å¤©ä¸è®¡è¿å°ãæ©éã缺å¡ï¼è¯·åç±»åæ¬èº«æ¯ LEAVEï¼ä¸ä¼å½ä¸ä¸é¢çå¤æï¼ |
| | | if (!onLeave) { |
| | | // ç»è®¡è¿å° |
| | | if (HrmAttendanceClockTypeEnum.LATE.getType().equals(record.getClockInType())) { |
| | | lateCount++; |
| | | } |
| | | // ç»è®¡æ©é |
| | | if (HrmAttendanceClockTypeEnum.EARLY.getType().equals(record.getClockOutType())) { |
| | | earlyCount++; |
| | | } |
| | | // ç»è®¡ç¼ºå¡ï¼è¯·åå½å¤©æ²¡æå¡å±æ£å¸¸ï¼ä¸ç®ç¼ºå¡ |
| | | if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType())) { |
| | | absentCount++; |
| | | } |
| | | if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockOutType())) { |
| | | absentCount++; |
| | | } |
| | | } |
| | | // 累计å çæ¶é¿ |
| | | if (record.getOvertimeHours() != null) { |
| | | overtimeHours = overtimeHours.add(record.getOvertimeHours()); |
| | | } |
| | | // ç»è®¡å®é
åºå¤å¤©æ°ï¼ææ£å¸¸æå¡è®°å½ï¼ |
| | | // ç»è®¡å®é
åºå¤å¤©æ°ï¼ææå¡è®°å½å³ç®ï¼çº¯è¯·åæªæå¡ä¸ç®åºå¤ï¼ |
| | | if (record.getClockInTime() != null || record.getClockOutTime() != null) { |
| | | actualDays++; |
| | | } |
| | | } |
| | | |
| | | // 5.1 è¡¥ç®ãæè¯·åå使²¡æèå¤è®°å½ãçæ¥å |
| | | // è¿ç±»æ
嵿¯è¯·åå½å¤©å®å
¨æ²¡æ¥æå¡ï¼ä¸ä¼çæ recordï¼ |
| | | // ä¸é¢å¾ªç¯è¦çä¸å°ï¼éè¦åç¬æè¯·åå累计工æ¶ã |
| | | if (!monthLeaves.isEmpty()) { |
| | | java.util.Set<LocalDate> recordDates = records.stream() |
| | | .map(HrmAttendanceRecordDO::getDate).collect(java.util.stream.Collectors.toSet()); |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId()); |
| | | // æå¤©å½ç»è¯·åå |
| | | java.util.Map<LocalDate, List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO>> byDate |
| | | = new java.util.LinkedHashMap<>(); |
| | | for (var leave : monthLeaves) { |
| | | if (leave.getStartTime() == null || leave.getEndTime() == null) { |
| | | continue; |
| | | } |
| | | LocalDate d = leave.getStartTime().toLocalDate(); |
| | | LocalDate last = leave.getEndTime().toLocalDate(); |
| | | if (last.isAfter(endDate)) { |
| | | last = endDate; |
| | | } |
| | | for (LocalDate cur = d; !cur.isAfter(last); cur = cur.plusDays(1)) { |
| | | if (cur.isBefore(startDate)) { |
| | | continue; |
| | | } |
| | | byDate.computeIfAbsent(cur, k -> new java.util.ArrayList<>()).add(leave); |
| | | } |
| | | } |
| | | for (var entry : byDate.entrySet()) { |
| | | // å·²æèå¤è®°å½çæ¥æï¼è¯·åå·¥æ¶å·²å¨ record.leaveHours éç®è¿ |
| | | if (recordDates.contains(entry.getKey())) { |
| | | continue; |
| | | } |
| | | leaveHours = leaveHours.add(calcLeaveHoursByDay(entry.getValue(), rule, entry.getKey())); |
| | | } |
| | | } |
| | | |
| | |
| | | .earlyCount(earlyCount) |
| | | .absentCount(absentCount) |
| | | .overtimeHours(overtimeHours) |
| | | .leaveHours(BigDecimal.ZERO) |
| | | .leaveHours(leaveHours) |
| | | .build(); |
| | | attendanceSummaryMapper.insert(summary); |
| | | } else { |
| | |
| | | summary.setEarlyCount(earlyCount); |
| | | summary.setAbsentCount(absentCount); |
| | | summary.setOvertimeHours(overtimeHours); |
| | | summary.setLeaveHours(leaveHours); // 请åå·¥æ¶ä¹è¦æ´æ°ï¼å¦åéç®æ¶æ°¸è¿åå¨ä¸ä¸æ¬¡ |
| | | attendanceSummaryMapper.updateById(summary); |
| | | } |
| | | } |
| | |
| | | /** |
| | | * 计ç®åºåºå¤å¤©æ°ï¼å·¥ä½æ¥æ°éï¼ |
| | | */ |
| | | /** |
| | | * è®¡ç®æä¸å¤©ç请åå·¥æ¶ï¼ä¸èå¤è®°å½éçç®æ³ä¿æä¸è´ï¼ |
| | | * |
| | | * <p>åæ¯å¼ è¯·ååä¸èå¤è§å工使¶æ®µç交éç´¯å ï¼ç²¾ç¡®å° 0.01 å°æ¶ã</p> |
| | | */ |
| | | private BigDecimal calcLeaveHoursByDay( |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves, |
| | | HrmAttendanceRuleDO rule, LocalDate date) { |
| | | LocalDateTime workStart; |
| | | LocalDateTime workEnd; |
| | | if (rule != null && rule.getWorkStartTime() != null && rule.getWorkEndTime() != null) { |
| | | workStart = LocalDateTime.of(date, rule.getWorkStartTime()); |
| | | workEnd = LocalDateTime.of(date, rule.getWorkEndTime()); |
| | | if (!workEnd.isAfter(workStart)) { |
| | | workEnd = workEnd.plusDays(1); |
| | | } |
| | | } else { |
| | | workStart = date.atStartOfDay(); |
| | | workEnd = date.plusDays(1).atStartOfDay(); |
| | | } |
| | | BigDecimal total = BigDecimal.ZERO; |
| | | for (var leave : leaves) { |
| | | if (leave.getStartTime() == null || leave.getEndTime() == null) { |
| | | continue; |
| | | } |
| | | LocalDateTime from = leave.getStartTime().isAfter(workStart) ? leave.getStartTime() : workStart; |
| | | LocalDateTime to = leave.getEndTime().isBefore(workEnd) ? leave.getEndTime() : workEnd; |
| | | if (!to.isAfter(from)) { |
| | | continue; |
| | | } |
| | | long minutes = java.time.Duration.between(from, to).toMinutes(); |
| | | total = total.add(BigDecimal.valueOf(minutes) |
| | | .divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP)); |
| | | } |
| | | return total; |
| | | } |
| | | |
| | | private int calculateWorkDays(LocalDate startDate, LocalDate endDate) { |
| | | int workDays = 0; |
| | | LocalDate current = startDate; |