longz
2026-07-08 700f9d0da314a4ea701fe9f68d305b67d72ec5fb
backend/src/main/java/com/sinata/appointment/service/PostApprovalOrchestrationService.java
@@ -7,16 +7,21 @@
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;
@@ -24,8 +29,13 @@
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;
@@ -47,11 +57,14 @@
    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) {
@@ -81,7 +94,10 @@
                                .eq(AppointmentVisitor::getDelFlag, 0)
                                .eq(AppointmentVisitor::getOrderId, order.getId())
                                .orderByAsc(AppointmentVisitor::getId));
        int validMinutes = getMaxConsecutiveDays() * 24 * 60;
        Map<Long, String> driverPlateMap = loadDriverPlateMap(order.getId());
        Map<Long, MpVisitor> visitorProfileMap = loadVisitorProfileMap(visitors);
        Map<Long, QrcodeTicket> existingTicketMap = loadActiveUnusedTicketMap(order.getId(), visitors);
        int validMinutes = resolveValidMinutes();
        LocalDateTime generateTime = resolveGenerateTime(order);
        LocalDateTime expireTime = generateTime.plusMinutes(validMinutes);
        String visitPurpose = buildVisitPurpose(order);
@@ -90,17 +106,18 @@
        int createdCount = 0;
        for (AppointmentVisitor visitor : visitors) {
            QrcodeTicket existed = findActiveUnusedTicket(order.getId(), visitor.getMpVisitorId());
            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());
                    order.getId(),
                    visitor.getMpVisitorId(),
                    existed.getId());
                continue;
            }
            MpVisitor visitorProfile = loadVisitor(visitor.getMpVisitorId());
            MpVisitor visitorProfile = visitorProfileMap.get(visitor.getMpVisitorId());
            visitor.setPlateNo(driverPlateMap.get(visitor.getMpVisitorId()));
            String ticketUuid = UUID.randomUUID().toString().replace("-", "");
            String visitorIdCardPlain = resolveVisitorIdCardPlain(visitor, visitorProfile);
            String payload =
@@ -125,13 +142,15 @@
            ticket.setGenerateTime(generateTime);
            ticket.setExpireTime(expireTime);
            qrcodeTicketMapper.insert(ticket);
            existingTicketMap.put(visitor.getMpVisitorId(), ticket);
            createdCount++;
            log.info(
                    "{} raw-payload orderId={} mpVisitorId={} payload={}",
                    "{} raw-payload orderId={} mpVisitorId={} ticketUuid={} payloadLength={}",
                    IntegrationConstant.HIKVISION_QRCODE_EVENT,
                    order.getId(),
                    visitor.getMpVisitorId(),
                    payload);
                    ticketUuid,
                    payload == null ? 0 : payload.length());
        }
        if (visitors.isEmpty()) {
@@ -140,6 +159,8 @@
        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());
        }
