package com.ruoyi.web.integration.dahua; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.domain.SysDoorDevice; import com.ruoyi.system.domain.SysVisitorRecord; import com.ruoyi.system.mapper.SysDoorDeviceMapper; import com.ruoyi.system.utils.DoorDeviceCredentialCrypto; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; /** * 访客记录与大华门禁设备之间的授权编排服务。 */ @Slf4j @Service public class DahuaVisitorAccessService { private final DahuaAccessProperties properties; private final SysDoorDeviceMapper doorDeviceMapper; private final DahuaFaceImageLoader faceImageLoader; private final DahuaAccessControlClient accessControlClient; public DahuaVisitorAccessService(DahuaAccessProperties properties, SysDoorDeviceMapper doorDeviceMapper, DahuaFaceImageLoader faceImageLoader, DahuaAccessControlClient accessControlClient) { this.properties = properties; this.doorDeviceMapper = doorDeviceMapper; this.faceImageLoader = faceImageLoader; this.accessControlClient = accessControlClient; } /** * 按访客访问区域向对应启用的大华设备下发人员和人脸。 */ public OperationResult grant(SysVisitorRecord record) { if (!properties.isEnabled()) { return OperationResult.disabled(); } if (record == null) { throw new ServiceException("下发大华门禁权限时访客记录不能为空"); } List devices = findDevices(record.getVisitAreaIds()); // if (devices.isEmpty()) { // log.info("访客记录未关联启用的大华门禁设备,recordId={},areaIds={}", // record.getRecordId(), record.getVisitAreaIds()); // return OperationResult.noDevice(); // } validateGrantRecord(record); byte[] faceImage = faceImageLoader.load(record.getFaceImageUrl()); String userId = buildUserId(record); DahuaAccessControlClient.VisitorCredential credential = new DahuaAccessControlClient.VisitorCredential( userId, record.getVisitorName(), record.getIdCard(), record.getValidStartTime(), record.getValidEndTime(), faceImage); OperationResult result = execute(devices, device -> accessControlClient.grant(buildCredential(device), credential)); if (properties.isFailOnError() && !result.failures.isEmpty() && result.successCount > 0) { log.warn("大华门禁访客下发部分失败,开始撤销本次已下发权限,recordId={}", record.getRecordId()); OperationResult compensation = execute(result.successfulDevices, device -> accessControlClient.revoke(buildCredential(device), userId)); if (!compensation.failures.isEmpty()) { log.error("大华门禁访客下发补偿撤权未全部成功,recordId={},失败设备数={}", record.getRecordId(), compensation.failures.size()); } } handleFailure(result, "大华门禁访客人脸下发失败"); return result; } /** * 按原访问区域从对应大华设备删除人脸和人员权限。 */ public OperationResult revoke(SysVisitorRecord record) { if (!properties.isEnabled()) { return OperationResult.disabled(); } if (record == null) { throw new ServiceException("撤销大华门禁权限时访客记录不能为空"); } List devices = findDevices(record.getVisitAreaIds()); if (devices.isEmpty()) { log.info("访客记录未关联启用的大华门禁设备,无需撤权,recordId={},areaIds={}", record.getRecordId(), record.getVisitAreaIds()); return OperationResult.noDevice(); } String userId = buildUserId(record); OperationResult result = execute(devices, device -> accessControlClient.revoke(buildCredential(device), userId)); handleFailure(result, "大华门禁访客权限撤销失败"); return result; } private OperationResult execute(List devices, DeviceAction action) { OperationResult result = new OperationResult("success"); for (SysDoorDevice device : devices) { try { action.execute(device); result.successCount++; result.successfulDevices.add(device); log.info("大华门禁访客权限操作成功,doorId={},deviceName={},deviceId={}", device.getDoorId(), device.getDeviceName(), device.getDeviceId()); } catch (Exception e) { result.failures.add(new DeviceFailure(device.getDoorId(), device.getDeviceName(), device.getDeviceId(), e.getMessage())); log.error("大华门禁访客权限操作失败,doorId={},deviceName={},deviceId={}", device.getDoorId(), device.getDeviceName(), device.getDeviceId(), e); } } if (!result.failures.isEmpty()) { result.status = result.successCount == 0 ? "failed" : "partial"; } return result; } private List findDevices(String visitAreaIds) { List areaIds = parseAreaIds(visitAreaIds); if (areaIds.isEmpty()) { return Collections.emptyList(); } List devices = doorDeviceMapper.selectEnabledDahuaDevicesByAreaIds(areaIds); return devices == null ? Collections.emptyList() : devices; } private List parseAreaIds(String value) { if (StringUtils.isEmpty(StringUtils.trim(value))) { return Collections.emptyList(); } Set ids = new LinkedHashSet<>(); for (String item : value.split(",")) { String id = StringUtils.trim(item); if (StringUtils.isEmpty(id)) { continue; } try { long areaId = Long.parseLong(id); if (areaId <= 0) { throw new NumberFormatException(); } ids.add(areaId); } catch (NumberFormatException e) { throw new ServiceException("访客访问区域ID无效:" + id); } } return new ArrayList<>(ids); } private String buildUserId(SysVisitorRecord record) { String userId = StringUtils.trim(record.getRegisterNo()); if (StringUtils.isEmpty(userId)) { if (record.getRecordId() == null) { throw new ServiceException("访客记录缺少登记编号和记录ID,无法生成门禁用户ID"); } userId = "V" + record.getRecordId(); } if (userId.getBytes(StandardCharsets.UTF_8).length >= DahuaSdkLayout.USER_ID_LENGTH) { throw new ServiceException("访客门禁用户ID编码后长度必须小于32字节"); } return userId; } private DahuaDeviceCredential buildCredential(SysDoorDevice device) { Integer port = device.getDevicePort(); Integer doorNo = device.getDoorNo(); Integer timeSectionNo = device.getTimeSectionNo(); String charset = StringUtils.trim(device.getCharset()); return new DahuaDeviceCredential( device.getDeviceId(), port == null ? 37777 : port, StringUtils.isEmpty(StringUtils.trim(device.getLoginUsername())) ? "admin" : StringUtils.trim(device.getLoginUsername()), DoorDeviceCredentialCrypto.decrypt(device.getLoginPasswordCipher()), doorNo == null ? 0 : doorNo, timeSectionNo == null ? 255 : timeSectionNo, StringUtils.isEmpty(charset) ? "GBK" : charset); } private void validateGrantRecord(SysVisitorRecord record) { if (StringUtils.isEmpty(StringUtils.trim(record.getVisitorName()))) { throw new ServiceException("访客姓名不能为空"); } if (StringUtils.isEmpty(StringUtils.trim(record.getFaceImageUrl()))) { throw new ServiceException("访客人脸图片不能为空"); } if (record.getValidStartTime() == null || record.getValidEndTime() == null) { throw new ServiceException("访客有效期不能为空"); } if (!record.getValidStartTime().before(record.getValidEndTime())) { throw new ServiceException("访客有效期结束时间必须晚于开始时间"); } } private void handleFailure(OperationResult result, String message) { if (properties.isFailOnError() && !result.failures.isEmpty()) { throw new ServiceException(message + ",失败设备数:" + result.failures.size()); } } @FunctionalInterface private interface DeviceAction { void execute(SysDoorDevice device); } /** * 多设备门禁操作汇总。 */ public static final class OperationResult { private String status; private int successCount; private final List failures = new ArrayList<>(); private final List successfulDevices = new ArrayList<>(); private OperationResult(String status) { this.status = status; } private static OperationResult disabled() { return new OperationResult("disabled"); } private static OperationResult noDevice() { return new OperationResult("no_device"); } public String getStatus() { return status; } public int getSuccessCount() { return successCount; } public List getFailures() { return Collections.unmodifiableList(failures); } } /** * 单台设备失败信息,不包含账号、密码等敏感配置。 */ public static final class DeviceFailure { private final Long doorId; private final String deviceName; private final String deviceId; private final String message; private DeviceFailure(Long doorId, String deviceName, String deviceId, String message) { this.doorId = doorId; this.deviceName = deviceName; this.deviceId = deviceId; this.message = message; } public Long getDoorId() { return doorId; } public String getDeviceName() { return deviceName; } public String getDeviceId() { return deviceId; } public String getMessage() { return message; } } }