From 2a3c3cdbf2398695d61633e072f98ee2757e9e27 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期二, 11 八月 2026 11:20:26 +0800
Subject: [PATCH] feat(appointment): 添加访客预约系统解除绑定功能

---
 backend/src/main/java/com/sinata/appointment/service/IntegrationHikvisionService.java |  102 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 95 insertions(+), 7 deletions(-)

diff --git a/backend/src/main/java/com/sinata/appointment/service/IntegrationHikvisionService.java b/backend/src/main/java/com/sinata/appointment/service/IntegrationHikvisionService.java
index 39f7c84..379fed5 100644
--- a/backend/src/main/java/com/sinata/appointment/service/IntegrationHikvisionService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/IntegrationHikvisionService.java
@@ -3,16 +3,26 @@
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.sinata.appointment.constants.AppointmentBizConstants;
+import com.sinata.appointment.constants.BizErrorCode;
+import com.sinata.appointment.config.HikvisionQrcodeProperties;
 import com.sinata.appointment.entity.AppointmentOrder;
 import com.sinata.appointment.entity.AppointmentVisitor;
 import com.sinata.appointment.entity.MpUser;
 import com.sinata.appointment.entity.MpVisitor;
+import com.sinata.appointment.exception.BizException;
+import com.sinata.appointment.util.MobileMaskUtil;
+import com.sinata.appointment.util.SensitiveDataUtil;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
+import java.util.Base64;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -35,8 +45,35 @@
             DateTimeFormatter.ofPattern("yyMMddHHmmss.SSS");
 
     private final ObjectMapper objectMapper;
+    private final HikvisionQrcodeProperties properties;
 
     public String requestQrcodePayload(
+            AppointmentOrder order,
+            AppointmentVisitor visitor,
+            MpVisitor visitorProfile,
+            MpUser visitedUser,
+            String visitPurpose,
+            String areas,
+            LocalDateTime qrGenerateTime,
+            LocalDateTime qrExpireTime,
+            int validMinutes,
+            String codeId,
+            String visitorIdCardPlain) {
+        return buildPlainQrcodePayload(
+                order,
+                visitor,
+                visitorProfile,
+                visitedUser,
+                visitPurpose,
+                areas,
+                qrGenerateTime,
+                qrExpireTime,
+                validMinutes,
+                codeId,
+                visitorIdCardPlain);
+    }
+
+    public String buildPlainQrcodePayload(
             AppointmentOrder order,
             AppointmentVisitor visitor,
             MpVisitor visitorProfile,
@@ -55,10 +92,7 @@
         payload.put("visitorIdCard", trimToEmpty(visitorIdCardPlain));
         payload.put("visitorIdCardType", VISITOR_ID_CARD_TYPE);
         payload.put("visitorPhone", firstNonBlank(visitor.getMobile(), valueOf(visitorProfile, MpVisitor::getVisitorMobile)));
-        payload.put("workUnit", firstNonBlank(valueOf(visitorProfile, MpVisitor::getVisitorCompany), order.getVisitorOrg()));
         payload.put("carNumber", trimToEmpty(visitor.getPlateNo()));
-        payload.put("visitPurpose", trimToEmpty(visitPurpose));
-        payload.put("jobNo", valueOf(visitedUser, MpUser::getStaffNo));
         payload.put("beVisitorPhone", valueOf(visitedUser, MpUser::getMobile));
         payload.put("startTime", formatDateTime(startTime, QR_TIME_FORMATTER));
         payload.put("endTime", formatDateTime(endTime, QR_TIME_FORMATTER));
@@ -67,9 +101,6 @@
         payload.put("date", formatDateTime(qrGenerateTime, QR_DATE_FORMATTER));
         payload.put("validTime", VALID_TIME_PREFIX + Math.max(validMinutes, 1));
         payload.put("codeId", trimToEmpty(codeId));
-        payload.put("customa", EMPTY);
-        payload.put("customb", EMPTY);
-        payload.put("customc", EMPTY);
         try {
             String json = objectMapper.writeValueAsString(payload);
             log.info(
@@ -79,13 +110,70 @@
                     codeId,
                     qrGenerateTime,
                     qrExpireTime,
-                    json);
+                    toSafePayloadLog(payload));
             return json;
         } catch (JsonProcessingException ex) {
             throw new IllegalStateException("二维码内容生成失败", ex);
         }
     }
 
+    public String encryptQrcodePayload(String plainPayload) {
+        if (!StringUtils.hasText(plainPayload) || !plainPayload.trim().startsWith("{")) {
+            return plainPayload;
+        }
+        if (!properties.isEncryptEnabled()) {
+            return plainPayload;
+        }
+        if (!isValidAesConfig()) {
+            throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "二维码加密配置无效");
+        }
+        try {
+            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+            SecretKeySpec keySpec =
+                    new SecretKeySpec(properties.getAesKey().getBytes(StandardCharsets.UTF_8), "AES");
+            IvParameterSpec ivSpec =
+                    new IvParameterSpec(properties.getAesIv().getBytes(StandardCharsets.UTF_8));
+            cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+            byte[] encrypted = cipher.doFinal(plainPayload.getBytes(StandardCharsets.UTF_8));
+            return Base64.getEncoder().encodeToString(encrypted);
+        } catch (BizException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "二维码加密配置无效");
+        }
+    }
+
+    private boolean isValidAesConfig() {
+        return properties.getAesKey() != null
+                && properties.getAesIv() != null
+                && properties.getAesKey().getBytes(StandardCharsets.UTF_8).length == 16
+                && properties.getAesIv().getBytes(StandardCharsets.UTF_8).length == 16;
+    }
+
+    private Map<String, Object> toSafePayloadLog(Map<String, Object> payload) {
+        Map<String, Object> safe = new LinkedHashMap<>(payload);
+        if (safe.containsKey("visitorIdCard")) {
+            safe.put("visitorIdCard", maskIdCardForLog(safe.get("visitorIdCard")));
+        }
+        if (safe.containsKey("visitorPhone")) {
+            safe.put("visitorPhone", maskMobileForLog(safe.get("visitorPhone")));
+        }
+        if (safe.containsKey("beVisitorPhone")) {
+            safe.put("beVisitorPhone", maskMobileForLog(safe.get("beVisitorPhone")));
+        }
+        return safe;
+    }
+
+    private String maskIdCardForLog(Object value) {
+        String text = value == null ? "" : String.valueOf(value).trim();
+        return StringUtils.hasText(text) ? SensitiveDataUtil.maskIdCard(text) : EMPTY;
+    }
+
+    private String maskMobileForLog(Object value) {
+        String text = value == null ? "" : String.valueOf(value).trim();
+        return StringUtils.hasText(text) ? MobileMaskUtil.mask(text) : EMPTY;
+    }
+
     private LocalDateTime resolveVisitStartTime(AppointmentOrder order) {
         LocalDate visitDate = firstNonNull(order.getVisitStartDate(), order.getVisitEndDate(), LocalDate.now());
         if (!AppointmentBizConstants.TIME_MODE_MULTI.equalsIgnoreCase(trimToEmpty(order.getVisitTimeMode()))) {

--
Gitblit v1.7.1