longz
2026-07-08 700f9d0da314a4ea701fe9f68d305b67d72ec5fb
backend/src/main/java/com/sinata/appointment/service/MiniAppointmentApplicationService.java
@@ -14,6 +14,7 @@
import com.sinata.appointment.constants.QrcodeTicketStatus;
import com.sinata.appointment.constants.SystemSettingKeys;
import com.sinata.appointment.dto.MiniAppointmentCreateRequest;
import com.sinata.appointment.dto.MiniAppointmentCreateResultVo;
import com.sinata.appointment.dto.MiniAppointmentListVo;
import com.sinata.appointment.dto.MiniApprovalHistoryRow;
import com.sinata.appointment.dto.MiniAppointmentVehicleItemRequest;
@@ -21,6 +22,7 @@
import com.sinata.appointment.dto.MiniMyAppointmentDetailVo;
import com.sinata.appointment.dto.MiniMyVisitorCodeOrderVo;
import com.sinata.appointment.dto.MiniMyVisitorCodeTicketVo;
import com.sinata.appointment.dto.MiniSharedAppointmentDetailVo;
import com.sinata.appointment.dto.MiniVisitRecordListVo;
import com.sinata.appointment.dto.ApprovalFlowNodeVo;
import com.sinata.appointment.entity.AppointmentOrder;
@@ -64,13 +66,16 @@
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.context.ApplicationEventPublisher;
import com.sinata.appointment.service.event.AppointmentApprovedEvent;
import com.sinata.appointment.service.event.AppointmentApproversNotifyEvent;
@Slf4j
@Service
@RequiredArgsConstructor
public class MiniAppointmentApplicationService {
@@ -98,11 +103,13 @@
    private final ApprovalEngineService approvalEngineService;
    private final PostApprovalOrchestrationService postApprovalOrchestrationService;
    private final ApprovalFlowViewApplicationService approvalFlowViewApplicationService;
    private final SensitiveWordFilterService sensitiveWordFilterService;
    private final IntegrationHikvisionService integrationHikvisionService;
    private final ObjectMapper objectMapper;
    private final ApplicationEventPublisher applicationEventPublisher;
    @Transactional(rollbackFor = Exception.class)
    public Long create(MiniAppointmentCreateRequest req) {
    public MiniAppointmentCreateResultVo create(MiniAppointmentCreateRequest req) {
        MpUser current = miniAuthApplicationService.currentMpUser();
        FieldRequiredContext required = loadFieldRequiredContext();
        MpUser visited = loadVisitedUser(req.getVisitedUserId());
@@ -144,7 +151,7 @@
        order.setVisitedUserName(StringUtils.hasText(visited.getNickName()) ? visited.getNickName() : "员工");
        order.setVisitedDeptId(visited.getDeptId());
        order.setVisitReasonCode(req.getVisitReasonCode().trim());
        order.setVisitReasonOther(trimToNull(req.getVisitReasonOther()));
        order.setVisitReasonOther(sensitiveWordFilterService.filter(trimToNull(req.getVisitReasonOther())));
        order.setVisitorOrg(resolveVisitorOrg(visitorMap.values()));
        order.setVisitTimeMode(mode);
        order.setVisitTimeSlot(
@@ -156,6 +163,7 @@
        order.setPersonEntryFlag(personEntry);
        order.setVehicleEntryFlag(vehicleEntry);
        order.setSubmitterId(current.getId());
        order.setSubmitterVisibleFlag(1);
        order.setHolidayFlag(holidayFlag);
        order.setStatus(AppointmentOrderStatus.PENDING_APPROVAL);
        appointmentOrderMapper.insert(order);
@@ -188,9 +196,15 @@
            ApprovalInstance inst = approvalEngineService.startFlow(order);
            if (inst == null) {
                applicationEventPublisher.publishEvent(new AppointmentApprovedEvent(order.getId()));
            } else {
                applicationEventPublisher.publishEvent(
                        new AppointmentApproversNotifyEvent(order.getId(), inst.getId()));
            }
        }
        return order.getId();
        MiniAppointmentCreateResultVo result = new MiniAppointmentCreateResultVo();
        result.setId(order.getId());
        result.setUuid(order.getUuid());
        return result;
    }
    @Transactional(rollbackFor = Exception.class)
@@ -214,13 +228,34 @@
        ApprovalInstance latestInst = loadLatestInstanceByOrderId(order.getId());
        if (latestInst != null
                && ApprovalInstanceStatus.IN_PROGRESS.equals(latestInst.getStatus())) {
            approvalEngineService.rejectInstance(latestInst, CANCEL_REASON_BY_SUBMITTER);
            approvalEngineService.cancelInstance(latestInst, CANCEL_REASON_BY_SUBMITTER);
        }
        order.setStatus(AppointmentOrderStatus.CANCELLED);
        order.setRejectReason(CANCEL_REASON_BY_SUBMITTER);
        appointmentOrderMapper.updateById(order);
        expireUnusedTicketsByOrderId(order.getId());
    }
    @Transactional(rollbackFor = Exception.class)
    public void deleteMyOrderRecord(Long orderId, String uuid) {
        if (orderId == null || orderId <= 0) {
            throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "预约单不存在");
        }
        MpUser current = miniAuthApplicationService.currentMpUser();
        AppointmentOrder order = appointmentOrderMapper.selectById(orderId);
        if (order == null || (order.getDelFlag() != null && order.getDelFlag() == 1)) {
            throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "预约单不存在");
        }
        UuidAssert.match(order, uuid);
        if (!Objects.equals(order.getSubmitterId(), current.getId())) {
            throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "仅预约发起人可删除");
        }
        if (!isDeletableStatus(order.getStatus(), resolveDisplayStatus(order))) {
            throw new BizException(BizErrorCode.ORDER_STATE_INVALID, "当前状态不可删除");
        }
        order.setSubmitterVisibleFlag(0);
        appointmentOrderMapper.updateById(order);
    }
    public IPage<MiniAppointmentListVo> pageMine(
@@ -244,6 +279,9 @@
        List<AppointmentOrder> orders = rows.stream().map(this::toOrderSnapshot).toList();
        Map<Long, String> deptNameMap = loadDeptNameMap(orders);
        Map<String, String> reasonNameMap = loadReasonNameMap(orders);
        List<Long> orderIds = orders.stream().map(AppointmentOrder::getId).filter(Objects::nonNull).toList();
        Map<Long, String> visitorDisplayMap = loadVisitorDisplayMapByOrderIds(orderIds);
        Map<Long, String> plateTextMap = loadPlateTextMapByOrderIds(orderIds);
        List<MiniAppointmentListVo> records = new ArrayList<>();
        for (MiniApprovalHistoryRow row : rows) {
            AppointmentOrder order = toOrderSnapshot(row);
@@ -254,6 +292,8 @@
            vo.setVisitTimeText(buildVisitTimeText(order));
            vo.setVisitReasonText(buildVisitReasonText(order, reasonNameMap));
            vo.setAccessAreaText(buildAccessAreaText(order.getPassAccessAreaNames()));
            vo.setVisitorDisplayText(visitorDisplayMap.get(order.getId()));
            vo.setPlateNoText(plateTextMap.get(order.getId()));
            records.add(vo);
        }
        result.setRecords(records);
@@ -261,12 +301,12 @@
    }
    public IPage<MiniVisitRecordListVo> pageVisitRecords(
            long page, long size, String visitorNameKeyword, LocalDate visitDate) {
            long page, long size, boolean futureOnly, String visitorNameKeyword, LocalDate visitDate) {
        MpUser current = miniAuthApplicationService.currentMpUser();
        Page<AppointmentOrder> orderPage = new Page<>(Math.max(1L, page), Math.max(1L, size));
        IPage<AppointmentOrder> pagedOrders =
                appointmentOrderMapper.pageMiniVisitRecords(
                        orderPage, current.getId(), trimToNull(visitorNameKeyword), visitDate);
                        orderPage, current.getId(), futureOnly, trimToNull(visitorNameKeyword), visitDate);
        Page<MiniVisitRecordListVo> result =
                new Page<>(pagedOrders.getCurrent(), pagedOrders.getSize(), pagedOrders.getTotal());
        List<AppointmentOrder> orders = pagedOrders.getRecords();
