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.config.HikvisionQrcodeProperties; import com.sinata.appointment.entity.ApprovalInstance; import com.sinata.appointment.entity.ApprovalTask; import com.sinata.appointment.entity.AppointmentOrder; import com.sinata.appointment.entity.AppointmentVisitor; import com.sinata.appointment.entity.AppointmentVehicle; 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.AppointmentVehicleMapper; import com.sinata.appointment.mapper.AppointmentVisitorMapper; import com.sinata.appointment.mapper.ApprovalInstanceMapper; import com.sinata.appointment.mapper.ApprovalTaskMapper; 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 java.util.stream.Stream; 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 ApprovalTaskMapper approvalTaskMapper; private final AppointmentVisitorMapper appointmentVisitorMapper; private final AppointmentVehicleMapper appointmentVehicleMapper; private final MpVisitorMapper mpVisitorMapper; private final MpUserMapper mpUserMapper; private final BizVisitReasonMapper bizVisitReasonMapper; private final SysBizSettingCacheService sysBizSettingCacheService; private final HikvisionQrcodeProperties hikvisionQrcodeProperties; @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 driverPlateMap = loadDriverPlateMap(order.getId()); Map visitorProfileMap = loadVisitorProfileMap(visitors); Map existingTicketMap = loadActiveUnusedTicketMap(order.getId(), visitors); int validMinutes = resolveValidMinutes(); 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()); visitor.setPlateNo(driverPlateMap.get(visitor.getMpVisitorId())); String ticketUuid = UUID.randomUUID().toString().replace("-", ""); String visitorIdCardPlain = resolveVisitorIdCardPlain(visitor, visitorProfile); QrcodeTicket ticket = new QrcodeTicket(); ticket.setUuid(ticketUuid); ticket.setOrderId(order.getId()); ticket.setMpVisitorId(visitor.getMpVisitorId()); ticket.setPayload("{}"); ticket.setTicketStatus(QrcodeTicketStatus.UNUSED); ticket.setGenerateTime(generateTime); ticket.setExpireTime(expireTime); qrcodeTicketMapper.insert(ticket); String payload = integrationHikvisionService.requestQrcodePayload( order, visitor, visitorProfile, visitedUser, visitPurpose, areas, generateTime, expireTime, validMinutes, String.valueOf(ticket.getId()), visitorIdCardPlain); ticket.setPayload(payload); qrcodeTicketMapper.updateById(ticket); existingTicketMap.put(visitor.getMpVisitorId(), ticket); createdCount++; log.info( "{} raw-payload orderId={} mpVisitorId={} ticketId={} ticketUuid={} payloadLength={}", IntegrationConstant.HIKVISION_QRCODE_EVENT, order.getId(), visitor.getMpVisitorId(), ticket.getId(), ticketUuid, payload == null ? 0 : payload.length()); } if (visitors.isEmpty()) { log.info("{} orderId={} no visitors found, skip qrcode creation", IntegrationConstant.HIKVISION_QRCODE_EVENT, order.getId()); } try { integrationWechatService.notifyApprovalFinalResult(order, true); integrationWechatService.notifyApprovalNoticeUsers( order, true, loadNoticeUsers(latestInstance.getId()), visitors.size()); } 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); ApprovalInstance latestInstance = loadLatestInstance(order.getId()); if (latestInstance != null) { List visitors = appointmentVisitorMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentVisitor::getDelFlag, 0) .eq(AppointmentVisitor::getOrderId, order.getId())); integrationWechatService.notifyApprovalNoticeUsers( order, false, loadNoticeUsers(latestInstance.getId()), visitors.size()); } } catch (Exception e) { log.warn( "{} orderId={} failed: {}", IntegrationConstant.WECHAT_NOTIFY_EVENT, order.getId(), e.getMessage()); } } private ApprovalInstance loadLatestInstance(Long orderId) { if (orderId == null) { return null; } return approvalInstanceMapper.selectOne( new LambdaQueryWrapper() .eq(ApprovalInstance::getDelFlag, 0) .eq(ApprovalInstance::getOrderId, orderId) .orderByDesc(ApprovalInstance::getId) .last("limit 1")); } private List loadNoticeUsers(Long instanceId) { if (instanceId == null) { return List.of(); } List tasks = approvalTaskMapper.selectList( new LambdaQueryWrapper() .eq(ApprovalTask::getDelFlag, 0) .eq(ApprovalTask::getInstanceId, instanceId) .isNotNull(ApprovalTask::getNoticeIds) .ne(ApprovalTask::getNoticeIds, "") .orderByAsc(ApprovalTask::getId)); if (tasks.isEmpty()) { return List.of(); } LinkedHashSet noticeUserIds = new LinkedHashSet<>(); for (ApprovalTask task : tasks) { noticeUserIds.addAll(parseCommaSeparatedIds(task.getNoticeIds())); } if (noticeUserIds.isEmpty()) { return List.of(); } List users = mpUserMapper.selectList( new LambdaQueryWrapper() .eq(MpUser::getDelFlag, 0) .in(MpUser::getId, noticeUserIds)); Map userMap = new LinkedHashMap<>(); for (MpUser user : users) { userMap.putIfAbsent(user.getId(), user); } return noticeUserIds.stream().map(userMap::get).filter(Objects::nonNull).toList(); } private List parseCommaSeparatedIds(String raw) { if (!StringUtils.hasText(raw)) { return List.of(); } return Stream.of(raw.split(",")) .filter(StringUtils::hasText) .map(String::trim) .map(value -> { try { return Long.parseLong(value); } catch (NumberFormatException ex) { return null; } }) .filter(Objects::nonNull) .filter(id -> id > 0) .distinct() .toList(); } 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 loadDriverPlateMap(Long orderId) { if (orderId == null) { return Map.of(); } List rows = appointmentVehicleMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentVehicle::getDelFlag, 0) .eq(AppointmentVehicle::getOrderId, orderId) .orderByAsc(AppointmentVehicle::getId)); if (rows.isEmpty()) { return Map.of(); } Map> grouped = new LinkedHashMap<>(); for (AppointmentVehicle row : rows) { if (row.getDriverVisitorId() == null || !StringUtils.hasText(row.getPlateNo())) { continue; } grouped.computeIfAbsent(row.getDriverVisitorId(), key -> new java.util.ArrayList<>()).add(row.getPlateNo().trim()); } Map result = new LinkedHashMap<>(); for (Map.Entry> entry : grouped.entrySet()) { result.put(entry.getKey(), String.join(",", entry.getValue())); } 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 int resolveValidMinutes() { Integer configured = hikvisionQrcodeProperties.getValidMinutes(); if (configured != null && configured > 0) { return configured; } return getMaxConsecutiveDays() * 24 * 60; } private String buildVisitPurpose(AppointmentOrder order) { String code = trimToNull(order.getVisitReasonCode()); if (!StringUtils.hasText(code)) { return ""; } if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(code)) { return "其他"; } 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(); } }