From 700f9d0da314a4ea701fe9f68d305b67d72ec5fb Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期三, 08 七月 2026 15:08:37 +0800
Subject: [PATCH] 各类修复fix

---
 backend/src/main/java/com/sinata/appointment/service/MiniAppointmentApplicationService.java |  666 +++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 474 insertions(+), 192 deletions(-)

diff --git a/backend/src/main/java/com/sinata/appointment/service/MiniAppointmentApplicationService.java b/backend/src/main/java/com/sinata/appointment/service/MiniAppointmentApplicationService.java
index 4cf442b..6006207 100644
--- a/backend/src/main/java/com/sinata/appointment/service/MiniAppointmentApplicationService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/MiniAppointmentApplicationService.java
@@ -14,12 +14,15 @@
 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;
 import com.sinata.appointment.dto.MiniApprovalDetailVisitorVo;
 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;
@@ -63,11 +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 {
@@ -95,10 +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());
@@ -140,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(
@@ -152,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);
@@ -179,14 +191,20 @@
 
         List<String> chain = approvalEngineService.resolveEnabledNodeChain(order);
         if (chain.isEmpty()) {
-            postApprovalOrchestrationService.onOrderFullyApproved(appointmentOrderMapper.selectById(order.getId()));
+            applicationEventPublisher.publishEvent(new AppointmentApprovedEvent(order.getId()));
         } else {
             ApprovalInstance inst = approvalEngineService.startFlow(order);
             if (inst == null) {
-                postApprovalOrchestrationService.onOrderFullyApproved(appointmentOrderMapper.selectById(order.getId()));
+                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)
@@ -210,7 +228,7 @@
         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);
@@ -219,193 +237,130 @@
         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(
             long page, long size, String status, String visitedUserNameKeyword, LocalDate visitDate) {
         MpUser current = miniAuthApplicationService.currentMpUser();
-        LambdaQueryWrapper<ApprovalTask> tq =
-                new LambdaQueryWrapper<ApprovalTask>()
-                        .eq(ApprovalTask::getDelFlag, 0)
-                        .eq(ApprovalTask::getAssigneeUserId, current.getId())
-                        .in(
-                                ApprovalTask::getTaskStatus,
-                                ApprovalTaskStatus.APPROVED,
-                                ApprovalTaskStatus.REJECTED)
-                        .orderByDesc(ApprovalTask::getUpdateTime)
-                        .orderByDesc(ApprovalTask::getId);
-        if (StringUtils.hasText(status)) {
-            tq.eq(ApprovalTask::getTaskStatus, status.trim());
+        Page<MiniApprovalHistoryRow> rowPage = new Page<>(Math.max(1L, page), Math.max(1L, size));
+        IPage<MiniApprovalHistoryRow> pagedRows =
+                approvalTaskMapper.pageMiniApprovalHistory(
+                        rowPage,
+                        current.getId(),
+                        trimToNull(status),
+                        trimToNull(visitedUserNameKeyword),
+                        visitDate);
+        Page<MiniAppointmentListVo> result =
+                new Page<>(pagedRows.getCurrent(), pagedRows.getSize(), pagedRows.getTotal());
+        List<MiniApprovalHistoryRow> rows = pagedRows.getRecords();
+        if (rows == null || rows.isEmpty()) {
+            result.setRecords(List.of());
+            return result;
         }
-        List<ApprovalTask> tasks = approvalTaskMapper.selectList(tq);
-        if (tasks.isEmpty()) {
-            return new Page<>(page, size);
-        }
-
-        java.util.LinkedHashMap<Long, ApprovalTask> latestTaskByOrder = new java.util.LinkedHashMap<>();
-        for (ApprovalTask task : tasks) {
-            ApprovalInstance inst = approvalInstanceMapper.selectById(task.getInstanceId());
-            if (inst == null || (inst.getDelFlag() != null && inst.getDelFlag() == 1) || inst.getOrderId() == null) {
-                continue;
-            }
-            latestTaskByOrder.putIfAbsent(inst.getOrderId(), task);
-        }
-        if (latestTaskByOrder.isEmpty()) {
-            return new Page<>(page, size);
-        }
-
-        List<AppointmentOrder> orders =
-                appointmentOrderMapper.selectList(
-                        new LambdaQueryWrapper<AppointmentOrder>()
-                                .eq(AppointmentOrder::getDelFlag, 0)
-                                .in(AppointmentOrder::getId, latestTaskByOrder.keySet()));
-        java.util.Map<Long, AppointmentOrder> orderMap =
-                orders.stream().collect(java.util.stream.Collectors.toMap(AppointmentOrder::getId, x -> x, (a, b) -> a));
-
-        List<MiniAppointmentListVo> all = new java.util.ArrayList<>();
-        for (var e : latestTaskByOrder.entrySet()) {
-            AppointmentOrder order = orderMap.get(e.getKey());
-            if (order == null) {
-                continue;
-            }
-            if (StringUtils.hasText(visitedUserNameKeyword)) {
-                if (!StringUtils.hasText(order.getVisitedUserName())
-                        || !order.getVisitedUserName().contains(visitedUserNameKeyword.trim())) {
-                    continue;
-                }
-            }
-            if (visitDate != null) {
-                if (order.getVisitStartDate() == null
-                        || order.getVisitEndDate() == null
-                        || visitDate.isBefore(order.getVisitStartDate())
-                        || visitDate.isAfter(order.getVisitEndDate())) {
-                    continue;
-                }
-            }
+        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);
             MiniAppointmentListVo vo = toListVo(order);
-            vo.setStatus(e.getValue().getTaskStatus());
-            all.add(vo);
+            vo.setStatus(row.getTaskStatus());
+            vo.setVisitedDeptName(deptNameMap.get(order.getVisitedDeptId()));
+            vo.setVisitedUserDisplay(buildVisitedUserDisplay(order.getVisitedUserName(), vo.getVisitedDeptName()));
+            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);
         }
