Procházet zdrojové kódy

港大排班考勤改造增加补卡功能:
1.增加补卡功能,上送日期,上班打卡,下班打卡时间支持补卡
2.优化考勤状态更新代码

xiexh před 3 dny
rodič
revize
89e9f00d7c

+ 157 - 33
ibps-provider-root/modules/provider-business/src/main/java/com/lc/ibps/components/employee/domain/AttendanceDetail.java

@@ -185,15 +185,21 @@ public class AttendanceDetail extends AbstractDomain<String, AttendanceDetailPo>
 			throw new Exception("考勤类型不能为空");
 		}
 		if (BeanUtils.isEmpty(clockType)) {
-			throw new Exception("打卡类型不能为空,请传入 clockType=1(上班) 或 2(下班)");
+			throw new Exception("打卡类型不能为空,请传入 clockType=1(上班) 或 2(下班)或 3(补卡)");
 		}
-		boolean isMorningClock;
+
+		boolean isMorningClock = false; //上班卡
+		boolean isClockOut = false;     //下班卡
+		boolean isMendCard = false;     //补卡
 		if ("1".equals(clockType)) {
 			isMorningClock = true;
 		} else if ("2".equals(clockType)) {
-			isMorningClock = false;
-		} else {
-			throw new Exception("无效的打卡类型,应为 1(上班) 或 2(下班)");
+			isClockOut = true;
+		} else if("3".equals(clockType)){
+			isMendCard = true;
+			validateInfo(po);
+		}else {
+			throw new Exception("无效的打卡类型,应为 1(上班) 或 2(下班)或 3(补卡)");
 		}
 		// 2. 查询用户信息(检查用户是否存在,并获取工号)
 		Map<String, String> userParam = new HashMap<>();
@@ -213,17 +219,15 @@ public class AttendanceDetail extends AbstractDomain<String, AttendanceDetailPo>
 		configParam.put("pai_ban_bu_men_", buMen);
 		configParam.put("pei_zhi_ming_chen", paiBanMingChen);
 		configParam.put("pai_ban_lei_xing_", banCiMing);
+		configParam.put("sheng_xiao_biao_z", "Y");
 		List<Map<String, Object>> configList = updateDataTableDao.selectDataTable("t_gdpbpzb", configParam, null);
 		if (BeanUtils.isEmpty(configList)) {
-			throw new Exception("考勤配置不存在");
+			throw new Exception("考勤配置不存在或者已经失效");
 		}
 		if (configList.size() > 1) {
 			throw new Exception("考勤配置存在多条记录,请联系管理员");
 		}
 		Map<String, Object> configMap = configList.get(0);
-		if ("N".equals(configMap.get("sheng_xiao_biao_z"))) {
-			throw new Exception("考勤配置已失效");
-		}
 		// 获取配置字段
 		String shangBanShiJianPz = (String) configMap.get("shang_ban_shi_jia");
 		String xiaBanShiJianPz = (String) configMap.get("xia_ban_shi_jian_");
@@ -250,13 +254,23 @@ public class AttendanceDetail extends AbstractDomain<String, AttendanceDetailPo>
 		LocalDateTime now = LocalDateTime.now();
 		LocalDate today = now.toLocalDate();
 		LocalDate banCiDate = today; // 默认
+		if(isMendCard){//补卡日期取上送日期
+			banCiDate = LocalDate.parse(po.getRiQi(),DATE_FORMATTER);
+		}
 
 		//和业务沟通后确认,跨日下班打卡只打上一日的下班卡
