package com.sinata.appointment.service; 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; import org.springframework.util.StringUtils; /** * 海康出码适配:当前按 PDF 示例组装二维码 payload 字符串。 */ @Slf4j @Service @RequiredArgsConstructor public class IntegrationHikvisionService { private static final String VISITOR_ID_CARD_TYPE = "111"; private static final String VALID_TIME_PREFIX = "m"; private static final String EMPTY = ""; private static final DateTimeFormatter QR_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); private static final DateTimeFormatter QR_DATE_FORMATTER = 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, MpUser visitedUser, String visitPurpose, String areas, LocalDateTime qrGenerateTime, LocalDateTime qrExpireTime, int validMinutes, String codeId, String visitorIdCardPlain) { LocalDateTime startTime = resolveVisitStartTime(order); LocalDateTime endTime = resolveVisitEndTime(order); Map payload = new LinkedHashMap<>(); payload.put("visitorName", firstNonBlank(visitor.getName(), valueOf(visitorProfile, MpVisitor::getVisitorName))); 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("beVisitorPhone", valueOf(visitedUser, MpUser::getMobile)); payload.put("startTime", formatDateTime(startTime, QR_TIME_FORMATTER)); payload.put("endTime", formatDateTime(endTime, QR_TIME_FORMATTER)); payload.put("areas", trimToEmpty(areas)); payload.put("beVisitorName", firstNonBlank(order.getVisitedUserName(), valueOf(visitedUser, MpUser::getNickName))); payload.put("date", formatDateTime(qrGenerateTime, QR_DATE_FORMATTER)); payload.put("validTime", VALID_TIME_PREFIX + Math.max(validMinutes, 1)); payload.put("codeId", trimToEmpty(codeId)); try { String json = objectMapper.writeValueAsString(payload); log.info( "Hikvision payload prepared orderId={} mpVisitorId={} codeId={} generateTime={} expireTime={} payload={}", order.getId(), visitor.getMpVisitorId(), codeId, qrGenerateTime, qrExpireTime, 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 toSafePayloadLog(Map payload) { Map safe = new LinkedHashMap<>(payload); safe.put("visitorIdCard", maskIdCardForLog(safe.get("visitorIdCard"))); safe.put("visitorPhone", maskMobileForLog(safe.get("visitorPhone"))); 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()))) { String slot = trimToEmpty(order.getVisitTimeSlot()).toUpperCase(); if (AppointmentBizConstants.TIME_SLOT_PM.equals(slot)) { return LocalDateTime.of(visitDate, LocalTime.of(12, 0)); } } return LocalDateTime.of(visitDate, LocalTime.MIN); } private LocalDateTime resolveVisitEndTime(AppointmentOrder order) { LocalDate visitDate = firstNonNull(order.getVisitEndDate(), order.getVisitStartDate(), LocalDate.now()); if (!AppointmentBizConstants.TIME_MODE_MULTI.equalsIgnoreCase(trimToEmpty(order.getVisitTimeMode()))) { String slot = trimToEmpty(order.getVisitTimeSlot()).toUpperCase(); if (AppointmentBizConstants.TIME_SLOT_AM.equals(slot)) { return LocalDateTime.of(visitDate, LocalTime.of(11, 59, 59)); } } return LocalDateTime.of(visitDate, LocalTime.of(23, 59, 59)); } private String formatDateTime(LocalDateTime value, DateTimeFormatter formatter) { return value == null ? EMPTY : value.format(formatter); } private String trimToEmpty(String value) { return value == null ? EMPTY : value.trim(); } private String firstNonBlank(String... values) { if (values == null) { return EMPTY; } for (String value : values) { if (StringUtils.hasText(value)) { return value.trim(); } } return EMPTY; } @SafeVarargs private final T firstNonNull(T... values) { if (values == null) { return null; } for (T value : values) { if (value != null) { return value; } } return null; } private String valueOf(T source, StringGetter getter) { if (source == null) { return EMPTY; } String value = getter.get(source); return StringUtils.hasText(value) ? value.trim() : EMPTY; } @FunctionalInterface private interface StringGetter { String get(T source); } }