@@ -274,13 +314,20 @@
            result.setRecords(List.of());
            return result;
        }
        Map<String, String> reasonNameMap = loadReasonNameMap(orders);
        List<Long> orderIds = orders.stream().map(AppointmentOrder::getId).filter(Objects::nonNull).toList();
        Map<Long, List<String>> visitorNamesMap = loadVisitorNamesMap(orders.stream().map(AppointmentOrder::getId).toList());
        Map<Long, String> visitorDisplayMap = loadVisitorDisplayMapByOrderIds(orderIds);
        Map<Long, String> plateTextMap = loadPlateTextMapByOrderIds(orderIds);
        List<MiniVisitRecordListVo> records = new ArrayList<>();
        for (AppointmentOrder order : orders) {
            MiniVisitRecordListVo vo = new MiniVisitRecordListVo();
            vo.setOrderId(order.getId());
            vo.setOrderNo(order.getOrderNo());
            vo.setVisitorNames(visitorNamesMap.getOrDefault(order.getId(), List.of()));
            vo.setVisitorDisplayText(visitorDisplayMap.get(order.getId()));
            vo.setPlateNoText(plateTextMap.get(order.getId()));
            vo.setVisitReasonText(buildVisitReasonText(order, reasonNameMap));
            vo.setVisitStartDate(order.getVisitStartDate());
            vo.setVisitEndDate(order.getVisitEndDate());
            vo.setVisitTimeMode(order.getVisitTimeMode());
@@ -326,15 +373,81 @@
                                .eq(AppointmentOrder::getStatus, AppointmentOrderStatus.APPROVED)
                                .orderByDesc(AppointmentOrder::getVisitStartDate)
                                .orderByDesc(AppointmentOrder::getId));
        return buildVisitorCodeOrders(orders, true);
    }
    public MiniSharedAppointmentDetailVo detailSharedOrder(String uuid) {
        AppointmentOrder order = loadApprovedOrderByUuid(uuid);
        String deptName = loadDeptName(order.getVisitedDeptId());
        MiniSharedAppointmentDetailVo vo = new MiniSharedAppointmentDetailVo();
        vo.setOrderId(order.getId());
        vo.setOrderUuid(order.getUuid());
        vo.setVisitedUserDisplay(buildVisitedUserDisplay(order.getVisitedUserName(), deptName));
        vo.setAccessAreaText(buildAccessAreaText(order.getPassAccessAreaNames()));
        vo.setVisitTimeText(buildVisitTimeText(order));
        return vo;
    }
    public MiniMyVisitorCodeOrderVo getSharedVisitorCodes(String uuid) {
        AppointmentOrder order = loadApprovedOrderByUuid(uuid);
        List<MiniMyVisitorCodeOrderVo> rows = buildVisitorCodeOrders(List.of(order), false);
        if (rows.isEmpty()) {
            MiniMyVisitorCodeOrderVo vo = new MiniMyVisitorCodeOrderVo();
            vo.setOrderId(order.getId());
            vo.setOrderUuid(order.getUuid());
            vo.setVisitedUserDisplay(buildVisitedUserDisplay(order.getVisitedUserName(), loadDeptName(order.getVisitedDeptId())));
            vo.setAccessAreaText(buildAccessAreaText(order.getPassAccessAreaNames()));
            vo.setVisitTimeText(buildVisitTimeText(order));
            vo.setQrCodes(List.of());
            return vo;
        }
        return rows.get(0);
    }
    public MiniMyVisitorCodeOrderVo getSharedVisitorCodeByTicketUuid(String ticketUuid) {
        QrcodeTicket ticket = loadTicketByUuid(ticketUuid);
        AppointmentOrder order = loadApprovedOrderById(ticket.getOrderId());
        String deptName = loadDeptName(order.getVisitedDeptId());
        MiniMyVisitorCodeOrderVo vo = new MiniMyVisitorCodeOrderVo();
        vo.setOrderId(order.getId());
        vo.setOrderUuid(order.getUuid());
        vo.setVisitedUserDisplay(buildVisitedUserDisplay(order.getVisitedUserName(), deptName));
        vo.setAccessAreaText(buildAccessAreaText(order.getPassAccessAreaNames()));
        vo.setVisitTimeText(buildVisitTimeText(order));
        vo.setValidStartTime(ticket.getGenerateTime());
        vo.setValidEndTime(ticket.getExpireTime());
        if (!QrcodeTicketStatus.UNUSED.equals(ticket.getTicketStatus())) {
            vo.setQrCodes(List.of());
            return vo;
        }
        Map<Long, MpVisitor> visitorMap =
                loadMpVisitorMap(
                        ticket.getMpVisitorId() == null
                                ? Set.of()
                                : Set.of(ticket.getMpVisitorId()));
        List<AppointmentVisitor> orderVisitors =
                loadOrderVisitorRows(List.of(order.getId())).getOrDefault(order.getId(), List.of());
        vo.setQrCodes(List.of(toMyVisitorCodeTicketVo(ticket, orderVisitors, visitorMap)));
        return vo;
    }
    private List<MiniMyVisitorCodeOrderVo> buildVisitorCodeOrders(
            List<AppointmentOrder> orders, boolean futureOnly) {
        if (orders.isEmpty()) {
            return List.of();
        }
        LocalDateTime now = LocalDateTime.now();
        List<AppointmentOrder> eligibleOrders =
                orders.stream()
                        .filter(order -> resolveVisitStartAt(order).isAfter(now))
                        .toList();
        List<AppointmentOrder> eligibleOrders = orders;
        if (futureOnly) {
            LocalDateTime now = LocalDateTime.now();
            eligibleOrders =
                    orders.stream()
                            .filter(order -> resolveVisitStartAt(order).isAfter(now))
                            .toList();
        }
        if (eligibleOrders.isEmpty()) {
            return List.of();
        }
@@ -387,31 +500,82 @@
        return result;
    }
    private AppointmentOrder loadApprovedOrderByUuid(String uuid) {
        String normalizedUuid = trimToNull(uuid);
        if (!StringUtils.hasText(normalizedUuid)) {
            throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "预约单不存在");
        }
        AppointmentOrder order =
                appointmentOrderMapper.selectOne(
                        new LambdaQueryWrapper<AppointmentOrder>()
                                .eq(AppointmentOrder::getDelFlag, 0)
                                .eq(AppointmentOrder::getUuid, normalizedUuid)
                                .eq(AppointmentOrder::getStatus, AppointmentOrderStatus.APPROVED)
                                .last("limit 1"));
        if (order == null) {
            throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "预约单不存在");
        }
        return order;
    }
    private AppointmentOrder loadApprovedOrderById(Long orderId) {
        if (orderId == null || orderId <= 0) {
            throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "预约单不存在");
        }
        AppointmentOrder order =
                appointmentOrderMapper.selectOne(
                        new LambdaQueryWrapper<AppointmentOrder>()
                                .eq(AppointmentOrder::getDelFlag, 0)
                                .eq(AppointmentOrder::getId, orderId)
                                .eq(AppointmentOrder::getStatus, AppointmentOrderStatus.APPROVED)
                                .last("limit 1"));
        if (order == null) {
            throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "预约单不存在");
        }
        return order;
    }
    private QrcodeTicket loadTicketByUuid(String ticketUuid) {
        String normalizedTicketUuid = trimToNull(ticketUuid);
        if (!StringUtils.hasText(normalizedTicketUuid)) {
            throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "访客码不存在");
        }
        QrcodeTicket ticket =
                qrcodeTicketMapper.selectOne(
                        new LambdaQueryWrapper<QrcodeTicket>()
                                .eq(QrcodeTicket::getDelFlag, 0)
                                .eq(QrcodeTicket::getUuid, normalizedTicketUuid)
                                .last("limit 1"));
        if (ticket == null) {
            throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "访客码不存在");
        }
        return ticket;
    }
    public MiniMyAppointmentDetailVo detailMyOrder(Long orderId, String uuid) {
        if (orderId == null || orderId <= 0) {
            throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "预约单不存在");
        }
        MpUser current = miniAuthApplicationService.currentMpUser();
        AppointmentOrder order = appointmentOrderMapper.selectById(orderId);
        if (order == null || (order.getDelFlag() != null && order.getDelFlag() == 1)) {
        AppointmentOrder order = appointmentOrderMapper.selectMiniVisibleOrderById(orderId, current.getId());
        if (order == null) {
            throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "预约单不存在");
        }
        UuidAssert.match(order, uuid);
        if (!Objects.equals(order.getSubmitterId(), current.getId())) {
            throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "无权查看该预约详情");
        }
        String deptName = loadDeptName(order.getVisitedDeptId());
        String reasonName = loadReasonName(order.getVisitReasonCode());
        String displayStatus = resolveDisplayStatus(order);
        MiniMyAppointmentDetailVo vo = new MiniMyAppointmentDetailVo();
        vo.setOrderId(order.getId());
        vo.setOrderUuid(order.getUuid());
        vo.setOrderNo(order.getOrderNo());
        vo.setStatus(order.getStatus());
        vo.setDisplayStatus(resolveDisplayStatus(order));
        vo.setDisplayStatus(displayStatus);
        vo.setStatusText(resolveOrderStatusText(order));
        vo.setCancelable(isCancelableStatus(order.getStatus()));
        vo.setDeletable(isDeletableStatus(order.getStatus(), displayStatus));
        vo.setVisitedUserId(order.getVisitedUserId());
        vo.setVisitedUserName(order.getVisitedUserName());
        vo.setVisitedDeptName(deptName);
