package com.ruoyi.web.controller.service; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.exception.ServiceException; import com.ruoyi.common.utils.bean.BeanUtils; import com.ruoyi.common.utils.file.FileUploadUtils; import com.ruoyi.system.domain.SysVisitorRecord; import com.ruoyi.system.domain.SysVisitorSignoutTemp; import com.ruoyi.system.dto.ApiVisitorRecordSinginRequest; 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 org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.*; import java.util.*; import java.util.concurrent.TimeUnit; @Service @Slf4j public class ApiPullSingInDataService { @Resource private SysVisitorRecordMapper visitorRecordMapper; @Resource private SysVisitorSignoutTempMapper signoutTempMapper; @Resource private RedisTemplate redisTemplate; /** * 处理签到数据(支持签离先到的场景) */ @Transactional(rollbackFor = Exception.class) public AjaxResult processSignInData(List requestList) { if (requestList == null || requestList.isEmpty()) { throw new ServiceException("上传失败,未获取到签到数据"); } int successCount = 0; int skipCount = 0; int errorCount = 0; for (ApiVisitorRecordSinginRequest request : requestList) { try { if (request == null) { log.warn("签到数据为空,已跳过"); errorCount++; continue; } // 1. 参数校验 if (!StringUtils.hasText(request.getIdCard())) { log.warn("身份证号为空,已跳过,访问人姓名: {}", request.getVisitorName()); errorCount++; continue; } if (com.ruoyi.common.utils.StringUtils.isNotEmpty(request.getFaceImageUrl())) { String url = FaceBase64ToUrlUtil.processFaceImage(request.getFaceImageUrl(), request.getIdCard()); request.setFaceImageUrl(url); } // 2. 防重校验(基于设备编号+身份证号+登记时间) String uniqueKey = generateUniqueKey(request); String redisKey = "offline:signin:" + uniqueKey; if (Boolean.TRUE.equals(redisTemplate.hasKey(redisKey))) { log.warn("签到数据重复,已跳过, 身份证号: {}, 登记时间: {}", request.getIdCard(), request.getRegisterTime()); skipCount++; continue; } // 3. 处理单条签到数据 boolean processed = processSingleSignInData(request); if (processed) { // 记录处理成功状态到Redis(7天过期) redisTemplate.opsForValue().set(redisKey, "1", 7, TimeUnit.DAYS); successCount++; } else { errorCount++; } } catch (Exception e) { log.error("处理签到数据失败, 身份证号: {}, 设备编号: {}", request.getIdCard(), request.getDeviceNo(), 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 processSingleSignInData(ApiVisitorRecordSinginRequest request) { // 设置登记时间(如果没有则用当前时间) Date registerTime = request.getRegisterTime() != null ? request.getRegisterTime() : new Date(); // 1. 查询当天是否有未签离的记录 SysVisitorRecord existingRecord = findActiveRecordToday( request.getIdCard(), registerTime); if (existingRecord != null) { log.warn("该访客当天已有进行中的记录,已跳过, recordId: {}, 身份证号: {}", existingRecord.getRecordId(), request.getIdCard()); return false; } // 2. 查询是否有未关联的签离数据(签离先到的情况) SysVisitorSignoutTemp signoutTemp = findUnlinkedSignout( request.getIdCard(), registerTime); // 3. 创建签到记录 SysVisitorRecord record = new SysVisitorRecord(); BeanUtils.copyProperties(request, record); record.setRegisterTime(registerTime); record.setRegisterSource("访客机"); // record.setRegisterType("ONSITE"); record.setCreateTime(new Date()); record.setDelFlag("0"); record.setRegisterNo(generateRegisterNo()); // 记录设备信息 if (StringUtils.hasText(request.getDeviceNo())) { record.setRemark("签到设备ID: " + request.getDeviceNo()); } // 4. 如果有未关联的签离数据,直接关联 if (signoutTemp != null) { // 设置签离信息 record.setSignTime(signoutTemp.getSignTime()); record.setSignReceiverName(signoutTemp.getSignReceiverName()); record.setSignVerifyMethod(signoutTemp.getSignVerifyMethod()); record.setSignImageUrl(signoutTemp.getSignImageUrl()); // 改签离的状态 //签离时间在有效期内 → SIGNED_OUT(正常签离) // //签离时间超过有效期结束时间 → PENDING_CONFIRM(逾期滞留) //签离时间在有效期结束后的第二天及以后 → OVERDUE_AUTO_SIGNED_OUT(超期自动签离) // 5. 根据签离时间和有效期计算状态 String status = calculateSignOutStatus(record); record.setStatus(status); // 记录签离状态说明 String statusRemark = getStatusRemark(status, record); if (StringUtils.hasText(statusRemark)) { record.setRemark((record.getRemark() != null ? record.getRemark() + ";" : "") + statusRemark); } // 插入签到记录 visitorRecordMapper.insert(record); // 更新签离临时表关联状态 signoutTemp.setRelateStatus("1"); signoutTemp.setRelateRecordId(record.getRecordId()); signoutTemp.setUpdateTime(new Date()); signoutTempMapper.updateById(signoutTemp); log.info("签到数据已关联签离数据,recordId: {}, tempId: {}, 身份证号: {}", record.getRecordId(), signoutTemp.getTempId(), request.getIdCard()); } else { // 没有签离数据,正常创建签到记录 record.setStatus("VISITING"); visitorRecordMapper.insert(record); log.info("签到数据插入成功,recordId: {}, 身份证号: {}", record.getRecordId(), request.getIdCard()); } return true; } /** * 计算签离状态 * * @param record 访客记录 * @return 签离状态 */ private String calculateSignOutStatus(SysVisitorRecord record) { Date signTime = record.getSignTime(); Date validStartTime = record.getValidStartTime(); Date validEndTime = record.getValidEndTime(); // 如果没有签离时间,返回来访中 if (signTime == null) { return "VISITING"; } // 如果没有有效期结束时间,默认正常签离 if (validEndTime == null) { log.warn("记录没有设置有效期结束时间,默认取消访问,recordId: {}", record.getRecordId()); return "CANCELLED"; } // 获取有效期的最后一天的结束时间(23:59:59) Date validEndDayEnd = DateUtil.endOfDay(validEndTime); // 获取有效期结束日期的第二天开始时间(00:00:00) Date nextDayStart = DateUtil.beginOfDay(DateUtil.offsetDay(validEndTime, 1)); // 情况1: 签离时间在有效期内(<= 有效期结束日期的23:59:59) if (signTime.before(validEndDayEnd) || signTime.equals(validEndDayEnd)) { log.debug("签离时间在有效期内,状态: SIGNED_OUT, signTime: {}, validEnd: {}", signTime, validEndDayEnd); return "SIGNED_OUT"; } // 情况2: 签离时间在有效期结束后的第二天及以后(>= 第二天00:00:00) if (signTime.after(nextDayStart) || signTime.equals(nextDayStart)) { log.debug("签离时间在有效期结束第二天及以后,状态: OVERDUE_AUTO_SIGNED_OUT, signTime: {}, nextDayStart: {}", signTime, nextDayStart); return "OVERDUE_AUTO_SIGNED_OUT"; } // 情况3: 签离时间在有效期结束后但还在当天(即有效期结束当天的00:00:00 到 23:59:59之后) // 实际上就是超过了有效期结束时间,但还在有效期结束的当天(已经过了23:59:59) // 这种情况属于逾期滞留 if (signTime.after(validEndTime) && signTime.before(nextDayStart)) { log.debug("签离时间在有效期结束后当天,状态: PENDING_CONFIRM, signTime: {}, validEnd: {}", signTime, validEndTime); return "PENDING_CONFIRM"; } // 默认情况:如果不在上述任何情况,按正常签离处理 log.warn("签离时间不在预期范围内,默认取消,signTime: {}, validEnd: {}", signTime, validEndTime); return "CANCELLED"; } /** * 获取状态说明 */ private String getStatusRemark(String status, SysVisitorRecord record) { switch (status) { case "SIGNED_OUT": return "正常签离(签离时间:" + DateUtil.format(record.getSignTime(), "yyyy-MM-dd HH:mm:ss") + ")"; case "PENDING_CONFIRM": return "逾期滞留(签离时间超过有效期,有效期至:" + DateUtil.format(record.getValidEndTime(), "yyyy-MM-dd HH:mm:ss") + ",签离时间:" + DateUtil.format(record.getSignTime(), "yyyy-MM-dd HH:mm:ss") + ")"; case "OVERDUE_AUTO_SIGNED_OUT": return "超期自动签离(签离时间在有效期结束后,有效期至:" + DateUtil.format(record.getValidEndTime(), "yyyy-MM-dd HH:mm:ss") + ",签离时间:" + DateUtil.format(record.getSignTime(), "yyyy-MM-dd HH:mm:ss") + ")"; default: return ""; } } /** * 查询当天进行中的记录 * * @param idCard 身份证号 * @param registerTime 登记时间 * @return 进行中的记录 */ private SysVisitorRecord findActiveRecordToday(String idCard, Date registerTime) { Date startOfDay = DateUtil.beginOfDay(registerTime); Date endOfDay = DateUtil.offsetDay(startOfDay, 1); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysVisitorRecord::getIdCard, idCard) .ge(SysVisitorRecord::getRegisterTime, startOfDay) .lt(SysVisitorRecord::getRegisterTime, endOfDay) .in(SysVisitorRecord::getStatus, Arrays.asList("VISITING", "PENDING_CONFIRM")) .eq(SysVisitorRecord::getDelFlag, "0") .orderByDesc(SysVisitorRecord::getRegisterTime) .last("limit 1"); return visitorRecordMapper.selectOne(wrapper); } /** * 查找未关联的签离数据 * * @param idCard 身份证号 * @param signInTime 签到时间 * @return 未关联的签离数据 */ private SysVisitorSignoutTemp findUnlinkedSignout(String idCard, Date signInTime) { // 查找签到时间未来3天未关联数据 Date threeDaysAgo = DateUtil.offsetDay(signInTime, 3); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysVisitorSignoutTemp::getIdCard, idCard) .eq(SysVisitorSignoutTemp::getRelateStatus, "0") .ge(SysVisitorSignoutTemp::getSignTime, threeDaysAgo) .orderByAsc(SysVisitorSignoutTemp::getSignTime) .last("limit 1"); return signoutTempMapper.selectOne(wrapper); } /** * 生成唯一键(用于防重) * * @param request 签到请求 * @return 唯一键 */ private String generateUniqueKey(ApiVisitorRecordSinginRequest request) { StringBuilder sb = new StringBuilder(); // sb.append(request.getDeviceNo()).append(":"); sb.append(request.getIdCard()).append(":"); sb.append(request.getRegisterTime() != null ? request.getRegisterTime().getTime() : System.currentTimeMillis()); return sb.toString(); } /** * 生成登记编号 * * @return 登记编号 */ private String generateRegisterNo() { // 格式:DJ + 年月日 + 时间戳后6位 + 随机4位 String dateStr = DateUtil.format(new Date(), "yyyyMMdd"); String timeStr = String.valueOf(System.currentTimeMillis()); String randomStr = UUID.randomUUID().toString().substring(0, 4).toUpperCase(); return "DJ" + dateStr + timeStr.substring(timeStr.length() - 6) + randomStr; } }