package com.sinata.appointment.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.sinata.appointment.constants.AppointmentBizConstants; import com.sinata.appointment.constants.AppointmentOrderStatus; import com.sinata.appointment.constants.ApprovalInstanceStatus; import com.sinata.appointment.constants.IntegrationConstant; import com.sinata.appointment.constants.QrcodeTicketStatus; import com.sinata.appointment.constants.SystemSettingKeys; import com.sinata.appointment.entity.ApprovalInstance; import com.sinata.appointment.entity.AppointmentOrder; import com.sinata.appointment.entity.AppointmentVisitor; import com.sinata.appointment.entity.BizVisitReason; import com.sinata.appointment.entity.MpUser; import com.sinata.appointment.entity.MpVisitor; import com.sinata.appointment.entity.QrcodeTicket; import com.sinata.appointment.mapper.AppointmentOrderMapper; import com.sinata.appointment.mapper.AppointmentVisitorMapper; import com.sinata.appointment.mapper.ApprovalInstanceMapper; import com.sinata.appointment.mapper.BizVisitReasonMapper; import com.sinata.appointment.mapper.MpUserMapper; import com.sinata.appointment.mapper.MpVisitorMapper; import com.sinata.appointment.mapper.QrcodeTicketMapper; import com.sinata.appointment.util.SensitiveDataUtil; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; /** * 审批通过后的编排:出码、通知(异步由集成服务触发)。 */ @Slf4j @Service @RequiredArgsConstructor public class PostApprovalOrchestrationService { private static final int DEFAULT_MAX_CONSECUTIVE_DAYS = 7; private final IntegrationHikvisionService integrationHikvisionService; private final IntegrationWechatService integrationWechatService; private final QrcodeTicketMapper qrcodeTicketMapper; private final AppointmentOrderMapper appointmentOrderMapper; private final ApprovalInstanceMapper approvalInstanceMapper; private final AppointmentVisitorMapper appointmentVisitorMapper; private final MpVisitorMapper mpVisitorMapper; private final MpUserMapper mpUserMapper; private final BizVisitReasonMapper bizVisitReasonMapper; private final SysBizSettingCacheService sysBizSettingCacheService; @Transactional(rollbackFor = Exception.class) public void onOrderFullyApproved(AppointmentOrder order) { ApprovalInstance latestInstance = approvalInstanceMapper.selectOne( new LambdaQueryWrapper() .eq(ApprovalInstance::getDelFlag, 0) .eq(ApprovalInstance::getOrderId, order.getId()) .orderByDesc(ApprovalInstance::getId) .last("limit 1")); if (latestInstance != null && !ApprovalInstanceStatus.APPROVED.equals(latestInstance.getStatus())) { log.info( "{} orderId={} skipped, instanceStatus={}", IntegrationConstant.HIKVISION_QRCODE_EVENT, order.getId(), latestInstance.getStatus()); return; } order.setStatus(AppointmentOrderStatus.APPROVED); appointmentOrderMapper.updateById(order); List visitors = appointmentVisitorMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentVisitor::getDelFlag, 0) .eq(AppointmentVisitor::getOrderId, order.getId()) .orderByAsc(AppointmentVisitor::getId)); Map visitorProfileMap = loadVisitorProfileMap(visitors); Map existingTicketMap = loadActiveUnusedTicketMap(order.getId(), visitors); int validMinutes = getMaxConsecutiveDays() * 24 * 60; LocalDateTime generateTime = resolveGenerateTime(order); LocalDateTime expireTime = generateTime.plusMinutes(validMinutes); String visitPurpose = buildVisitPurpose(order); String areas = normalizeAreas(order.getPassAccessAreaNames()); MpUser visitedUser = loadVisitedUser(order.getVisitedUserId()); int createdCount = 0; for (AppointmentVisitor visitor : visitors) { QrcodeTicket existed = existingTicketMap.get(visitor.getMpVisitorId()); if (existed != null) { log.info( "{} orderId={} mpVisitorId={} reuse ticketId={}", IntegrationConstant.HIKVISION_QRCODE_EVENT, order.getId(), visitor.getMpVisitorId(), existed.getId()); continue; } MpVisitor visitorProfile = visitorProfileMap.get(visitor.getMpVisitorId()); String ticketUuid = UUID.randomUUID().toString().replace("-", ""); String visitorIdCardPlain = resolveVisitorIdCardPlain(visitor, visitorProfile); String payload = integrationHikvisionService.requestQrcodePayload( order, visitor, visitorProfile, visitedUser, visitPurpose, areas, generateTime, expireTime, validMinutes, ticketUuid, visitorIdCardPlain); QrcodeTicket ticket = new QrcodeTicket(); ticket.setUuid(ticketUuid); ticket.setOrderId(order.getId()); ticket.setMpVisitorId(visitor.getMpVisitorId()); ticket.setPayload(payload); ticket.setTicketStatus(QrcodeTicketStatus.UNUSED); ticket.setGenerateTime(generateTime); ticket.setExpireTime(expireTime); qrcodeTicketMapper.insert(ticket); existingTicketMap.put(visitor.getMpVisitorId(), ticket); createdCount++; log.info( "{} raw-payload orderId={} mpVisitorId={} payload={}", IntegrationConstant.HIKVISION_QRCODE_EVENT, order.getId(), visitor.getMpVisitorId(), payload); } if (visitors.isEmpty()) { log.info("{} orderId={} no visitors found, skip qrcode creation", IntegrationConstant.HIKVISION_QRCODE_EVENT, order.getId()); } try { integrationWechatService.notifyApprovalFinalResult(order, true); } catch (Exception e) { log.warn("{} orderId={} failed: {}", IntegrationConstant.WECHAT_NOTIFY_EVENT, order.getId(), e.getMessage()); } log.info( "{} orderId={} createdTicketCount={}", IntegrationConstant.HIKVISION_QRCODE_EVENT, order.getId(), createdCount); } @Transactional(rollbackFor = Exception.class) public void onOrderRejected(AppointmentOrder order, String reason) { order.setStatus(AppointmentOrderStatus.REJECTED); order.setRejectReason(reason); appointmentOrderMapper.updateById(order); try { integrationWechatService.notifyApprovalFinalResult(order, false); } catch (Exception e) { log.warn( "{} orderId={} failed: {}", IntegrationConstant.WECHAT_NOTIFY_EVENT, order.getId(), e.getMessage()); } } private Map loadVisitorProfileMap(List visitors) { if (visitors == null || visitors.isEmpty()) { return Map.of(); } List visitorIds = visitors.stream() .map(AppointmentVisitor::getMpVisitorId) .filter(Objects::nonNull) .distinct() .toList(); if (visitorIds.isEmpty()) { return Map.of(); } List rows = mpVisitorMapper.selectList( new LambdaQueryWrapper() .eq(MpVisitor::getDelFlag, 0) .in(MpVisitor::getId, visitorIds)); Map result = new LinkedHashMap<>(); for (MpVisitor row : rows) { result.putIfAbsent(row.getId(), row); } return result; } private Map loadActiveUnusedTicketMap(Long orderId, List visitors) { if (orderId == null || visitors == null || visitors.isEmpty()) { return new LinkedHashMap<>(); } LinkedHashSet visitorIds = visitors.stream() .map(AppointmentVisitor::getMpVisitorId) .filter(Objects::nonNull) .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); if (visitorIds.isEmpty()) { return new LinkedHashMap<>(); } List rows = qrcodeTicketMapper.selectList( new LambdaQueryWrapper() .eq(QrcodeTicket::getDelFlag, 0) .eq(QrcodeTicket::getOrderId, orderId) .eq(QrcodeTicket::getTicketStatus, QrcodeTicketStatus.UNUSED) .in(QrcodeTicket::getMpVisitorId, visitorIds) .orderByDesc(QrcodeTicket::getId)); Map result = new LinkedHashMap<>(); for (QrcodeTicket row : rows) { if (row.getMpVisitorId() != null) { result.putIfAbsent(row.getMpVisitorId(), row); } } return result; } private LocalDateTime resolveGenerateTime(AppointmentOrder order) { LocalDate visitDate = order.getVisitStartDate() != null ? order.getVisitStartDate() : order.getVisitEndDate(); if (visitDate == null) { visitDate = LocalDate.now(); } return visitDate.atStartOfDay(); } private int getMaxConsecutiveDays() { String raw = trimToNull(sysBizSettingCacheService.getValue(SystemSettingKeys.BOOKING_MAX_CONSECUTIVE_DAYS)); if (!StringUtils.hasText(raw)) { return DEFAULT_MAX_CONSECUTIVE_DAYS; } try { return Math.max(1, Integer.parseInt(raw)); } catch (NumberFormatException ex) { return DEFAULT_MAX_CONSECUTIVE_DAYS; } } private String buildVisitPurpose(AppointmentOrder order) { String code = trimToNull(order.getVisitReasonCode()); if (!StringUtils.hasText(code)) { return ""; } if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(code)) { String other = trimToNull(order.getVisitReasonOther()); return StringUtils.hasText(other) ? "其他-" + other : "其他"; } BizVisitReason reason = bizVisitReasonMapper.selectOne( new LambdaQueryWrapper() .eq(BizVisitReason::getDelFlag, 0) .eq(BizVisitReason::getReasonCode, code) .last("limit 1")); return reason != null && StringUtils.hasText(reason.getReasonName()) ? reason.getReasonName().trim() : code; } private String normalizeAreas(String rawJson) { String raw = trimToNull(rawJson); if (!StringUtils.hasText(raw)) { return ""; } String cleaned = raw.trim(); if (cleaned.startsWith("[") && cleaned.endsWith("]")) { cleaned = cleaned.substring(1, cleaned.length() - 1); } cleaned = cleaned.replace("\"", "").trim(); while (cleaned.endsWith(",")) { cleaned = cleaned.substring(0, cleaned.length() - 1).trim(); } return cleaned; } private MpUser loadVisitedUser(Long visitedUserId) { if (visitedUserId == null) { return null; } return mpUserMapper.selectOne( new LambdaQueryWrapper() .eq(MpUser::getDelFlag, 0) .eq(MpUser::getId, visitedUserId) .last("limit 1")); } private MpVisitor loadVisitor(Long mpVisitorId) { if (mpVisitorId == null) { return null; } return mpVisitorMapper.selectOne( new LambdaQueryWrapper() .eq(MpVisitor::getDelFlag, 0) .eq(MpVisitor::getId, mpVisitorId) .last("limit 1")); } private String resolveVisitorIdCardPlain(AppointmentVisitor visitor, MpVisitor visitorProfile) { if (StringUtils.hasText(visitor.getIdCardCipher())) { return SensitiveDataUtil.decryptIdCard(visitor.getIdCardCipher()); } if (visitorProfile != null && StringUtils.hasText(visitorProfile.getIdCardCipher())) { return SensitiveDataUtil.decryptIdCard(visitorProfile.getIdCardCipher()); } return null; } private String trimToNull(String value) { if (!StringUtils.hasText(value)) { return null; } return value.trim(); } }