@@ -419,9 +583,19 @@
        vo.setAccessAreaText(buildAccessAreaText(order.getPassAccessAreaNames()));
        vo.setVisitTypeText(buildVisitTypeText(order));
        vo.setVisitReasonText(buildVisitReasonText(order, reasonName == null ? Map.of() : Map.of(trimToNull(order.getVisitReasonCode()), reasonName)));
        vo.setVisitReasonCode(order.getVisitReasonCode());
        vo.setVisitReasonOther(order.getVisitReasonOther());
        vo.setVisitTimeText(buildVisitTimeText(order));
        vo.setVisitTimeMode(order.getVisitTimeMode());
        vo.setVisitTimeSlot(order.getVisitTimeSlot());
        vo.setVisitStartDate(order.getVisitStartDate());
        vo.setVisitEndDate(order.getVisitEndDate());
        vo.setPersonEntry(order.getPersonEntryFlag() == null || Boolean.TRUE.equals(order.getPersonEntryFlag()));
        vo.setVehicleEntry(Boolean.TRUE.equals(order.getVehicleEntryFlag()));
        vo.setCreateTime(order.getCreateTime());
        vo.setVisitors(loadOrderVisitors(order.getId()));
        vo.setRebookVisitors(loadRebookVisitors(order.getId()));
        vo.setRebookVehicles(loadRebookVehicles(order.getId()));
        List<ApprovalFlowNodeVo> flowTree = approvalFlowViewApplicationService.treeByOrderId(order.getId());
        vo.setFlowTree(flowTree == null ? List.of() : flowTree);
        return vo;
