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