longz
2026-08-11 2a3c3cdbf2398695d61633e072f98ee2757e9e27
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()))) {