@@ -446,14 +620,14 @@
        if (!StringUtils.hasText(c)) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "拜访事由不能为空");
        }
        String txt = trimToNull(other);
        if (!StringUtils.hasText(txt)) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "请输入事由说明");
        }
        if (txt.length() > 15) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "事由说明不能超过15个字");
        }
        if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(c)) {
            String txt = trimToNull(other);
            if (!StringUtils.hasText(txt)) {
                throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "选择其他时须填写自定义内容");
            }
            if (txt.length() > 255) {
                throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "自定义内容不能超过255字");
            }
            return;
        }
        Long cnt =
@@ -698,6 +872,7 @@
        vo.setVisitStartDate(row.getVisitStartDate());
        vo.setVisitEndDate(row.getVisitEndDate());
        vo.setVisitReasonCode(row.getVisitReasonCode());
        vo.setVisitReasonOther(row.getVisitReasonOther());
        vo.setCreateTime(row.getCreateTime());
        return vo;
    }
@@ -811,6 +986,92 @@
        return StringUtils.hasText(dept) ? dept : "-";
    }
    private Map<Long, String> loadVisitorDisplayMapByOrderIds(List<Long> orderIds) {
        if (orderIds == null || orderIds.isEmpty()) {
            return Map.of();
        }
        List<AppointmentVisitor> rows =
                appointmentVisitorMapper.selectList(
                        new LambdaQueryWrapper<AppointmentVisitor>()
                                .eq(AppointmentVisitor::getDelFlag, 0)
                                .in(AppointmentVisitor::getOrderId, orderIds)
                                .orderByAsc(AppointmentVisitor::getId));
        Set<Long> visitorIds =
                rows.stream()
                        .map(AppointmentVisitor::getMpVisitorId)
                        .filter(Objects::nonNull)
                        .collect(Collectors.toCollection(LinkedHashSet::new));
        Map<Long, String> companyMap = loadMpVisitorCompanyMap(visitorIds);
        Map<Long, List<String>> grouped = new LinkedHashMap<>();
        for (AppointmentVisitor row : rows) {
            if (row.getOrderId() == null) {
                continue;
            }
            String name = trimToNull(row.getName());
            if (!StringUtils.hasText(name)) {
                continue;
            }
            String company = companyMap.get(row.getMpVisitorId());
            String display = StringUtils.hasText(company) ? name + "(" + company + ")" : name;
            grouped.computeIfAbsent(row.getOrderId(), key -> new ArrayList<>()).add(display);
        }
        Map<Long, String> result = new LinkedHashMap<>();
        for (Map.Entry<Long, List<String>> entry : grouped.entrySet()) {
            String text = entry.getValue().stream().distinct().collect(Collectors.joining("、"));
            if (StringUtils.hasText(text)) {
                result.put(entry.getKey(), text);
            }
        }
        return result;
    }
    private Map<Long, String> loadMpVisitorCompanyMap(Set<Long> visitorIds) {
        if (visitorIds == null || visitorIds.isEmpty()) {
            return Map.of();
        }
        List<MpVisitor> rows =
                mpVisitorMapper.selectList(
                        new LambdaQueryWrapper<MpVisitor>()
                                .eq(MpVisitor::getDelFlag, 0)
                                .in(MpVisitor::getId, visitorIds));
        Map<Long, String> result = new LinkedHashMap<>();
        for (MpVisitor row : rows) {
            result.put(row.getId(), trimToNull(row.getVisitorCompany()));
        }
        return result;
    }
    private Map<Long, String> loadPlateTextMapByOrderIds(List<Long> orderIds) {
        if (orderIds == null || orderIds.isEmpty()) {
            return Map.of();
        }
        List<AppointmentVehicle> rows =
                appointmentVehicleMapper.selectList(
                        new LambdaQueryWrapper<AppointmentVehicle>()
                                .eq(AppointmentVehicle::getDelFlag, 0)
                                .in(AppointmentVehicle::getOrderId, orderIds)
                                .orderByAsc(AppointmentVehicle::getId));
        Map<Long, List<String>> grouped = new LinkedHashMap<>();
        for (AppointmentVehicle row : rows) {
            if (row.getOrderId() == null) {
                continue;
            }
            String plateNo = trimToNull(row.getPlateNo());
            if (!StringUtils.hasText(plateNo)) {
                continue;
            }
            grouped.computeIfAbsent(row.getOrderId(), key -> new ArrayList<>()).add(plateNo);
        }
        Map<Long, String> result = new LinkedHashMap<>();
        for (Map.Entry<Long, List<String>> entry : grouped.entrySet()) {
            String text = entry.getValue().stream().distinct().collect(Collectors.joining("、"));
            if (StringUtils.hasText(text)) {
                result.put(entry.getKey(), text);
            }
        }
        return result;
    }
    private String resolveOrderStatusText(AppointmentOrder row) {
        return switch (trimToNull(row.getStatus())) {
            case AppointmentOrderStatus.PENDING_APPROVAL -> "审批中";
@@ -842,12 +1103,19 @@
        return DISPLAY_STATUS_INVALID;
    }
    private boolean isDeletableStatus(String status, String displayStatus) {
        if (AppointmentOrderStatus.CANCELLED.equals(status)) {
            return true;
        }
        return DISPLAY_STATUS_INVALID.equals(displayStatus);
    }
    private String buildVisitTimeText(AppointmentOrder row) {
        if (AppointmentBizConstants.TIME_MODE_SINGLE.equals(row.getVisitTimeMode())) {
            String slotLabel = switch (trimToNull(row.getVisitTimeSlot())) {
                case AppointmentBizConstants.TIME_SLOT_ALL_DAY -> "全天";
                case AppointmentBizConstants.TIME_SLOT_AM -> "上午";
                case AppointmentBizConstants.TIME_SLOT_PM -> "下午";
                case AppointmentBizConstants.TIME_SLOT_ALL_DAY -> "00:00-23:59";
                case AppointmentBizConstants.TIME_SLOT_AM -> "00:00-11:59";
                case AppointmentBizConstants.TIME_SLOT_PM -> "12:00-23:59";
                default -> "";
            };
            if (row.getVisitStartDate() == null) {
@@ -867,12 +1135,12 @@
    }
    private String buildVisitReasonText(AppointmentOrder row, Map<String, String> reasonNameMap) {
        if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(trimToNull(row.getVisitReasonCode()))) {
            String other = trimToNull(row.getVisitReasonOther());
            return StringUtils.hasText(other) ? "其他-" + other : "其他";
        String code = trimToNull(row.getVisitReasonCode());
        if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(code)) {
            return "其他";
        }
        String reasonName = reasonNameMap.get(trimToNull(row.getVisitReasonCode()));
        return StringUtils.hasText(reasonName) ? reasonName : trimToNull(row.getVisitReasonCode());
        String reasonName = reasonNameMap.get(code);
        return StringUtils.hasText(reasonName) ? reasonName : code;
    }
    private String buildAccessAreaText(String rawJson) {
@@ -894,16 +1162,16 @@
    }
    private LocalDateTime resolveVisitStartAt(AppointmentOrder row) {
        LocalDate date = row.getVisitStartDate() != null ? row.getVisitStartDate() : row.getVisitEndDate();
        LocalDate date = row.getVisitEndDate() != null ? row.getVisitEndDate() : row.getVisitStartDate();
        if (date == null) {
            return LocalDateTime.MIN;
        }
        if (!AppointmentBizConstants.TIME_MODE_SINGLE.equals(row.getVisitTimeMode())) {
            return LocalDateTime.of(date, LocalTime.MIN);
            return LocalDateTime.of(date, LocalTime.MAX);
        }
        return switch (trimToNull(row.getVisitTimeSlot())) {
            case AppointmentBizConstants.TIME_SLOT_PM -> LocalDateTime.of(date, LocalTime.of(12, 0));
            default -> LocalDateTime.of(date, LocalTime.MIN);
            case AppointmentBizConstants.TIME_SLOT_AM -> LocalDateTime.of(date, LocalTime.of(12, 0));
            default -> LocalDateTime.of(date, LocalTime.MAX);
        };
    }
@@ -941,7 +1209,7 @@
        vo.setTicketId(ticket.getId());
        vo.setTicketUuid(ticket.getUuid());
        vo.setMpVisitorId(ticket.getMpVisitorId());
        vo.setPayload(ticket.getPayload());
        vo.setPayload(integrationHikvisionService.encryptQrcodePayload(ticket.getPayload()));
        vo.setGenerateTime(ticket.getGenerateTime());
        vo.setExpireTime(ticket.getExpireTime());
        vo.setVisitorName(resolveVisitorName(ticket.getMpVisitorId(), orderVisitors, visitorMap));
@@ -1023,6 +1291,73 @@
        return result;
    }
    private List<MiniMyAppointmentDetailVo.RebookVisitorItem> loadRebookVisitors(Long orderId) {
        List<AppointmentVisitor> rows =
                appointmentVisitorMapper.selectList(
                        new LambdaQueryWrapper<AppointmentVisitor>()
                                .eq(AppointmentVisitor::getDelFlag, 0)
                                .eq(AppointmentVisitor::getOrderId, orderId)
                                .orderByAsc(AppointmentVisitor::getId));
        if (rows.isEmpty()) {
            return List.of();
        }
        List<Long> visitorIds =
                rows.stream()
                        .map(AppointmentVisitor::getMpVisitorId)
                        .filter(Objects::nonNull)
                        .toList();
        Map<Long, MpVisitor> visitorMap = loadVisitorMap(visitorIds);
        List<MiniMyAppointmentDetailVo.RebookVisitorItem> result = new ArrayList<>();
        for (AppointmentVisitor row : rows) {
            MpVisitor visitor = visitorMap.get(row.getMpVisitorId());
            MiniMyAppointmentDetailVo.RebookVisitorItem item = new MiniMyAppointmentDetailVo.RebookVisitorItem();
            item.setVisitorId(row.getMpVisitorId());
            item.setVisitorUuid(visitor == null ? null : visitor.getUuid());
            item.setName(firstNonBlank(row.getName(), visitor == null ? null : visitor.getVisitorName(), "-"));
            item.setGender(visitor == null ? null : visitor.getGender());
            item.setCompanyName(visitor == null ? null : trimToNull(visitor.getVisitorCompany()));
            item.setMobileMasked(
                    MobileMaskUtil.mask(
                            firstNonBlank(
                                    row.getMobile(),
                                    visitor == null ? null : visitor.getVisitorMobile(),
                                    null)));
            String idCardPlain =
                    StringUtils.hasText(row.getIdCardCipher())
                            ? SensitiveDataUtil.decryptIdCard(row.getIdCardCipher())
                            : (visitor != null && StringUtils.hasText(visitor.getIdCardCipher())
                                    ? SensitiveDataUtil.decryptIdCard(visitor.getIdCardCipher())
                                    : null);
            item.setIdCardMasked(
                    StringUtils.hasText(idCardPlain)
                            ? SensitiveDataUtil.maskIdCard(idCardPlain)
                            : null);
            result.add(item);
        }
        return result;
    }
    private List<MiniMyAppointmentDetailVo.RebookVehicleItem> loadRebookVehicles(Long orderId) {
        List<AppointmentVehicle> rows =
                appointmentVehicleMapper.selectList(
                        new LambdaQueryWrapper<AppointmentVehicle>()
                                .eq(AppointmentVehicle::getDelFlag, 0)
                                .eq(AppointmentVehicle::getOrderId, orderId)
                                .orderByAsc(AppointmentVehicle::getId));
        if (rows.isEmpty()) {
            return List.of();
        }
        List<MiniMyAppointmentDetailVo.RebookVehicleItem> result = new ArrayList<>();
        for (AppointmentVehicle row : rows) {
            MiniMyAppointmentDetailVo.RebookVehicleItem item = new MiniMyAppointmentDetailVo.RebookVehicleItem();
            item.setPlateNo(trimToNull(row.getPlateNo()));
            item.setDriverVisitorId(row.getDriverVisitorId());
            item.setDriverName(trimToNull(row.getDriverName()));
            result.add(item);
        }
        return result;
    }
    private Map<Long, MpVisitor> loadVisitorMap(List<Long> visitorIds) {
        if (visitorIds.isEmpty()) {
            return Map.of();