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<String, Object> redisTemplate;
|
|
/**
|
* 处理签离数据(支持签离先到的场景)
|
*/
|
@Transactional(rollbackFor = Exception.class)
|
public AjaxResult processSignOutData(List<ApiVisitorRecodeSingoutRequest> 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<SysVisitorRecord> 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<SysVisitorRecord> 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);
|
}
|
|
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();
|
}
|
}
|