-        return slicePage(page, size, all);
+        result.setRecords(records);
+        return result;
     }
 
     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();
-        List<AppointmentOrder> orders =
-                appointmentOrderMapper.selectList(
-                        new LambdaQueryWrapper<AppointmentOrder>()
-                                .eq(AppointmentOrder::getDelFlag, 0)
-                                .eq(AppointmentOrder::getStatus, AppointmentOrderStatus.APPROVED)
-                                .eq(AppointmentOrder::getVisitedUserId, current.getId()));
-        if (orders.isEmpty()) {
-            return new Page<>(page, size);
+        Page<AppointmentOrder> orderPage = new Page<>(Math.max(1L, page), Math.max(1L, size));
+        IPage<AppointmentOrder> pagedOrders =
+                appointmentOrderMapper.pageMiniVisitRecords(
+                        orderPage, current.getId(), futureOnly, trimToNull(visitorNameKeyword), visitDate);
+        Page<MiniVisitRecordListVo> result =
+                new Page<>(pagedOrders.getCurrent(), pagedOrders.getSize(), pagedOrders.getTotal());
+        List<AppointmentOrder> orders = pagedOrders.getRecords();
+        if (orders == null || orders.isEmpty()) {
+            result.setRecords(List.of());
+            return result;
         }
+        Map<String, String> reasonNameMap = loadReasonNameMap(orders);
         List<Long> orderIds = orders.stream().map(AppointmentOrder::getId).filter(Objects::nonNull).toList();
-        List<AppointmentVisitor> visitors =
-                appointmentVisitorMapper.selectList(
-                        new LambdaQueryWrapper<AppointmentVisitor>()
-                                .eq(AppointmentVisitor::getDelFlag, 0)
-                                .in(AppointmentVisitor::getOrderId, orderIds)
-                                .orderByAsc(AppointmentVisitor::getId));
-        Map<Long, List<String>> visitorNamesMap = new java.util.LinkedHashMap<>();
-        for (AppointmentVisitor row : visitors) {
-            visitorNamesMap.computeIfAbsent(row.getOrderId(), k -> new java.util.ArrayList<>());
-            if (StringUtils.hasText(row.getName())) {
-                visitorNamesMap.get(row.getOrderId()).add(row.getName().trim());
-            }
-        }
-
-        List<MiniVisitRecordListVo> rows = new java.util.ArrayList<>();
+        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) {
-            List<String> names = visitorNamesMap.getOrDefault(order.getId(), List.of());
-            if (StringUtils.hasText(visitorNameKeyword)) {
-                String kw = visitorNameKeyword.trim();
-                boolean hit = false;
-                for (String n : names) {
-                    if (n.contains(kw)) {
-                        hit = true;
-                        break;
-                    }
-                }
-                if (!hit) {
-                    continue;
-                }
-            }
-            if (visitDate != null) {
-                if (order.getVisitStartDate() == null
-                        || order.getVisitEndDate() == null
-                        || visitDate.isBefore(order.getVisitStartDate())
-                        || visitDate.isAfter(order.getVisitEndDate())) {
-                    continue;
-                }
-            }
             MiniVisitRecordListVo vo = new MiniVisitRecordListVo();
             vo.setOrderId(order.getId());
             vo.setOrderNo(order.getOrderNo());
-            vo.setVisitorNames(names);
+            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());
             vo.setVisitTimeSlot(order.getVisitTimeSlot());
             vo.setCreateTime(order.getCreateTime());