-		if (!isMorningClock && "Y".equals(shiFouKuaRiPz)) {
+		if (isClockOut && "Y".equals(shiFouKuaRiPz)) {
 			banCiDate = today.minusDays(1);
 		}
-		String riQi = banCiDate.format(DATE_FORMATTER);
-		po.setRiQi(riQi);
+		String ClockRiQi = banCiDate.format(DATE_FORMATTER);
+		String riQi = "";
+		if(!isMendCard){//上班卡和下班卡为当前日期
+			po.setRiQi(ClockRiQi);
+			riQi = ClockRiQi;
+		}else{
+			//补卡日期为系统上送日期
+			riQi = po.getRiQi();
+		}
 		po.setShiFouKuaRi(shiFouKuaRiPz);
 
 		// 构建班次开始时间
@@ -287,8 +301,8 @@ public class AttendanceDetail extends AbstractDomain<String, AttendanceDetailPo>
 			diDian = result.getData().get(0).getPath().split(StringPool.BACK_SLASH + StringPool.DOT)[1];
 			po.setDiDian(diDian);
 		}catch (Exception ex){
-			LOGGER.error("Can't get didian information",ex);
-			return null;
+			diDian = result.getData().get(0).getPath().split(StringPool.BACK_SLASH+StringPool.DOT)[0];
+			po.setDiDian(diDian);
 		}
 		// 5. 查询当天是否存在考勤记录
 		Map<String, String> recordParam = new HashMap<>();
@@ -304,6 +318,9 @@ public class AttendanceDetail extends AbstractDomain<String, AttendanceDetailPo>
 			if (recordList.size() > 1) {
 				throw new Exception("考勤记录存在多条,请联系管理员排查");
 			}
+			if(isMendCard){
+				throw new Exception("用户{"+userMap.get("NAME_")+"}在"+riQi+"日已经打卡,无需补卡");
+			}
 			// 更新对象,存储了用户当日打卡记录id
 			existingPo = new AttendanceDetailPo();
 			Map<String, Object> recordMap = recordList.get(0);
@@ -368,7 +385,7 @@ public class AttendanceDetail extends AbstractDomain<String, AttendanceDetailPo>
 				po.setKaoQinZhuangTa("异常");
 			}
 			po.setDaKaCiShu(1L);
