package com.ruoyi.web.controller.service; import cn.hutool.core.date.DateUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.bean.BeanUtils; import com.ruoyi.system.domain.SysVisitorRecord; import com.ruoyi.system.domain.SysVisitorSignoutTemp; import com.ruoyi.system.dto.ApiVisitorRecodeSingoutRequest; import com.ruoyi.system.mapper.SysVisitorRecordMapper; import com.ruoyi.system.mapper.SysVisitorSignoutTempMapper; import com.ruoyi.web.controller.tool.FaceBase64ToUrlUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.util.*; import java.util.concurrent.TimeUnit; /** * 访客签离离线数据同步服务 * * @author ruoyi */ @Slf4j @Service public class ApiPullSignOutDataService { @Resource private SysVisitorRecordMapper visitorRecordMapper; @Resource private SysVisitorSignoutTempMapper signoutTempMapper; @Resource private RedisTemplate redisTemplate; @Resource private SysVisitorRecordService visitorRecordService; /** * 处理签离数据(支持签离先到的场景) */ @Transactional(rollbackFor = Exception.class) public AjaxResult processSignOutData(List requestList) { if (requestList == null || requestList.isEmpty()) { throw new ServiceException("上传失败,未获取到签离数据"); } int successCount = 0; int skipCount = 0; int errorCount = 0; for (ApiVisitorRecodeSingoutRequest request : requestList) { try { if (request == null) { log.warn("签离数据为空,已跳过"); errorCount++; continue; } // 1. 参数校验 if (!StringUtils.hasText(request.getIdCard())) { log.warn("身份证号为空,已跳过,人员姓名: {}", request.getIntervieweeName()); errorCount++; continue; } if (com.ruoyi.common.utils.StringUtils.isNotEmpty(request.getSignImageUrl())) { String url = FaceBase64ToUrlUtil.processFaceImage(request.getSignImageUrl(), request.getIdCard()); request.setSignImageUrl(url); } // 2. 防重校验(身份证号+签离时间) String uniqueKey = generateUniqueKey(request); String redisKey = "offline:signout:" + uniqueKey; if (Boolean.TRUE.equals(redisTemplate.hasKey(redisKey))) { log.warn("签离数据重复,已跳过, 身份证号: {}, 签离时间: {}", request.getIdCard(), request.getSignTime()); skipCount++; continue; } // 3. 处理单条签离数据 boolean processed = processSingleSignOutData(request); if (processed) { // 记录处理成功状态到Redis(7天过期) redisTemplate.opsForValue().set(redisKey, "1", 7, TimeUnit.DAYS); successCount++; } else { errorCount++; } } catch (Exception e) { log.error("处理签离数据失败, 身份证号: {}", request.getIdCard(), e); errorCount++; } } log.info("离线签离数据同步完成,成功:{},跳过:{},失败:{}", successCount, skipCount, errorCount); if (errorCount > 0) { return AjaxResult.error(String.format("同步完成,成功:%d,跳过:%d,失败:%d", successCount, skipCount, errorCount)); } return AjaxResult.success(String.format("同步成功,成功:%d,跳过:%d", successCount, skipCount)); } /** * 处理单条签离数据 * * @param request 签离请求 * @return 是否处理成功 */ private boolean processSingleSignOutData(ApiVisitorRecodeSingoutRequest request) { // 设置签离时间(如果没有则用当前时间) Date signOutTime = request.getSignTime() != null ? request.getSignTime() : new Date(); // 1. 先保存签离数据到临时表 SysVisitorSignoutTemp signoutTemp = new SysVisitorSignoutTemp(); BeanUtils.copyProperties(request, signoutTemp); signoutTemp.setSignTime(signOutTime); signoutTemp.setRelateStatus("0"); signoutTemp.setCreateTime(new Date()); signoutTempMapper.insert(signoutTemp); log.info("签离数据已保存到临时表,tempId: {}, 身份证号: {}", signoutTemp.getTempId(), request.getIdCard()); // 2. 尝试匹配签到记录(查找未签离的记录) SysVisitorRecord matchedRecord = findMatchSignInRecord(request.getIdCard(), signOutTime); if (matchedRecord != null) { // 找到匹配的签到记录,更新签离信息 return associateWithSignInRecord(matchedRecord, signoutTemp); } else { // 未找到匹配的签到记录,暂存临时表等待后续关联 log.info("未找到匹配的签到记录,签离数据暂存临时表,tempId: {}, 身份证号: {}", signoutTemp.getTempId(), request.getIdCard()); return true; } } /** * 查找匹配的签到记录 * * @param idCard 身份证号 * @param signOutTime 签离时间 * @return 匹配的签到记录 */ private SysVisitorRecord findMatchSignInRecord(String idCard, Date signOutTime) { // 获取签离时间当天的开始和结束时间 Date signOutDayStart = DateUtil.beginOfDay(signOutTime); Date signOutDayEnd = DateUtil.endOfDay(signOutTime); // 策略1: 查找当天进行的记录(状态为VISITING或PENDING_CONFIRM) // todo 业务上有个字段决定来访日期是 当天 还是 多天 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysVisitorRecord::getIdCard, idCard) .in(SysVisitorRecord::getStatus, Arrays.asList("VISITING", "PENDING_CONFIRM")) .eq(SysVisitorRecord::getDelFlag, "0") .orderByDesc(SysVisitorRecord::getRegisterTime) .last("limit 1"); SysVisitorRecord record = visitorRecordMapper.selectOne(wrapper); if (record != null) { // 检查签到日期是否在签离日期前后3天内 Date registerDayStart = DateUtil.beginOfDay(record.getRegisterTime()); Date threeDaysBefore = DateUtil.offsetDay(signOutTime, -3); Date threeDaysAfter = DateUtil.offsetDay(signOutTime, 3); if (record.getRegisterTime().after(threeDaysBefore) && record.getRegisterTime().before(threeDaysAfter)) { log.info("找到匹配的签到记录,recordId: {}, registerTime: {}, signOutTime: {}", record.getRecordId(), record.getRegisterTime(), signOutTime); return record; } } // 策略2: 查找3天内已签离但可能状态异常的记录(防止数据不一致) Date threeDaysAgo = DateUtil.offsetDay(signOutTime, -3); wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysVisitorRecord::getIdCard, idCard) .in(SysVisitorRecord::getStatus, Arrays.asList("SIGNED_OUT", "OVERDUE_AUTO_SIGNED_OUT")) .eq(SysVisitorRecord::getDelFlag, "0") .ge(SysVisitorRecord::getSignTime, threeDaysAgo) .le(SysVisitorRecord::getSignTime, signOutDayEnd) .orderByDesc(SysVisitorRecord::getSignTime) .last("limit 1"); record = visitorRecordMapper.selectOne(wrapper); if (record != null) { log.warn("找到已签离的记录,可能需要重新关联,recordId: {}, status: {}", record.getRecordId(), record.getStatus()); // 检查签离时间是否与当前签离时间接近(30分钟内) if (isTimeClose(record.getSignTime(), signOutTime, 30)) { log.info("签离时间接近,可能为重复数据,recordId: {}", record.getRecordId()); return record; } } log.info("未找到匹配的签到记录,身份证号: {}, 签离时间: {}", idCard, signOutTime); return null; } /** * 关联签离记录到签到记录 * * @param record 签到记录 * @param signoutTemp 签离临时记录 * @return 是否关联成功 */ private boolean associateWithSignInRecord(SysVisitorRecord record, SysVisitorSignoutTemp signoutTemp) { // 1. 检查记录是否已有签离时间(防止重复关联) if (record.getSignTime() != null) { log.warn("签到记录已有签离时间,跳过关联,recordId: {}, 已有签离时间: {}", record.getRecordId(), record.getSignTime()); // 如果签离时间不同,记录日志 if (!isTimeClose(record.getSignTime(), signoutTemp.getSignTime(), 5)) { log.warn("签离时间不一致,已有: {}, 新: {}", record.getSignTime(), signoutTemp.getSignTime()); } // 更新临时表状态但不更新主表 signoutTemp.setRelateStatus("1"); signoutTemp.setRelateRecordId(record.getRecordId()); signoutTemp.setUpdateTime(new Date()); signoutTempMapper.updateById(signoutTemp); return true; } // 2. 计算签离状态 String status = calculateSignOutStatus(record, signoutTemp.getSignTime()); // 3. 更新签到记录 LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(SysVisitorRecord::getRecordId, record.getRecordId()) .set(SysVisitorRecord::getSignTime, signoutTemp.getSignTime()) .set(SysVisitorRecord::getSignReceiverName, signoutTemp.getSignReceiverName()) .set(SysVisitorRecord::getSignVerifyMethod, signoutTemp.getSignVerifyMethod()) .set(SysVisitorRecord::getSignImageUrl, signoutTemp.getSignImageUrl()) .set(SysVisitorRecord::getStatus, status) .set(SysVisitorRecord::getUpdateTime, new Date()); // 添加备注信息 String remark = buildSignOutRemark(record, signoutTemp, status); if (StringUtils.hasText(remark)) { updateWrapper.set(SysVisitorRecord::getRemark, (record.getRemark() != null ? record.getRemark() + ";" : "") + remark); } visitorRecordService.revokeAccess(record); int updateCount = visitorRecordMapper.update(null, updateWrapper); if (updateCount <= 0) { log.error("关联签离记录失败,更新影响行数为0,recordId: {}", record.getRecordId()); return false; } // 4. 更新签离临时表关联状态 signoutTemp.setRelateStatus("1"); signoutTemp.setRelateRecordId(record.getRecordId()); signoutTemp.setUpdateTime(new Date()); signoutTempMapper.updateById(signoutTemp); log.info("签离数据关联成功,recordId: {}, tempId: {}, 状态: {}, 身份证号: {}", record.getRecordId(), signoutTemp.getTempId(), status, record.getIdCard()); return true; } /** * 计算签离状态 * * @param record 签到记录 * @param signOutTime 签离时间 * @return 签离状态 */ private String calculateSignOutStatus(SysVisitorRecord record, Date signOutTime) { Date validEndTime = record.getValidEndTime(); // 如果没有有效期结束时间,默认正常签离 if (validEndTime == null) { log.warn("记录没有设置有效期结束时间,默认正常签离,recordId: {}", record.getRecordId()); return "SIGNED_OUT"; } // 获取有效期的最后一天的结束时间(23:59:59) Date validEndDayEnd = DateUtil.endOfDay(validEndTime); // 获取有效期结束日期的第二天开始时间(00:00:00) Date nextDayStart = DateUtil.beginOfDay(DateUtil.offsetDay(validEndTime, 1)); // 情况1: 签离时间在有效期内(<= 有效期结束日期的23:59:59) if (signOutTime.before(validEndDayEnd) || signOutTime.equals(validEndDayEnd)) { return "SIGNED_OUT"; } // 情况2: 签离时间在有效期结束后的第二天及以后(>= 第二天00:00:00) if (signOutTime.after(nextDayStart) || signOutTime.equals(nextDayStart)) { return "OVERDUE_AUTO_SIGNED_OUT"; } // 情况3: 签离时间在有效期结束后但还在当天 if (signOutTime.after(validEndTime) && signOutTime.before(nextDayStart)) { return "PENDING_CONFIRM"; } // 默认情况 return "SIGNED_OUT"; } /** * 构建签离备注信息 */ private String buildSignOutRemark(SysVisitorRecord record, SysVisitorSignoutTemp signoutTemp, String status) { StringBuilder remark = new StringBuilder(); remark.append("签离设备: ").append(signoutTemp.getDeviceNo()); remark.append("; 签离时间: ").append(DateUtil.format(signoutTemp.getSignTime(), "yyyy-MM-dd HH:mm:ss")); switch (status) { case "SIGNED_OUT": remark.append("; 正常签离"); break; case "PENDING_CONFIRM": remark.append("; 逾期滞留(超过有效期,有效期至: ") .append(DateUtil.format(record.getValidEndTime(), "yyyy-MM-dd HH:mm:ss")) .append(")"); break; case "OVERDUE_AUTO_SIGNED_OUT": remark.append("; 超期自动签离(有效期至: ") .append(DateUtil.format(record.getValidEndTime(), "yyyy-MM-dd HH:mm:ss")) .append(")"); break; default: break; } return remark.toString(); } /** * 判断两个时间是否接近(分钟) */ private boolean isTimeClose(Date time1, Date time2, int minutes) { if (time1 == null || time2 == null) { return false; } long diff = Math.abs(time1.getTime() - time2.getTime()); long diffMinutes = diff / (60 * 1000); return diffMinutes <= minutes; } /** * 生成唯一键(用于防重) */ private String generateUniqueKey(ApiVisitorRecodeSingoutRequest request) { StringBuilder sb = new StringBuilder(); // sb.append(request.getDeviceNo()).append(":"); sb.append(request.getIdCard()).append(":"); sb.append(request.getSignTime() != null ? request.getSignTime().getTime() : System.currentTimeMillis()); return sb.toString(); } }