package com.ruoyi.web.controller.service; import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.system.domain.SysVisitorRecord; import com.ruoyi.system.mapper.SysVisitorRecordMapper; import com.ruoyi.web.integration.dahua.DahuaVisitorAccessService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; @Service @Slf4j @Transactional(rollbackFor = Exception.class) public class SysVisitorRecordService extends ServiceImpl { @Autowired private RedisTemplate redisTemplate; @Autowired private DahuaVisitorAccessService dahuaVisitorAccessService; @Autowired private VisitorRegisterSyncService visitorRegisterSyncService; private static final String REDIS_KEY_PREFIX = "visitor:expire:"; /** * 登记访客时设置 Redis 过期 Key */ public void registerVisitor(SysVisitorRecord record) { if (record.getValidEndTime() == null) { throw new IllegalArgumentException("访客有效期结束时间不能为空"); } if (!record.getValidEndTime().after(new Date())) { log.warn("访客记录 {} 登记时已超期,不下发门禁权限", record.getRecordId()); autoSignOut(record); return; } // 下发门禁权限后设置过期监听,确保登记与到期撤权使用同一条业务链路。 DahuaVisitorAccessService.OperationResult accessResult = dahuaVisitorAccessService.grant(record); registerGrantRollbackCompensation(record, accessResult); setRedisExpireKey(record); log.info("访客 {} 登记成功,有效期至:{}", record.getRecordId(), record.getValidEndTime()); } /** * 设置 Redis 过期 Key */ private void setRedisExpireKey(SysVisitorRecord record) { Date now = new Date(); Date validEndTime = record.getValidEndTime(); if (validEndTime == null) { throw new IllegalArgumentException("访客有效期结束时间不能为空"); } // 如果有效期结束时间小于当前时间,直接签离 if (validEndTime.before(now)) { log.warn("访客记录 {} 已超期,直接签离", record.getRecordId()); autoSignOut(record); return; } // 计算过期时间(毫秒) long expireTime = validEndTime.getTime() - now.getTime(); String key = REDIS_KEY_PREFIX + record.getRecordId(); // 设置 Redis Key,过期时间为 validEndTime redisTemplate.opsForValue().set( key, record.getRecordId().toString(), expireTime, TimeUnit.MILLISECONDS ); log.info("设置Redis过期Key成功,Key:{},过期时间:{} 毫秒", key, expireTime); } /** * 根据记录ID自动签离(由Redis过期事件触发) */ @Transactional public void autoSignOutByRecordId(Long recordId) { try { // 1. 查询记录 SysVisitorRecord record = this.getById(recordId); if (record == null) { log.warn("访客记录不存在,ID:{}", recordId); return; } // 2. 检查状态(只有 VISITING 状态才处理) if (!"VISITING".equals(record.getStatus())) { log.info("访客记录 {} 状态为 {},无需处理", recordId, record.getStatus()); return; } // 3. 检查是否真的超期(双重检查) Date now = new Date(); if (!record.getValidEndTime().before(now)) { log.info("访客记录 {} 有效期未结束,无需签离", recordId); return; } // 4. 执行签离 autoSignOut(record); // 5. 删除 Redis Key(防止重复触发) redisTemplate.delete(REDIS_KEY_PREFIX + recordId); log.info("Redis过期触发签离成功,记录ID:{},签离时间:{}", recordId, now); } catch (Exception e) { log.error("自动签离失败,记录ID:{}", recordId, e); throw e; } } /** * 执行自动签离 */ private void autoSignOut(SysVisitorRecord record) { Date now = new Date(); revokeAccess(record); record.setStatus("PENDING_CONFIRM"); record.setSignTime(now); record.setRemark("逾期滞留,等待确认"); record.setUpdateTime(now); this.updateById(record); syncMiddlePlatformStatus(record); } /** * 撤销访客在大华门禁设备上的人脸及人员权限。 */ public void revokeAccess(SysVisitorRecord record) { dahuaVisitorAccessService.revoke(record); } /** * 同步访客状态到事件中心;失败不回滚本地签离结果。 */ public void syncMiddlePlatformStatus(SysVisitorRecord record) { visitorRegisterSyncService.syncVisitorStatus(record); } private void registerGrantRollbackCompensation(SysVisitorRecord record, DahuaVisitorAccessService.OperationResult accessResult) { if (accessResult.getSuccessCount() <= 0 || !TransactionSynchronizationManager.isSynchronizationActive()) { return; } TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCompletion(int status) { if (status != TransactionSynchronization.STATUS_ROLLED_BACK) { return; } redisTemplate.delete(REDIS_KEY_PREFIX + record.getRecordId()); try { dahuaVisitorAccessService.revoke(record); log.info("访客登记事务回滚,大华门禁权限补偿撤销完成,recordId={}", record.getRecordId()); } catch (Exception e) { log.error("访客登记事务回滚,大华门禁权限补偿撤销失败,recordId={}", record.getRecordId(), e); } } }); } /** * 更新访客记录(并重置 Redis 过期时间) */ public boolean updateVisitor(SysVisitorRecord record) { // 1. 更新记录 boolean updated = this.updateById(record); if (!updated) { return false; } // 2. 如果状态变更为 VISITING,重新设置过期时间 if ("VISITING".equals(record.getStatus())) { setRedisExpireKey(record); } // 3. 如果已签离,删除 Redis Key if ("SIGNED_OUT".equals(record.getStatus()) || "OVERDUE_AUTO_SIGNED_OUT".equals(record.getStatus())) { redisTemplate.delete(REDIS_KEY_PREFIX + record.getRecordId()); } syncMiddlePlatformStatus(record); return true; } /** * 兜底方案:启动时检查所有超期记录 */ @Transactional public int checkAndAutoSignOutOnStartup() { // 查询所有超期记录 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysVisitorRecord::getStatus, "VISITING") .lt(SysVisitorRecord::getValidEndTime, new Date()); List overdueRecords = this.list(queryWrapper); if (CollUtil.isEmpty(overdueRecords)) { log.info("启动检查:没有超期访客记录"); return 0; } log.info("启动检查:发现 {} 条超期记录,开始处理", overdueRecords.size()); Date now = new Date(); int count = 0; for (SysVisitorRecord record : overdueRecords) { try { revokeAccess(record); record.setStatus("OVERDUE_AUTO_SIGNED_OUT"); record.setSignTime(now); record.setRemark("系统自动签离(启动时检查)"); record.setUpdateTime(now); this.updateById(record); syncMiddlePlatformStatus(record); // 删除 Redis Key redisTemplate.delete(REDIS_KEY_PREFIX + record.getRecordId()); count++; } catch (Exception e) { log.error("启动检查处理记录失败,ID:{}", record.getRecordId(), e); } } log.info("启动检查完成,处理 {} 条超期记录", count); return count; } }