-		} else {
+		} else if (isClockOut){
 			// 下班打卡
 
 			if (existingPo != null) {
@@ -414,6 +431,90 @@ public class AttendanceDetail extends AbstractDomain<String, AttendanceDetailPo>
 				po.setKaoQinZhuangTa("异常");
 				//po中已经设置了班次信息,直接返回
 			}
+		}else if(isMendCard){
+
+			// 1. 获取用户传入的补卡时间
+			String inputDaKa1Str = po.getDaKaShiJian1();
+			String inputDaKa2Str = po.getDaKaShiJian2();
+
+			// 2. 解析配置好的班次时间
+			LocalDateTime configStart = banCiKaiShi; //配置的班次开始时间
+			LocalDateTime configEnd = banCiJieShu;   // 配置的班次结束时间
+
+			LocalDateTime inputDaKa1Time;
+			try {
+				inputDaKa1Time = LocalDateTime.parse(inputDaKa1Str, DATETIME_FORMATTER);
+			} catch (DateTimeParseException e) {
+				throw new Exception("上班打卡时间格式错误,应为 yyyy-MM-dd HH:mm:ss");
+			}
+
+			// 3.1 校验:上班打卡时间不能晚于配置的上班时间
+			if (inputDaKa1Time.isAfter(configStart)) {
+				// 上班卡日期必须与考勤记录日期(recordDate)为同一日
+				LocalDate recordDate = LocalDate.parse(po.getRiQi(), DATE_FORMATTER);
+				if (!inputDaKa1Time.toLocalDate().isEqual(recordDate)){
+					throw new Exception("上班打卡日期必须为考勤记录当天(" + riQi + ")");
+				}
+				throw new Exception("上班卡不允许修改比配置的上班时间{" + configStart.format(DATETIME_FORMATTER) + "}晚");
+			} else {
+				// 准时逻辑:状态正常,迟到时长为0
+				po.setDaKaShiJian1(inputDaKa1Str);
+				po.setZhuangTai1("正常");
+				po.setChiDaoShiChang(0L);
+			}
+
+			// 4. 处理下班打卡时间 (inputDaKa2Str 格式应为 yyyy-MM-dd HH:mm)
+			if (BeanUtils.isEmpty(inputDaKa2Str)) {
+				throw new Exception("补卡下班时间不能为空");
+			}
+			LocalDateTime inputDaKa2Time;
+			try {
+				inputDaKa2Time = LocalDateTime.parse(inputDaKa2Str, DATETIME_FORMATTER);
+			} catch (DateTimeParseException e) {
+				throw new Exception("下班打卡时间格式错误,应为 yyyy-MM-dd HH:mm");
+			}
+
+			// 4.1 校验:下班打卡时间不能早于配置的下班时间
+			if (inputDaKa2Time.isBefore(configEnd)) {
+				throw new Exception("下班卡不允许修改比配置的下班时间{" + configEnd.format(DATETIME_FORMATTER) + "}早");
+			} else {
+				if (!inputDaKa2Time.toLocalDate().isEqual(endDate)){
+					throw new Exception("下班打卡日期必须为考勤记录配置日期(" + endDate.format(DATE_FORMATTER) + ")");
+				}
+				// 正常/加班逻辑:状态正常,计算加班分钟数
+				po.setDaKaShiJian2(inputDaKa2Str);
+				po.setZhuangTai2("正常");
+				long jiaBanFen = Duration.between(configEnd, inputDaKa2Time).toMinutes();
+				po.setJiaBanShiChang(jiaBanFen);
+			}
+
+			// 5. 计算实际工作时长 (分钟)
+			// 校验下班不能早于上班
+			if (inputDaKa2Time.isBefore(inputDaKa1Time)) {
+				throw new Exception("补卡失败:下班打卡时间不能早于上班打卡时间");
+			}
+
+			// 校验上下班间隔不能超过24小时,日期校验前面已经写了,资本家真该死啊
+			long hoursDiff = Duration.between(inputDaKa1Time, inputDaKa2Time).toHours();
+			if (hoursDiff >= 24) {
+				throw new Exception("补卡失败:上下班打卡时间间隔不能超过24小时");
+			}
+
+			long workMinutes = Duration.between(inputDaKa1Time, inputDaKa2Time).toMinutes();
+			po.setGongZuoShiChan(workMinutes);
+
+			// 6. 设置考勤总状态
+			if ("正常".equals(po.getZhuangTai1()) && "正常".equals(po.getZhuangTai2())) {
+				po.setKaoQinZhuangTa("正常");
+			} else {
+				po.setKaoQinZhuangTa("异常");
+			}
+
+			// 7. 设置打卡次数 (补卡视为一次打卡操作,代表这条记录被补全了)
+			po.setDaKaCiShu(1L);
+
+		}else{
+
 		}
 		return po;
 	}