-            rows.add(vo);
+            records.add(vo);
         }
-
-        rows.sort(
-                java.util.Comparator
-                        .comparing(
-                                MiniVisitRecordListVo::getVisitEndDate,
-                                java.util.Comparator.nullsLast(java.util.Comparator.naturalOrder()))
-                        .reversed()
-                        .thenComparing(
-                                this::singleDaySlotSortWeight,
-                                java.util.Comparator.reverseOrder())
-                        .thenComparing(
-                                MiniVisitRecordListVo::getCreateTime,
-                                java.util.Comparator.nullsLast(java.util.Comparator.reverseOrder())));
-        return slicePageVisitRecord(page, size, rows);
+        result.setRecords(records);
+        return result;
     }
 
     public IPage<MiniAppointmentListVo> pageMyOrders(
             long page, long size, String status, String keyword) {
         MpUser current = miniAuthApplicationService.currentMpUser();
-        List<AppointmentOrder> orders =
-                appointmentOrderMapper.selectList(
-                        new LambdaQueryWrapper<AppointmentOrder>()
-                                .eq(AppointmentOrder::getDelFlag, 0)
-                                .eq(AppointmentOrder::getSubmitterId, current.getId())
-                                .orderByDesc(AppointmentOrder::getCreateTime)
-                                .orderByDesc(AppointmentOrder::getId));
-        if (orders.isEmpty()) {
-            return new Page<>(page, size);
+        Page<AppointmentOrder> orderPage = new Page<>(Math.max(1L, page), Math.max(1L, size));
+        IPage<AppointmentOrder> pagedOrders =
+                appointmentOrderMapper.pageMiniMyOrders(
+                        orderPage, current.getId(), trimToNull(status), trimToNull(keyword));
+        Page<MiniAppointmentListVo> result =
+                new Page<>(pagedOrders.getCurrent(), pagedOrders.getSize(), pagedOrders.getTotal());
+        List<AppointmentOrder> orders = pagedOrders.getRecords();
+        if (orders == null || orders.isEmpty()) {
+            result.setRecords(List.of());
+            return result;
         }
-
         Map<Long, String> deptNameMap = loadDeptNameMap(orders);
         Map<String, String> reasonNameMap = loadReasonNameMap(orders);
-        String normalizedKeyword = trimToNull(keyword);
-        String normalizedStatus = trimToNull(status);
-        List<MiniAppointmentListVo> rows = new ArrayList<>();
+        List<MiniAppointmentListVo> records = new ArrayList<>();
         for (AppointmentOrder order : orders) {
-            MiniAppointmentListVo vo = toMyOrderListVo(order, deptNameMap, reasonNameMap);
-            if (StringUtils.hasText(normalizedStatus)
-                    && !Objects.equals(normalizedStatus, vo.getDisplayStatus())) {
-                continue;
-            }
-            if (StringUtils.hasText(normalizedKeyword)
-                    && !containsIgnoreCase(vo.getVisitedUserDisplay(), normalizedKeyword)
-                    && !containsIgnoreCase(vo.getVisitReasonText(), normalizedKeyword)) {
-                continue;
-            }
-            rows.add(vo);
+            records.add(toMyOrderListVo(order, deptNameMap, reasonNameMap));
         }
-
-        rows.sort(
-                Comparator.comparing(
-                                MiniAppointmentListVo::getCreateTime,
-                                Comparator.nullsLast(Comparator.reverseOrder()))
-                        .thenComparing(
-                                MiniAppointmentListVo::getId,
-                                Comparator.nullsLast(Comparator.reverseOrder())));
-        return slicePage(page, size, rows);
+        result.setRecords(records);
+        return result;
     }
 
     public List<MiniMyVisitorCodeOrderVo> listMyVisitorCodes() {
@@ -418,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();
         }
@@ -479,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);
@@ -511,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;
@@ -538,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 =
@@ -790,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;
     }