@@ -157,6 +178,16 @@
        appointmentOrderMapper.updateById(order);
        try {
            integrationWechatService.notifyApprovalFinalResult(order, false);
            ApprovalInstance latestInstance = loadLatestInstance(order.getId());
            if (latestInstance != null) {
                List<AppointmentVisitor> visitors =
                        appointmentVisitorMapper.selectList(
                                new LambdaQueryWrapper<AppointmentVisitor>()
                                        .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: {}",
@@ -166,20 +197,151 @@
        }
    }
    private QrcodeTicket findActiveUnusedTicket(Long orderId, Long mpVisitorId) {
        LambdaQueryWrapper<QrcodeTicket> query =
                new LambdaQueryWrapper<QrcodeTicket>()
                        .eq(QrcodeTicket::getDelFlag, 0)
                        .eq(QrcodeTicket::getOrderId, orderId)
                        .eq(QrcodeTicket::getTicketStatus, QrcodeTicketStatus.UNUSED)
                        .orderByDesc(QrcodeTicket::getId)
                        .last("limit 1");
        if (mpVisitorId == null) {
            query.isNull(QrcodeTicket::getMpVisitorId);
        } else {
            query.eq(QrcodeTicket::getMpVisitorId, mpVisitorId);
    private ApprovalInstance loadLatestInstance(Long orderId) {
        if (orderId == null) {
            return null;
        }
        return qrcodeTicketMapper.selectOne(query);
        return approvalInstanceMapper.selectOne(
                new LambdaQueryWrapper<ApprovalInstance>()
                        .eq(ApprovalInstance::getDelFlag, 0)
                        .eq(ApprovalInstance::getOrderId, orderId)
                        .orderByDesc(ApprovalInstance::getId)
                        .last("limit 1"));
    }
    private List<MpUser> loadNoticeUsers(Long instanceId) {
        if (instanceId == null) {
            return List.of();
        }
        List<ApprovalTask> tasks =
                approvalTaskMapper.selectList(
                        new LambdaQueryWrapper<ApprovalTask>()
                                .eq(ApprovalTask::getDelFlag, 0)
                                .eq(ApprovalTask::getInstanceId, instanceId)
                                .isNotNull(ApprovalTask::getNoticeIds)
                                .ne(ApprovalTask::getNoticeIds, "")
                                .orderByAsc(ApprovalTask::getId));
        if (tasks.isEmpty()) {
            return List.of();
        }
        LinkedHashSet<Long> noticeUserIds = new LinkedHashSet<>();
        for (ApprovalTask task : tasks) {
            noticeUserIds.addAll(parseCommaSeparatedIds(task.getNoticeIds()));
        }
        if (noticeUserIds.isEmpty()) {
            return List.of();
        }
        List<MpUser> users =
                mpUserMapper.selectList(
                        new LambdaQueryWrapper<MpUser>()
                                .eq(MpUser::getDelFlag, 0)
                                .in(MpUser::getId, noticeUserIds));
        Map<Long, MpUser> userMap = new LinkedHashMap<>();
        for (MpUser user : users) {
            userMap.putIfAbsent(user.getId(), user);
        }
        return noticeUserIds.stream().map(userMap::get).filter(Objects::nonNull).toList();
    }
    private List<Long> 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<Long, MpVisitor> loadVisitorProfileMap(List<AppointmentVisitor> visitors) {
        if (visitors == null || visitors.isEmpty()) {
            return Map.of();
        }
        List<Long> visitorIds =
                visitors.stream()
                        .map(AppointmentVisitor::getMpVisitorId)
                        .filter(Objects::nonNull)
                        .distinct()
                        .toList();
        if (visitorIds.isEmpty()) {
            return Map.of();
        }
        List<MpVisitor> rows =
                mpVisitorMapper.selectList(
                        new LambdaQueryWrapper<MpVisitor>()
                                .eq(MpVisitor::getDelFlag, 0)
                                .in(MpVisitor::getId, visitorIds));
        Map<Long, MpVisitor> result = new LinkedHashMap<>();
        for (MpVisitor row : rows) {
            result.putIfAbsent(row.getId(), row);
        }
        return result;
    }
    private Map<Long, String> loadDriverPlateMap(Long orderId) {
        if (orderId == null) {
            return Map.of();
        }
        List<AppointmentVehicle> rows =
                appointmentVehicleMapper.selectList(
                        new LambdaQueryWrapper<AppointmentVehicle>()
                                .eq(AppointmentVehicle::getDelFlag, 0)
                                .eq(AppointmentVehicle::getOrderId, orderId)
                                .orderByAsc(AppointmentVehicle::getId));
        if (rows.isEmpty()) {
            return Map.of();
        }
        Map<Long, List<String>> 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<Long, String> result = new LinkedHashMap<>();
        for (Map.Entry<Long, List<String>> entry : grouped.entrySet()) {
            result.put(entry.getKey(), String.join(",", entry.getValue()));
        }
        return result;
    }
    private Map<Long, QrcodeTicket> loadActiveUnusedTicketMap(Long orderId, List<AppointmentVisitor> visitors) {
        if (orderId == null || visitors == null || visitors.isEmpty()) {
            return new LinkedHashMap<>();
        }
        LinkedHashSet<Long> visitorIds =
                visitors.stream()
                        .map(AppointmentVisitor::getMpVisitorId)
                        .filter(Objects::nonNull)
                        .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
        if (visitorIds.isEmpty()) {
            return new LinkedHashMap<>();
        }
        List<QrcodeTicket> rows =
                qrcodeTicketMapper.selectList(
                        new LambdaQueryWrapper<QrcodeTicket>()
                                .eq(QrcodeTicket::getDelFlag, 0)
                                .eq(QrcodeTicket::getOrderId, orderId)
                                .eq(QrcodeTicket::getTicketStatus, QrcodeTicketStatus.UNUSED)
                                .in(QrcodeTicket::getMpVisitorId, visitorIds)
                                .orderByDesc(QrcodeTicket::getId));
        Map<Long, QrcodeTicket> result = new LinkedHashMap<>();
        for (QrcodeTicket row : rows) {
            if (row.getMpVisitorId() != null) {
                result.putIfAbsent(row.getMpVisitorId(), row);
            }
        }
        return result;
    }
    private LocalDateTime resolveGenerateTime(AppointmentOrder order) {
@@ -202,14 +364,21 @@
        }
    }
    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)) {
            String other = trimToNull(order.getVisitReasonOther());
            return StringUtils.hasText(other) ? "其他-" + other : "其他";
            return "其他";
        }
        BizVisitReason reason =
                bizVisitReasonMapper.selectOne(