@@ -615,39 +716,42 @@ public class AttendanceDetail extends AbstractDomain<String, AttendanceDetailPo>
 		return records;
 	}
 	/**
+	 * 业务场景:下班卡没打卡,考勤状态为空
 	 * 修正所有满足条件的考勤记录(基于每条记录的 shi_fou_kua_ri_ 和 ri_qi_)
 	 * 非跨日记录:修正 ri_qi_ = 昨天
 	 * 跨日记录:修正 ri_qi_ = 前天
 	 * 利用 huan_cun_biao_zhi 防重,仅处理未标记的记录
 	 */
 	public void correctRecentAttendanceOnClock() {
-		// 修正非跨日:ri_qi_ 在 [CURDATE()-3, CURDATE()-1]
-		String sqlNonCross = "UPDATE t_attendance_detail " +
+		// 1. 在 Java 中提前计算好日期范围字符串,避免数据库隐式类型转换导致索引失效
+		LocalDate today = LocalDate.now();
+		// 非跨日:需要修正 [今天-3天, 今天-1天] 的数据
+		String nonCrossStartDate = today.minusDays(3).format(DATE_FORMATTER);
+		String nonCrossEndDate = today.minusDays(1).format(DATE_FORMATTER);
+
+		// 跨日:需要修正 [今天-4天, 今天-2天] 的数据
+		String crossStartDate = today.minusDays(4).format(DATE_FORMATTER);
+		String crossEndDate = today.minusDays(2).format(DATE_FORMATTER);
+
+		// 2. 提取公共的 SQL 逻辑,使用 ? 占位符进行参数化查询
+		String baseSql = "UPDATE t_attendance_detail " +
 				"SET kao_qin_zhuang_ta = CASE " +
 				"    WHEN zhuang_tai_1_ = '正常' AND zhuang_tai_2_ = '正常' THEN '正常' " +
 				"    ELSE '异常' END, " +
 				"    huan_cun_biao_zhi = 'Y', " +
 				"    update_time_ = NOW() " +
-				"WHERE shi_fou_kua_ri_ = 'N' " +
-				"  AND ri_qi_ BETWEEN DATE_SUB(CURDATE(), INTERVAL 3 DAY) AND DATE_SUB(CURDATE(), INTERVAL 1 DAY) " +
-				"  AND (huan_cun_biao_zhi IS NULL OR huan_cun_biao_zhi != 'Y')";
-
-		// 修正跨日:ri_qi_ 在 [CURDATE()-4, CURDATE()-2]
-		String sqlCross = "UPDATE t_attendance_detail " +
-				"SET kao_qin_zhuang_ta = CASE " +
-				"    WHEN zhuang_tai_1_ = '正常' AND zhuang_tai_2_ = '正常' THEN '正常' " +
-				"    ELSE '异常' END, " +
-				"    huan_cun_biao_zhi = 'Y', " +
-				"    update_time_ = NOW() " +
-				"WHERE shi_fou_kua_ri_ = 'Y' " +
-				"  AND ri_qi_ BETWEEN DATE_SUB(CURDATE(), INTERVAL 4 DAY) AND DATE_SUB(CURDATE(), INTERVAL 2 DAY) " +
+				"WHERE shi_fou_kua_ri_ = ? " +
+				"  AND ri_qi_ >= ? " +
+				"  AND ri_qi_ <= ? " +
 				"  AND (huan_cun_biao_zhi IS NULL OR huan_cun_biao_zhi != 'Y')";
 
 		try {
-			int updated1 = jdbcTemplate.update(sqlNonCross);
-			int updated2 = jdbcTemplate.update(sqlCross);
+			// 3. 执行非跨日数据的修正
+			int updated1 = jdbcTemplate.update(baseSql, "N", nonCrossStartDate, nonCrossEndDate);
+			// 4. 执行跨日数据的修正
+			int updated2 = jdbcTemplate.update(baseSql, "Y", crossStartDate, crossEndDate);
 			if (updated1 + updated2 > 0) {
-				LOGGER.info("打卡触发修正最近3天考勤状态,非跨日更新={}, 跨日更新={}", updated1, updated2);
+				LOGGER.info("打卡触发修正最近考勤状态,非跨日更新={}, 跨日更新={}", updated1, updated2);
 			}
 		} catch (Exception e) {
 			LOGGER.error("打卡触发修正考勤状态失败", e);
@@ -943,4 +1047,24 @@ public class AttendanceDetail extends AbstractDomain<String, AttendanceDetailPo>
 
 		return records;
 	}
+
+	public void validateInfo(AttendanceDetailPo attendanceDetailPo) throws Exception {
+		if (attendanceDetailPo == null) {
+			throw new Exception("参数不能为空");
+		}
+		if (BeanUtils.isEmpty(attendanceDetailPo.getDaKaShiJian1())) {
+			throw new Exception("补卡上班时间不能为空");
+		}
+		if (BeanUtils.isEmpty(attendanceDetailPo.getDaKaShiJian2())) {
+			throw new Exception("补卡下班时间不能为空");
+		}
+		if (BeanUtils.isEmpty(attendanceDetailPo.getRiQi())) {
+			throw new Exception("补卡日期不能为空");
+		}
+		try {
+			 LocalDate.parse(attendanceDetailPo.getRiQi(), DATE_FORMATTER);
+		} catch (DateTimeParseException e) {
+			throw new Exception("日期格式错误,应为 yyyy-MM-dd");
+		}
+	}
 }