@@ -807,6 +890,25 @@
         vo.setVisitReasonText(buildVisitReasonText(row, reasonNameMap));
         vo.setAccessAreaText(buildAccessAreaText(row.getPassAccessAreaNames()));
         return vo;
+    }
+
+    private AppointmentOrder toOrderSnapshot(MiniApprovalHistoryRow row) {
+        AppointmentOrder order = new AppointmentOrder();
+        order.setId(row.getId());
+        order.setUuid(row.getUuid());
+        order.setOrderNo(row.getOrderNo());
+        order.setVisitedUserName(row.getVisitedUserName());
+        order.setVisitedDeptId(row.getVisitedDeptId());
+        order.setVisitStartDate(row.getVisitStartDate());
+        order.setVisitEndDate(row.getVisitEndDate());
+        order.setVisitReasonCode(row.getVisitReasonCode());
+        order.setVisitReasonOther(row.getVisitReasonOther());
+        order.setVisitTimeMode(row.getVisitTimeMode());
+        order.setVisitTimeSlot(row.getVisitTimeSlot());
+        order.setPassAccessAreaNames(row.getPassAccessAreaNames());
+        order.setStatus(row.getStatus());
+        order.setCreateTime(row.getCreateTime());
+        return order;
     }
 
     private Map<Long, String> loadDeptNameMap(List<AppointmentOrder> orders) {
@@ -852,6 +954,26 @@
                                 LinkedHashMap::new));
     }
 
+    private Map<Long, List<String>> loadVisitorNamesMap(List<Long> orderIds) {
+        if (orderIds == null || orderIds.isEmpty()) {
+            return Map.of();
+        }
+        List<AppointmentVisitor> visitors =
+                appointmentVisitorMapper.selectList(
+                        new LambdaQueryWrapper<AppointmentVisitor>()
+                                .eq(AppointmentVisitor::getDelFlag, 0)
+                                .in(AppointmentVisitor::getOrderId, orderIds)
+                                .orderByAsc(AppointmentVisitor::getId));
+        Map<Long, List<String>> result = new LinkedHashMap<>();
+        for (AppointmentVisitor row : visitors) {
+            result.computeIfAbsent(row.getOrderId(), k -> new ArrayList<>());
+            if (StringUtils.hasText(row.getName())) {
+                result.get(row.getOrderId()).add(row.getName().trim());
+            }
+        }
+        return result;
+    }
+
     private String buildVisitedUserDisplay(String userName, String deptName) {
         String name = trimToNull(userName);
         String dept = trimToNull(deptName);
@@ -862,6 +984,92 @@
             return name;
         }
         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) {
@@ -895,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) {
@@ -920,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) {
@@ -947,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);
         };
     }
 
@@ -994,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));
@@ -1076,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();

--
Gitblit v1.7.1