feat(hrm,bi): 移植长辉电力的考勤请假联动与 BI 大屏图表
参考 dev_新疆_长辉 分支移植(与康意分叉仅 17 个提交):
考勤与请假打通、我的考勤状态、员工选择器 API,以及人资办公/
采购销售大屏的出勤与库存采购图表。
后端 HRM(21 个文件):
- 考勤记录新增请假关联字段 leaveFlag/leaveHours/leaveTypes
- 新增 HrmLeaveQueryService:按日期区间查询员工请假
- 考勤记录服务:打卡/补卡与请假状态联动,导入模板与导入解析增强
- 新增 HrmEmployeeApi / HrmEmployeeSimpleDTO:供跨模块查询员工
- 考勤汇总服务:按请假数据修正应出勤/实际出勤口径
- 新增错误码 ATTENDANCE_EMPLOYEE_NOT_BOUND
- 删除已废弃的 HrmAttendanceClockRespVO
- 保留康意原有的加班/排班/班次与 500 空值修复
后端 BI:仅新增 SQL 种子脚本(无 Java 代码变更)
数据库:
- sql/mysql/hrm_attendance_leave_fields_20261030.sql(补 3 列,幂等)
- sql/mysql/bi_add_attendance_stock_20261030.sql(放开人资大屏菜单 + 新增图表,幂等)
已在生产库 101.245.92.159:9000/mom-kyzn 执行成功,执行前备份至
mom-kyzn-chdl-backup-20261030.sql。
Co-Authored-By: Claude <noreply@anthropic.com>
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ============================================================ |
| | | -- BI 大屿°å¢ï¼åºå¤ææ ï¼äººèµåå
¬å¤§å±ï¼+ åºåéè´ï¼éè´éå®å¤§å±ï¼ |
| | | -- çææ¶é´ï¼2026-10-30 |
| | | -- |
| | | -- 1. æ¾å¼ã人èµåå
¬å¤§å±ãèåï¼åæ¥ visible=0 éèï¼å¯¼è´çä¸å°ï¼ |
| | | -- 2. 人èµåå
¬å¤§å± å¢å åºå¤ç / è¿å° / ç¼ºå¡ / 请åå·¥æ¶ / åºå¤è¶å¿ |
| | | -- 3. éè´éå®å¤§å± å¢å åºåå¯ç¨é / åºååç§ / å·²å
¥åºé / åºåTOP10 |
| | | -- |
| | | -- å¾è¡¨çº¦å®ï¼number ç±»å SQL è¿ååå valueï¼bar/pie è¿å label + value |
| | | -- å¹çï¼æ name+dashboard_id å¤éï¼å¯é夿§è¡ |
| | | -- ============================================================ |
| | | |
| | | -- ---------- 1. æ¾å¼äººèµåå
¬å¤§å±èå ---------- |
| | | UPDATE system_menu SET visible = b'1', updater = '1', update_time = NOW() |
| | | WHERE id = 1063005 AND deleted = b'0'; |
| | | |
| | | -- ---------- 2. 人èµåå
¬å¤§å±ï¼åºå¤ææ ---------- |
| | | -- å¸å±ï¼number å¡çå ç¨ y=0 è¡ x=12..23ï¼åæ 4 å¼ åªç¨å° x=0..11ï¼ |
| | | |
| | | INSERT INTO `bi_chart_config` |
| | | (`dashboard_id`, `name`, `chart_type`, `data_source_id`, `query_sql`, `position`, |
| | | `chart_options`, `refresh_interval`, `sort`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT d.id, 'æ¬æåºå¤ç', 'number', NULL, |
| | | 'SELECT ROUND(SUM(actual_days)/NULLIF(SUM(work_days),0)*100,1) AS value FROM hrm_attendance_summary WHERE deleted=0 AND `year_month`=DATE_FORMAT(NOW(),''%Y-%m'')', |
| | | '{"h": 4, "w": 3, "x": 12, "y": 0}', NULL, 30, 20, 1, '1', NOW(), '1', NOW(), b'0' |
| | | FROM `bi_dashboard` d |
| | | WHERE d.`code` = 'hr_oa' AND d.deleted = b'0' |
| | | AND NOT EXISTS (SELECT 1 FROM `bi_chart_config` c WHERE c.`dashboard_id` = d.id AND c.`name` = 'æ¬æåºå¤ç' AND c.deleted = b'0'); |
| | | |
| | | INSERT INTO `bi_chart_config` |
| | | (`dashboard_id`, `name`, `chart_type`, `data_source_id`, `query_sql`, `position`, |
| | | `chart_options`, `refresh_interval`, `sort`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT d.id, 'æ¬æè¿å°æ¬¡æ°', 'number', NULL, |
| | | 'SELECT IFNULL(SUM(late_count),0) AS value FROM hrm_attendance_summary WHERE deleted=0 AND `year_month`=DATE_FORMAT(NOW(),''%Y-%m'')', |
| | | '{"h": 4, "w": 3, "x": 15, "y": 0}', NULL, 30, 21, 1, '1', NOW(), '1', NOW(), b'0' |
| | | FROM `bi_dashboard` d |
| | | WHERE d.`code` = 'hr_oa' AND d.deleted = b'0' |
| | | AND NOT EXISTS (SELECT 1 FROM `bi_chart_config` c WHERE c.`dashboard_id` = d.id AND c.`name` = 'æ¬æè¿å°æ¬¡æ°' AND c.deleted = b'0'); |
| | | |
| | | INSERT INTO `bi_chart_config` |
| | | (`dashboard_id`, `name`, `chart_type`, `data_source_id`, `query_sql`, `position`, |
| | | `chart_options`, `refresh_interval`, `sort`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT d.id, 'æ¬æç¼ºå¡æ¬¡æ°', 'number', NULL, |
| | | 'SELECT IFNULL(SUM(absent_count),0) AS value FROM hrm_attendance_summary WHERE deleted=0 AND `year_month`=DATE_FORMAT(NOW(),''%Y-%m'')', |
| | | '{"h": 4, "w": 3, "x": 18, "y": 0}', NULL, 30, 22, 1, '1', NOW(), '1', NOW(), b'0' |
| | | FROM `bi_dashboard` d |
| | | WHERE d.`code` = 'hr_oa' AND d.deleted = b'0' |
| | | AND NOT EXISTS (SELECT 1 FROM `bi_chart_config` c WHERE c.`dashboard_id` = d.id AND c.`name` = 'æ¬æç¼ºå¡æ¬¡æ°' AND c.deleted = b'0'); |
| | | |
| | | INSERT INTO `bi_chart_config` |
| | | (`dashboard_id`, `name`, `chart_type`, `data_source_id`, `query_sql`, `position`, |
| | | `chart_options`, `refresh_interval`, `sort`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT d.id, 'æ¬æè¯·åå·¥æ¶', 'number', NULL, |
| | | 'SELECT ROUND(IFNULL(SUM(leave_hours),0),1) AS value FROM hrm_attendance_summary WHERE deleted=0 AND `year_month`=DATE_FORMAT(NOW(),''%Y-%m'')', |
| | | '{"h": 4, "w": 3, "x": 21, "y": 0}', NULL, 30, 23, 1, '1', NOW(), '1', NOW(), b'0' |
| | | FROM `bi_dashboard` d |
| | | WHERE d.`code` = 'hr_oa' AND d.deleted = b'0' |
| | | AND NOT EXISTS (SELECT 1 FROM `bi_chart_config` c WHERE c.`dashboard_id` = d.id AND c.`name` = 'æ¬æè¯·åå·¥æ¶' AND c.deleted = b'0'); |
| | | |
| | | -- åºå¤è¶å¿ï¼æåº¦åºå¤çæçº¿ï¼å x=12..23 åºåï¼ |
| | | INSERT INTO `bi_chart_config` |
| | | (`dashboard_id`, `name`, `chart_type`, `data_source_id`, `query_sql`, `position`, |
| | | `chart_options`, `refresh_interval`, `sort`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT d.id, 'åºå¤çè¶å¿', 'line', NULL, |
| | | 'SELECT `year_month` AS label, ROUND(SUM(actual_days)/NULLIF(SUM(work_days),0)*100,1) AS value FROM hrm_attendance_summary WHERE deleted=0 GROUP BY `year_month` ORDER BY `year_month`', |
| | | '{"h": 8, "w": 12, "x": 12, "y": 4}', NULL, 60, 24, 1, '1', NOW(), '1', NOW(), b'0' |
| | | FROM `bi_dashboard` d |
| | | WHERE d.`code` = 'hr_oa' AND d.deleted = b'0' |
| | | AND NOT EXISTS (SELECT 1 FROM `bi_chart_config` c WHERE c.`dashboard_id` = d.id AND c.`name` = 'åºå¤çè¶å¿' AND c.deleted = b'0'); |
| | | |
| | | -- ---------- 3. éè´éå®å¤§å±ï¼åºåéè´ ---------- |
| | | INSERT INTO `bi_chart_config` |
| | | (`dashboard_id`, `name`, `chart_type`, `data_source_id`, `query_sql`, `position`, |
| | | `chart_options`, `refresh_interval`, `sort`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT d.id, 'åºåå¯ç¨æ»é', 'number', NULL, |
| | | 'SELECT ROUND(IFNULL(SUM(quantity-frozen_quantity-reserved_quantity),0),0) AS value FROM mes_wm_material_stock WHERE deleted=0 AND frozen=0', |
| | | '{"h": 4, "w": 3, "x": 12, "y": 0}', NULL, 60, 20, 1, '1', NOW(), '1', NOW(), b'0' |
| | | FROM `bi_dashboard` d |
| | | WHERE d.`code` = 'purchase_sales' AND d.deleted = b'0' |
| | | AND NOT EXISTS (SELECT 1 FROM `bi_chart_config` c WHERE c.`dashboard_id` = d.id AND c.`name` = 'åºåå¯ç¨æ»é' AND c.deleted = b'0'); |
| | | |
| | | INSERT INTO `bi_chart_config` |
| | | (`dashboard_id`, `name`, `chart_type`, `data_source_id`, `query_sql`, `position`, |
| | | `chart_options`, `refresh_interval`, `sort`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT d.id, 'åºååç§æ°', 'number', NULL, |
| | | 'SELECT COUNT(DISTINCT item_id) AS value FROM mes_wm_material_stock WHERE deleted=0', |
| | | '{"h": 4, "w": 3, "x": 15, "y": 0}', NULL, 60, 21, 1, '1', NOW(), '1', NOW(), b'0' |
| | | FROM `bi_dashboard` d |
| | | WHERE d.`code` = 'purchase_sales' AND d.deleted = b'0' |
| | | AND NOT EXISTS (SELECT 1 FROM `bi_chart_config` c WHERE c.`dashboard_id` = d.id AND c.`name` = 'åºååç§æ°' AND c.deleted = b'0'); |
| | | |
| | | INSERT INTO `bi_chart_config` |
| | | (`dashboard_id`, `name`, `chart_type`, `data_source_id`, `query_sql`, `position`, |
| | | `chart_options`, `refresh_interval`, `sort`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT d.id, 'å»ç»åºåé', 'number', NULL, |
| | | 'SELECT ROUND(IFNULL(SUM(frozen_quantity+reserved_quantity),0),0) AS value FROM mes_wm_material_stock WHERE deleted=0', |
| | | '{"h": 4, "w": 3, "x": 18, "y": 0}', NULL, 60, 22, 1, '1', NOW(), '1', NOW(), b'0' |
| | | FROM `bi_dashboard` d |
| | | WHERE d.`code` = 'purchase_sales' AND d.deleted = b'0' |
| | | AND NOT EXISTS (SELECT 1 FROM `bi_chart_config` c WHERE c.`dashboard_id` = d.id AND c.`name` = 'å»ç»åºåé' AND c.deleted = b'0'); |
| | | |
| | | INSERT INTO `bi_chart_config` |
| | | (`dashboard_id`, `name`, `chart_type`, `data_source_id`, `query_sql`, `position`, |
| | | `chart_options`, `refresh_interval`, `sort`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT d.id, 'ä¾åºåå°è´§æ¹æ¬¡', 'number', NULL, |
| | | 'SELECT COUNT(*) AS value FROM mes_wm_material_stock WHERE deleted=0 AND vendor_id IS NOT NULL', |
| | | '{"h": 4, "w": 3, "x": 21, "y": 0}', NULL, 60, 23, 1, '1', NOW(), '1', NOW(), b'0' |
| | | FROM `bi_dashboard` d |
| | | WHERE d.`code` = 'purchase_sales' AND d.deleted = b'0' |
| | | AND NOT EXISTS (SELECT 1 FROM `bi_chart_config` c WHERE c.`dashboard_id` = d.id AND c.`name` = 'ä¾åºåå°è´§æ¹æ¬¡' AND c.deleted = b'0'); |
| | | |
| | | -- åºå TOP10ï¼æ¨ªåæ±ç¶ï¼ |
| | | INSERT INTO `bi_chart_config` |
| | | (`dashboard_id`, `name`, `chart_type`, `data_source_id`, `query_sql`, `position`, |
| | | `chart_options`, `refresh_interval`, `sort`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) |
| | | SELECT d.id, 'åºåæ°é TOP10', 'bar', NULL, |
| | | 'SELECT i.name AS label, ROUND(SUM(s.quantity-s.frozen_quantity-s.reserved_quantity),0) AS value FROM mes_wm_material_stock s JOIN mes_md_item i ON i.id=s.item_id AND i.deleted=0 WHERE s.deleted=0 GROUP BY i.name ORDER BY value DESC LIMIT 10', |
| | | '{"h": 8, "w": 12, "x": 12, "y": 4}', NULL, 120, 24, 1, '1', NOW(), '1', NOW(), b'0' |
| | | FROM `bi_dashboard` d |
| | | WHERE d.`code` = 'purchase_sales' AND d.deleted = b'0' |
| | | AND NOT EXISTS (SELECT 1 FROM `bi_chart_config` c WHERE c.`dashboard_id` = d.id AND c.`name` = 'åºåæ°é TOP10' AND c.deleted = b'0'); |
| | | |
| | | -- ---------- 4. æ ¡éª ---------- |
| | | SELECT d.`code` AS 大å±, COUNT(*) AS å¾è¡¨æ° |
| | | FROM bi_chart_config c JOIN bi_dashboard d ON d.id = c.dashboard_id |
| | | WHERE d.`code` IN ('hr_oa','purchase_sales') AND c.deleted = b'0' |
| | | GROUP BY d.`code`; |
| | | |
| | | SELECT id, name, visible+0 AS vis FROM system_menu WHERE id = 1063005; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ============================================================================ |
| | | -- èå¤è®°å½è¡¥å
请åå
³èåæ®µï¼éé¿è¾çµååæ¯ç§»æ¤ï¼ |
| | | -- |
| | | -- ç¨éï¼èå¤è®°å½å±ç¤ºè¯·åç¶æã请åå·¥æ¶ï¼æå¡é¢æ¿æç¤ºå·²è¯·å |
| | | -- 对åºå®ä½ï¼HrmAttendanceRecordDO#leaveFlag / leaveHours / leaveTypes |
| | | -- ç¹æ§ï¼å¹çï¼å¯é夿§è¡ |
| | | -- ============================================================================ |
| | | |
| | | SET @exists := (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'hrm_attendance_record' |
| | | AND column_name = 'leave_flag'); |
| | | SET @sql := IF(@exists = 0, |
| | | "ALTER TABLE `hrm_attendance_record` ADD COLUMN `leave_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT 'å½å¤©æ¯å¦è¯·å'", |
| | | "SELECT 1"); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists := (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'hrm_attendance_record' |
| | | AND column_name = 'leave_hours'); |
| | | SET @sql := IF(@exists = 0, |
| | | "ALTER TABLE `hrm_attendance_record` ADD COLUMN `leave_hours` decimal(10,2) DEFAULT NULL COMMENT 'å½å¤©è¯·åå·¥æ¶'", |
| | | "SELECT 1"); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists := (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'hrm_attendance_record' |
| | | AND column_name = 'leave_types'); |
| | | SET @sql := IF(@exists = 0, |
| | | "ALTER TABLE `hrm_attendance_record` ADD COLUMN `leave_types` varchar(255) DEFAULT NULL COMMENT 'å½å¤©è¯·åç±»åï¼å¤ä¸ªä»¥éå·åé'", |
| | | "SELECT 1"); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.hrm.api.employee; |
| | | |
| | | import cn.iocoder.yudao.module.hrm.api.employee.dto.HrmEmployeeSimpleDTO; |
| | | |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * åå·¥æ¡£æ¡ API æ¥å£ |
| | | * |
| | | * <p>æä¾ç»å
¶ä»æ¨¡åè·¨æ¨¡åæ¥è¯¢å工精ç®ä¿¡æ¯ï¼ |
| | | * ä¾å¦å®å
¨ç产模åçç¹ç§ä½ä¸è¯éè¦å
³èæè¯äººã</p> |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | public interface HrmEmployeeApi { |
| | | |
| | | /** |
| | | * è·åå工精ç®ä¿¡æ¯ |
| | | * |
| | | * @param id åå·¥ç¼å· |
| | | * @return å工信æ¯ï¼ä¸åå¨è¿å null |
| | | */ |
| | | HrmEmployeeSimpleDTO getEmployee(Long id); |
| | | |
| | | /** |
| | | * æ¹éè·åå工精ç®ä¿¡æ¯ |
| | | * |
| | | * @param ids åå·¥ç¼å·éå |
| | | * @return å工信æ¯å表 |
| | | */ |
| | | List<HrmEmployeeSimpleDTO> getEmployeeList(Collection<Long> ids); |
| | | |
| | | /** |
| | | * æ ¡éªåå·¥æ¯å¦åå¨ï¼ä¸æªå é¤ï¼ |
| | | * |
| | | * @param id åå·¥ç¼å· |
| | | * @return æ¯å¦åå¨ |
| | | */ |
| | | boolean validateEmployeeExists(Long id); |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.hrm.api.employee.dto; |
| | | |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * å工精ç®ä¿¡æ¯ DTO |
| | | * |
| | | * <p>ä¾å
¶ä»æ¨¡åï¼å¦å®å
¨ç产模åçç¹ç§ä½ä¸è¯ï¼å
³èåå·¥æ¡£æ¡æ¶ä½¿ç¨ã</p> |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Data |
| | | public class HrmEmployeeSimpleDTO { |
| | | |
| | | /** |
| | | * åå·¥ç¼å· |
| | | */ |
| | | private Long id; |
| | | |
| | | /** |
| | | * å
³èçç³»ç»ç¨æ·ç¼å· |
| | | * |
| | | * å
³è AdminUserDO#getId() |
| | | */ |
| | | private Long userId; |
| | | |
| | | /** |
| | | * åå·¥ç¼å·ï¼å·¥å·ï¼ |
| | | */ |
| | | private String employeeNo; |
| | | |
| | | /** |
| | | * åå·¥å§å |
| | | */ |
| | | private String name; |
| | | |
| | | /** |
| | | * é¨é¨ç¼å· |
| | | */ |
| | | private Long deptId; |
| | | |
| | | /** |
| | | * é¨é¨åç§° |
| | | */ |
| | | private String deptName; |
| | | |
| | | /** |
| | | * ææºå·ç |
| | | */ |
| | | private String phone; |
| | | |
| | | /** |
| | | * åå·¥ç¶æï¼1-å¨èï¼2-è¯ç¨ï¼3-离è |
| | | */ |
| | | private Integer employeeStatus; |
| | | |
| | | } |
| | |
| | | ErrorCode ATTENDANCE_EXCEPTION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_004_015, "èå¤å¼å¸¸å®¡æ¹æµç¨ä¸åå¨"); |
| | | ErrorCode ATTENDANCE_CLOCK_ALREADY_CLOCKED = new ErrorCode(1_030_004_016, "å·²æè¿å¡ï¼è¯·å¿é夿å¡"); |
| | | ErrorCode ATTENDANCE_HOLIDAY_DATE_RANGE_INVALID = new ErrorCode(1_030_004_017, "ç»ææ¥æã{}ãä¸è½æ©äºå¼å§æ¥æã{}ãï¼æ æ³ä¿åãè¯·éæ°éæ©æ£ç¡®çæ¥æåºé´"); |
| | | /** |
| | | * ç»å½è´¦å·æªç»å®å工档æ¡ã |
| | | * |
| | | * 注æï¼ä¸è¦å¤ç¨éç¨ç EMPLOYEE_NOT_EXISTSï¼1_030_000_000ï¼å»ä¼ èªå®ä¹ææ¡ ââ |
| | | * 该é误ç çæ¶æ¯æ¯ãåå·¥ä¸åå¨ãï¼ä¸å« {} å ä½ç¬¦ï¼ServiceExceptionUtil 伿 |
| | | * ãåæ°è¿å¤ãè¦å并丢å¼ä¼ å
¥çææ¡ï¼ç¨æ·åªè½çå°ãåå·¥ä¸åå¨ãï¼ |
| | | * æ¿ä¸å°ã请è系管çåç»å®å工档æ¡ãè¿å¥å¯æä½çæç¤ºã |
| | | */ |
| | | ErrorCode ATTENDANCE_EMPLOYEE_NOT_BOUND = new ErrorCode(1_030_004_018, "å½åç»å½è´¦å·æªå
³èå工档æ¡ï¼æ æ³æå¡ã请è系管çåå¨ãå工管çãä¸å°è¯¥ç»å½è´¦å·ç»å®å°åå·¥"); |
| | | ErrorCode ATTENDANCE_LOCATION_RULE_INVALID = new ErrorCode(1_030_004_050, "èå¤è§åå°ç¹é
ç½®ä¸å®æ´ï¼è¯·è系管çå"); |
| | | ErrorCode ATTENDANCE_LOCATION_REQUIRED = new ErrorCode(1_030_004_051, "请å¼å¯å®ä½å¹¶æä¾ç»çº¬åº¦ã精度ååæ ç³»åæå¡"); |
| | | ErrorCode ATTENDANCE_LOCATION_COORDINATE_INVALID = new ErrorCode(1_030_004_052, "æå¡ç»çº¬åº¦è¶
åºææèå´"); |
| | |
| | | NORMAL(1, "æ£å¸¸"), |
| | | LATE(2, "è¿å°"), |
| | | EARLY(3, "æ©é"), |
| | | MISSING(4, "缺å¡"); |
| | | MISSING(4, "缺å¡"), |
| | | /** |
| | | * 请å |
| | | * |
| | | * å½å¤©æå®¡æ¹éè¿ç请ååæ¶ä¸ºè¯¥ç±»åã请åå½å¤©ä»å
许æå¡ï¼ |
| | | * 使å¡ä¸ä¼è¢«å¤å®ä¸ºè¿å°/æ©éï¼æªæå¡ä¹ä¸ä¼å¤å®ä¸ºç¼ºå¡ã |
| | | */ |
| | | LEAVE(5, "请å"); |
| | | |
| | | private final Integer type; |
| | | private final String name; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.hrm.api.employee; |
| | | |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.hrm.api.employee.dto.HrmEmployeeSimpleDTO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper; |
| | | import cn.iocoder.yudao.module.system.api.dept.DeptApi; |
| | | import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; |
| | | import jakarta.annotation.Resource; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | |
| | | /** |
| | | * åå·¥æ¡£æ¡ API å®ç°ç±» |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Service |
| | | public class HrmEmployeeApiImpl implements HrmEmployeeApi { |
| | | |
| | | @Resource |
| | | private HrmEmployeeMapper employeeMapper; |
| | | @Resource |
| | | private DeptApi deptApi; |
| | | |
| | | @Override |
| | | public HrmEmployeeSimpleDTO getEmployee(Long id) { |
| | | if (id == null) { |
| | | return null; |
| | | } |
| | | HrmEmployeeDO employee = employeeMapper.selectById(id); |
| | | if (employee == null) { |
| | | return null; |
| | | } |
| | | return build(employee, getDeptName(employee.getDeptId())); |
| | | } |
| | | |
| | | @Override |
| | | public List<HrmEmployeeSimpleDTO> getEmployeeList(Collection<Long> ids) { |
| | | if (ids == null || ids.isEmpty()) { |
| | | return List.of(); |
| | | } |
| | | List<HrmEmployeeDO> employees = employeeMapper.selectBatchIds(ids); |
| | | if (employees.isEmpty()) { |
| | | return List.of(); |
| | | } |
| | | // æ¹éæ¥é¨é¨ï¼é¿å
N+1 |
| | | List<Long> deptIds = employees.stream().map(HrmEmployeeDO::getDeptId) |
| | | .filter(Objects::nonNull).distinct().toList(); |
| | | Map<Long, DeptRespDTO> deptMap = deptIds.isEmpty() ? Map.of() : deptApi.getDeptMap(deptIds); |
| | | return employees.stream() |
| | | .map(e -> { |
| | | DeptRespDTO dept = deptMap.get(e.getDeptId()); |
| | | return build(e, dept == null ? null : dept.getName()); |
| | | }) |
| | | .toList(); |
| | | } |
| | | |
| | | @Override |
| | | public boolean validateEmployeeExists(Long id) { |
| | | return id != null && employeeMapper.selectById(id) != null; |
| | | } |
| | | |
| | | private String getDeptName(Long deptId) { |
| | | if (deptId == null) { |
| | | return null; |
| | | } |
| | | DeptRespDTO dept = deptApi.getDept(deptId); |
| | | return dept == null ? null : dept.getName(); |
| | | } |
| | | |
| | | private static HrmEmployeeSimpleDTO build(HrmEmployeeDO employee, String deptName) { |
| | | HrmEmployeeSimpleDTO dto = BeanUtils.toBean(employee, HrmEmployeeSimpleDTO.class); |
| | | dto.setDeptName(deptName); |
| | | return dto; |
| | | } |
| | | |
| | | } |
| | |
| | | import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; |
| | | import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceClockReqVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceClockRespVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordImportExcelVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordImportReqVO; |
| | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | | import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; |
| | | import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.EMPLOYEE_NOT_EXISTS; |
| | | import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.ATTENDANCE_EMPLOYEE_NOT_BOUND; |
| | | import static cn.iocoder.yudao.module.hrm.enums.LogRecordConstants.*; |
| | | |
| | | @Tag(name = "管çåå° - HRM èå¤è®°å½") |
| | |
| | | |
| | | @PostMapping("/clock-in") |
| | | @Operation(summary = "ä¸çæå¡") |
| | | @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')") |
| | | @ApiAccessLog(requestEnable = false) |
| | | public CommonResult<HrmAttendanceClockRespVO> clockIn( |
| | | public CommonResult<HrmAttendanceRecordRespVO> clockIn( |
| | | @Valid @RequestBody(required = false) HrmAttendanceClockReqVO reqVO, |
| | | @RequestParam(value = "location", required = false) String legacyLocation) { |
| | | Long loginUserId = getLoginUserId(); |
| | | HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId); |
| | | if (employee == null) { |
| | | throw exception(EMPLOYEE_NOT_EXISTS, |
| | | "å½åç»å½è´¦å·æªå
³èå工档æ¡ï¼æ æ³æå¡ã请è系管çåå¨ãå工管çãä¸å°è¯¥ç»å½è´¦å·ç»å®å°åå·¥ã"); |
| | | throw exception(ATTENDANCE_EMPLOYEE_NOT_BOUND); |
| | | } |
| | | HrmAttendanceClockResult result = attendanceRecordService.clockIn( |
| | | employee.getId(), LocalDateTime.now(), reqVO, legacyLocation); |
| | | return success(buildClockResultVO(result, employee)); |
| | | return success(buildClockRespVO(result.record(), employee)); |
| | | } |
| | | |
| | | @PostMapping("/clock-out") |
| | | @Operation(summary = "ä¸çæå¡") |
| | | @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')") |
| | | @ApiAccessLog(requestEnable = false) |
| | | public CommonResult<HrmAttendanceClockRespVO> clockOut( |
| | | public CommonResult<HrmAttendanceRecordRespVO> clockOut( |
| | | @Valid @RequestBody(required = false) HrmAttendanceClockReqVO reqVO, |
| | | @RequestParam(value = "location", required = false) String legacyLocation) { |
| | | Long loginUserId = getLoginUserId(); |
| | | HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId); |
| | | if (employee == null) { |
| | | throw exception(EMPLOYEE_NOT_EXISTS, |
| | | "å½åç»å½è´¦å·æªå
³èå工档æ¡ï¼æ æ³æå¡ã请è系管çåå¨ãå工管çãä¸å°è¯¥ç»å½è´¦å·ç»å®å°åå·¥ã"); |
| | | throw exception(ATTENDANCE_EMPLOYEE_NOT_BOUND); |
| | | } |
| | | HrmAttendanceClockResult result = attendanceRecordService.clockOut( |
| | | employee.getId(), LocalDateTime.now(), reqVO, legacyLocation); |
| | | return success(buildClockResultVO(result, employee)); |
| | | return success(buildClockRespVO(result.record(), employee)); |
| | | } |
| | | |
| | | @GetMapping("/my-status") |
| | | @Operation(summary = "è·å¾å½åç»å½äººçèå¤ç¶æ") |
| | | @Parameter(name = "date", description = "è夿¥æï¼æ ¼å¼ yyyy-MM-ddï¼é»è®¤æå¡å¨å½å¤©", example = "2026-10-17") |
| | | @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')") |
| | | public CommonResult<HrmAttendanceMyStatusRespVO> getMyStatus( |
| | | @RequestParam(value = "date", required = false) |
| | | @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) LocalDate date) { |
| | |
| | | if (dept != null) { |
| | | respVO.setDeptName(dept.getName()); |
| | | } |
| | | } |
| | | return respVO; |
| | | } |
| | | |
| | | private HrmAttendanceClockRespVO buildClockResultVO(HrmAttendanceClockResult result, |
| | | HrmEmployeeDO employee) { |
| | | HrmAttendanceClockRespVO respVO = new HrmAttendanceClockRespVO(); |
| | | respVO.setRecord(buildClockRespVO(result.record(), employee)); |
| | | respVO.setLocationName(result.rule().getLocationName()); |
| | | if (Boolean.TRUE.equals(result.rule().getLocationRequired())) { |
| | | respVO.setDistanceMeters(result.distanceMeters()); |
| | | respVO.setRadiusMeters(result.rule().getRadiusMeters()); |
| | | respVO.setWithinRange(true); |
| | | } |
| | | return respVO; |
| | | } |
| | |
| | | @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; |
| | |
| | | /** |
| | | * å¹´æï¼æ ¼å¼ï¼yyyy-MM |
| | | * |
| | | * 注æï¼year_month æ¯ MySQL ä¿çåï¼å¿
é¡»æ¾å¼å åå¼å·ï¼å¦å SELECT è¯å¥ä¼æ¥è¯æ³é误 |
| | | * YEAR_MONTH å¨ MySQL 8 䏿¯ä¿çå
³é®åï¼æ¥æåä½ï¼ï¼ |
| | | * ä¸å åå¼å·ä¼å¯¼è´ MyBatis-Plus çæç SQL æ¥ 1064 è¯æ³éè¯¯ï¼ |
| | | * å æ¤è¿éæ¾å¼æå®å¸¦åå¼å·çååã |
| | | */ |
| | | @TableField("`year_month`") |
| | | private String yearMonth; |
| | |
| | | * @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)); |
| | | } |
| | | |
| | |
| | | |
| | | /** |
| | | * BPM 请å审æ¹åç±»ç¼ç |
| | | * |
| | | * è¯·åæµç¨ç»ä¸å½å¨ãèå¤å¼å¸¸å®¡æ¹ãåç±»ä¸ï¼ä¸èå¤å¼å¸¸å
±ç¨åä¸ä¸ªå®¡æ¹åç±»ï¼ |
| | | * ä¸ååç¬å»º hrm_leave_approve åç±»ã |
| | | */ |
| | | String APPROVE_CATEGORY_CODE = "hrm_leave_approve"; |
| | | String APPROVE_CATEGORY_CODE = "hrm_attendance_approve"; |
| | | |
| | | /** |
| | | * å建请åç³è¯· |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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) { |
| | |
| | | // 1. ç»å½ç¨æ· -> åå·¥æ¡£æ¡ |
| | | HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId); |
| | | if (employee == null) { |
| | | throw exception(EMPLOYEE_NOT_EXISTS, |
| | | "å½åç»å½è´¦å·æªå
³èå工档æ¡ï¼æ æ³æå¡ã请è系管çåå¨ãå工管çãä¸å°è¯¥ç»å½è´¦å·ç»å®å°åå·¥ã"); |
| | | throw exception(ATTENDANCE_EMPLOYEE_NOT_BOUND); |
| | | } |
| | | HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO(); |
| | | respVO.setUserId(loginUserId); |
| | |
| | | } |
| | | |
| | | // 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; |