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<String, Object> 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<String, Object> toSafePayloadLog(Map<String, Object> payload) {
|
Map<String, Object> 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> T firstNonNull(T... values) {
|
if (values == null) {
|
return null;
|
}
|
for (T value : values) {
|
if (value != null) {
|
return value;
|
}
|
}
|
return null;
|
}
|
|
private <T> String valueOf(T source, StringGetter<T> getter) {
|
if (source == null) {
|
return EMPTY;
|
}
|
String value = getter.get(source);
|
return StringUtils.hasText(value) ? value.trim() : EMPTY;
|
}
|
|
@FunctionalInterface
|
private interface StringGetter<T> {
|
String get(T source);
|
}
|
}
|