From 2a3c3cdbf2398695d61633e072f98ee2757e9e27 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期二, 11 八月 2026 11:20:26 +0800
Subject: [PATCH] feat(appointment): 添加访客预约系统解除绑定功能

---
 backend/src/main/java/com/sinata/appointment/service/AppointmentApplicationService.java |  266 +++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 198 insertions(+), 68 deletions(-)

diff --git a/backend/src/main/java/com/sinata/appointment/service/AppointmentApplicationService.java b/backend/src/main/java/com/sinata/appointment/service/AppointmentApplicationService.java
index 8de75bd..f2d6deb 100644
--- a/backend/src/main/java/com/sinata/appointment/service/AppointmentApplicationService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/AppointmentApplicationService.java
@@ -21,6 +21,7 @@
 import com.sinata.appointment.entity.BizVisitReason;
 import com.sinata.appointment.entity.MpUser;
 import com.sinata.appointment.entity.MpVisitor;
+import com.sinata.appointment.entity.MpPassAccessArea;
 import com.sinata.appointment.entity.SysDept;
 import com.sinata.appointment.exception.BizException;
 import com.sinata.appointment.constants.BizErrorCode;
@@ -32,11 +33,15 @@
 import com.sinata.appointment.mapper.BizVisitReasonMapper;
 import com.sinata.appointment.mapper.MpUserMapper;
 import com.sinata.appointment.mapper.MpVisitorMapper;
+import com.sinata.appointment.mapper.MpPassAccessAreaMapper;
 import com.sinata.appointment.mapper.SysDeptMapper;
 import com.sinata.appointment.util.MobileMaskUtil;
 import com.sinata.appointment.util.SensitiveDataUtil;
 import com.sinata.appointment.util.UuidAssert;
-import java.io.ByteArrayOutputStream;
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.write.metadata.WriteSheet;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
@@ -71,6 +76,7 @@
     private final BizVisitReasonMapper bizVisitReasonMapper;
     private final MpUserMapper mpUserMapper;
     private final MpVisitorMapper mpVisitorMapper;
+    private final MpPassAccessAreaMapper mpPassAccessAreaMapper;
     private final SysDeptMapper sysDeptMapper;
     private final ObjectMapper objectMapper;
 
@@ -79,6 +85,7 @@
             long size,
             String visitedUserName,
             Long visitedDeptId,
+            Long accessAreaId,
             String accessArea,
             String localUnit,
             LocalDate visitDateStart,
@@ -87,12 +94,14 @@
             String approvalStatus,
             String visitorKeyword,
             String visitReasonCode) {
+        List<Long> accessAreaIds = resolveAccessAreaFilterIds(accessAreaId);
         Page<AppointmentOrder> orderPage = new Page<>(Math.max(1L, page), Math.max(1L, size));
         IPage<AppointmentOrder> pagedOrders =
                 appointmentOrderMapper.pageAdminOrders(
                         orderPage,
                         trimToNull(visitedUserName),
                         visitedDeptId,
+                        accessAreaIds,
                         trimToNull(accessArea),
                         trimToNull(localUnit),
                         visitDateStart,
@@ -104,9 +113,11 @@
         return buildAdminPage(pagedOrders);
     }
 
-    public byte[] exportExcel(
+    public void exportExcel(
+            HttpServletResponse response,
             String visitedUserName,
             Long visitedDeptId,
+            Long accessAreaId,
             String accessArea,
             String localUnit,
             LocalDate visitDateStart,
@@ -114,47 +125,60 @@
             String displayStatus,
             String approvalStatus,
             String visitorKeyword,
-            String visitReasonCode) {
+            String visitReasonCode)
+            throws IOException {
+        prepareExcelResponse(response, "appointments.xlsx");
+        List<Long> accessAreaIds = resolveAccessAreaFilterIds(accessAreaId);
         long pageNo = 1L;
         long pageSize = 200L;
-        List<AppointmentAdminExportRow> rows = new ArrayList<>();
-        while (true) {
-            Page<AppointmentOrder> orderPage = new Page<>(pageNo, pageSize);
-            IPage<AppointmentOrder> pagedOrders =
-                    appointmentOrderMapper.pageAdminOrders(
-                            orderPage,
-                            trimToNull(visitedUserName),
-                            visitedDeptId,
-                            trimToNull(accessArea),
-                            trimToNull(localUnit),
-                            visitDateStart,
-                            visitDateEnd,
-                            trimToNull(displayStatus),
-                            trimToNull(approvalStatus),
-                            trimToNull(visitorKeyword),
-                            trimToNull(visitReasonCode));
-            List<AppointmentOrder> orders = pagedOrders.getRecords();
-            if (orders == null || orders.isEmpty()) {
-                break;
+        boolean wroteAnyPage = false;
+        try (ExcelWriter writer =
+                EasyExcel.write(response.getOutputStream(), AppointmentAdminExportRow.class).build()) {
+            WriteSheet sheet = EasyExcel.writerSheet("预约记录").build();
+            while (true) {
+                Page<AppointmentOrder> orderPage = new Page<>(pageNo, pageSize);
+                IPage<AppointmentOrder> pagedOrders =
+                        appointmentOrderMapper.pageAdminOrders(
+                                orderPage,
+                                trimToNull(visitedUserName),
+                                visitedDeptId,
+                                accessAreaIds,
+                                trimToNull(accessArea),
+                                trimToNull(localUnit),
+                                visitDateStart,
+                                visitDateEnd,
+                                trimToNull(displayStatus),
+                                trimToNull(approvalStatus),
+                                trimToNull(visitorKeyword),
+                                trimToNull(visitReasonCode));
+                List<AppointmentOrder> orders = pagedOrders.getRecords();
+                if (orders == null || orders.isEmpty()) {
+                    break;
+                }
+                AdminPageData pageData = loadAdminPageData(orders);
+                writer.write(
+                        buildExportRows(
+                                orders,
+                                pageData.visitorRowsMap(),
+                                pageData.approvalInstanceMap(),
+                                pageData.reasonNameMap()),
+                        sheet);
+                wroteAnyPage = true;
+                if (pageNo * pageSize >= pagedOrders.getTotal()) {
+                    break;
+                }
+                pageNo++;
             }
-            AdminPageData pageData = loadAdminPageData(orders);
-            rows.addAll(
-                    buildExportRows(
-                            orders,
-                            pageData.visitorRowsMap(),
-                            pageData.approvalInstanceMap(),
-                            pageData.reasonNameMap()));
-            if (pageNo * pageSize >= pagedOrders.getTotal()) {
-                break;
+            if (!wroteAnyPage) {
+                writer.write(List.of(), sheet);
             }
-            pageNo++;
         }
-        try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
-            EasyExcel.write(bos, AppointmentAdminExportRow.class).sheet("预约记录").doWrite(rows);
-            return bos.toByteArray();
-        } catch (Exception e) {
-            return new byte[0];
-        }
+    }
+
+    private void prepareExcelResponse(HttpServletResponse response, String filename) {
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("UTF-8");
+        response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
     }
 
     public AppointmentOrder getById(Long id, String uuid) {
@@ -174,6 +198,7 @@
         Map<Long, MpVisitor> visitorMap = loadVisitorMap(visitorRows);
         Map<Long, String> plateMap = loadVisitorPlateMap(id);
         ApprovalInstance approvalInstance = loadLatestInstance(id);
+        MpUser submitter = loadSubmitter(order.getSubmitterId());
 
         AppointmentAdminDetailVo vo = new AppointmentAdminDetailVo();
         vo.setId(order.getId());
@@ -186,6 +211,9 @@
         vo.setLocalUnitText(defaultText(buildJsonNameText(order.getPassLocalUnitNames())));
         vo.setPlateNoText(defaultText(loadOrderPlateText(id)));
         vo.setVisitReasonText(defaultText(buildVisitReasonText(order, reasonNameMap)));
+        vo.setVisitReasonOther(defaultText(trimToNull(order.getVisitReasonOther())));
+        vo.setVisitTimeMode(trimToNull(order.getVisitTimeMode()));
+        vo.setVisitTimeSlot(trimToNull(order.getVisitTimeSlot()));
         vo.setVisitStartText(defaultText(resolveVisitStartText(order)));
         vo.setVisitEndText(defaultText(resolveVisitEndText(order)));
         String displayStatusCode = resolveDisplayStatus(order);
@@ -194,6 +222,9 @@
         String approvalStatusCode = resolveApprovalStatusCode(order, approvalInstance);
         vo.setApprovalStatusCode(approvalStatusCode);
         vo.setApprovalStatusText(resolveApprovalStatusText(approvalStatusCode));
+        vo.setSubmitterName(defaultText(resolveSubmitterName(submitter)));
+        vo.setSubmitterMobile(defaultText(trimToNull(submitter == null ? null : submitter.getMobile())));
+        vo.setSubmitTime(order.getCreateTime() == null ? "-" : order.getCreateTime().format(DATE_TIME_FORMATTER));
         vo.setVisitors(buildDetailVisitors(visitorRows, visitorMap, plateMap));
         return vo;
     }
@@ -216,6 +247,8 @@
             return page;
         }
         AdminPageData pageData = loadAdminPageData(orders);
+        List<Long> orderIds = orders.stream().map(AppointmentOrder::getId).filter(Objects::nonNull).toList();
+        Map<Long, String> orderPlateTextMap = loadOrderPlateTextMap(orderIds);
         List<AppointmentAdminListVo> records = new ArrayList<>();
         for (AppointmentOrder order : orders) {
             records.add(
@@ -224,7 +257,8 @@
                             pageData.deptNameMap().get(order.getVisitedDeptId()),
                             pageData.reasonNameMap(),
                             pageData.visitorRowsMap().get(order.getId()),
-                            pageData.approvalInstanceMap().get(order.getId())));
+                            pageData.approvalInstanceMap().get(order.getId()),
+                            orderPlateTextMap.get(order.getId())));
         }
         page.setRecords(records);
         return page;
@@ -257,6 +291,19 @@
                                 .eq(MpVisitor::getDelFlag, 0)
                                 .in(MpVisitor::getId, visitorIds));
         return visitors.stream().collect(Collectors.toMap(MpVisitor::getId, v -> v, (a, b) -> a, LinkedHashMap::new));
+    }
+
+    private MpUser loadSubmitter(Long submitterId) {
+        if (submitterId == null) {
+            return null;
+        }
+        List<MpUser> rows =
+                mpUserMapper.selectList(
+                        new LambdaQueryWrapper<MpUser>()
+                                .eq(MpUser::getDelFlag, 0)
+                                .eq(MpUser::getId, submitterId)
+                                .last("limit 1"));
+        return rows.isEmpty() ? null : rows.get(0);
     }
 
     private Map<Long, String> loadVisitorPlateMap(Long orderId) {
@@ -309,6 +356,34 @@
                 .map(String::trim)
                 .distinct()
                 .collect(Collectors.joining("、"));
+    }
+
+    private Map<Long, String> loadOrderPlateTextMap(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));
+        if (rows == null || rows.isEmpty()) {
+            return Map.of();
+        }
+        Map<Long, LinkedHashSet<String>> grouped = new LinkedHashMap<>();
+        for (AppointmentVehicle row : rows) {
+            if (row.getOrderId() == null || !StringUtils.hasText(row.getPlateNo())) {
+                continue;
+            }
+            grouped.computeIfAbsent(row.getOrderId(), key -> new LinkedHashSet<>());
+            grouped.get(row.getOrderId()).add(row.getPlateNo().trim());
+        }
+        Map<Long, String> result = new LinkedHashMap<>();
+        for (Map.Entry<Long, LinkedHashSet<String>> entry : grouped.entrySet()) {
+            result.put(entry.getKey(), String.join("、", entry.getValue()));
+        }
+        return result;
     }
 
     private Map<Long, List<AppointmentVisitor>> loadVisitorRows(List<Long> orderIds) {
@@ -399,7 +474,8 @@
             String deptName,
             Map<String, String> reasonNameMap,
             List<AppointmentVisitor> visitorRows,
-            ApprovalInstance approvalInstance) {
+            ApprovalInstance approvalInstance,
+            String orderPlateText) {
         AppointmentAdminListVo vo = new AppointmentAdminListVo();
         vo.setId(order.getId());
         vo.setUuid(order.getUuid());
@@ -409,8 +485,9 @@
         vo.setVisitedDeptName(defaultText(trimToNull(deptName)));
         vo.setAccessAreaText(defaultText(buildJsonNameText(order.getPassAccessAreaNames())));
         vo.setLocalUnitText(defaultText(buildJsonNameText(order.getPassLocalUnitNames())));
-        vo.setPlateNoText(defaultText(joinUniqueTexts(visitorRows, AppointmentVisitor::getPlateNo)));
+        vo.setPlateNoText(defaultText(firstNonBlank(orderPlateText, joinUniqueTexts(visitorRows, AppointmentVisitor::getPlateNo), null)));
         vo.setVisitReasonText(defaultText(buildVisitReasonText(order, reasonNameMap)));
+        vo.setVisitReasonOther(defaultText(trimToNull(order.getVisitReasonOther())));
         vo.setVisitTimeText(defaultText(buildVisitTimeText(order)));
         vo.setVisitorNamesText(defaultText(joinUniqueTexts(visitorRows, AppointmentVisitor::getName)));
         vo.setStatus(order.getStatus());
@@ -446,25 +523,25 @@
 
         List<AppointmentAdminExportRow> result = new ArrayList<>();
         for (AppointmentOrder order : orders) {
+            List<AppointmentVisitor> visitors = visitorRowsMap.get(order.getId());
             AppointmentAdminListVo summary =
                     toAdminListVo(
                             order,
                             null,
                             reasonNameMap,
-                            visitorRowsMap.get(order.getId()),
-                            approvalInstanceMap.get(order.getId()));
+                            visitors,
+                            approvalInstanceMap.get(order.getId()),
+                            loadOrderPlateText(order.getId()));
             Map<String, String> nodeApproverNames =
                     approvalNameMap.getOrDefault(order.getId(), Map.of());
             MpUser submitter = submitterMap.get(order.getSubmitterId());
-            List<AppointmentVisitor> visitors = visitorRowsMap.get(order.getId());
+            Map<Long, String> orderPlateMap = visitorPlateMap.getOrDefault(order.getId(), Map.of());
             if (visitors == null || visitors.isEmpty()) {
-                result.add(buildExportRow(summary, order, null, null, Map.of(), nodeApproverNames, submitter));
+                result.add(buildExportRow(summary, order, null, visitorMap, orderPlateMap, nodeApproverNames, submitter));
                 continue;
             }
-            Map<Long, String> orderPlateMap = visitorPlateMap.getOrDefault(order.getId(), Map.of());
             for (AppointmentVisitor visitorRow : visitors) {
-                MpVisitor visitor = visitorMap.get(visitorRow.getMpVisitorId());
-                result.add(buildExportRow(summary, order, visitorRow, visitor, orderPlateMap, nodeApproverNames, submitter));
+                result.add(buildExportRow(summary, order, visitorRow, visitorMap, orderPlateMap, nodeApproverNames, submitter));
             }
         }
         return result;
@@ -474,7 +551,7 @@
             AppointmentAdminListVo summary,
             AppointmentOrder order,
             AppointmentVisitor visitorRow,
-            MpVisitor visitor,
+            Map<Long, MpVisitor> visitorMap,
             Map<Long, String> orderPlateMap,
             Map<String, String> nodeApproverNames,
             MpUser submitter) {
@@ -483,31 +560,36 @@
         row.setVisitedUserName(summary.getVisitedUserName());
         row.setAccessAreaText(summary.getAccessAreaText());
         row.setLocalUnitText(summary.getLocalUnitText());
-        row.setPlateNoText(
-                defaultText(
-                        firstNonBlank(
-                                visitorRow == null ? null : visitorRow.getPlateNo(),
-                                visitorRow == null ? null : orderPlateMap.get(visitorRow.getMpVisitorId()),
-                                summary.getPlateNoText())));
+        row.setPlateNoText(defaultText(summary.getPlateNoText()));
         row.setVisitReasonText(summary.getVisitReasonText());
+        row.setVisitReasonOther(summary.getVisitReasonOther());
         row.setVisitTimeText(summary.getVisitTimeText());
         row.setDisplayStatusText(summary.getDisplayStatusText());
-        row.setVisitorLabel(defaultText(visitorRow == null ? null : trimToNull(visitorRow.getName())));
-        row.setVisitorName(
-                defaultText(
+        MpVisitor visitor = visitorRow == null ? null : visitorMap.get(visitorRow.getMpVisitorId());
+        String visitorName =
+                trimToNull(
                         firstNonBlank(
                                 visitorRow == null ? null : visitorRow.getName(),
                                 visitor == null ? null : visitor.getVisitorName(),
-                                null)));
-        row.setIdCardText(defaultText(resolveIdCardMasked(visitorRow, visitor)));
+                                null));
+        row.setVisitorLabel(defaultText(visitorName));
+        row.setVisitorName(defaultText(visitorName));
+        row.setIdCardText(defaultText(trimToNull(resolveIdCardMasked(visitorRow, visitor))));
         row.setMobileText(
                 defaultText(
-                        MobileMaskUtil.mask(
-                                firstNonBlank(
-                                        visitorRow == null ? null : visitorRow.getMobile(),
-                                        visitor == null ? null : visitor.getVisitorMobile(),
-                                        null))));
-        row.setCompanyName(defaultText(visitor == null ? null : trimToNull(visitor.getVisitorCompany())));
+                        trimToNull(
+                                MobileMaskUtil.mask(
+                                        firstNonBlank(
+                                                visitorRow == null ? null : visitorRow.getMobile(),
+                                                visitor == null ? null : visitor.getVisitorMobile(),
+                                                null)))));
+        row.setCompanyName(defaultText(trimToNull(visitor == null ? null : visitor.getVisitorCompany())));
+        if (visitorRow != null && visitorRow.getMpVisitorId() != null) {
+            String visitorPlateText = trimToNull(orderPlateMap.get(visitorRow.getMpVisitorId()));
+            if (StringUtils.hasText(visitorPlateText)) {
+                row.setPlateNoText(visitorPlateText);
+            }
+        }
         row.setHandlerApproverNames(defaultText(nodeApproverNames.get(ApprovalNodeCode.LEVEL1_HANDLER)));
         row.setDeptLeaderApproverNames(defaultText(nodeApproverNames.get(ApprovalNodeCode.LEVEL2_DEPT_LEADER)));
         row.setVisitDeptApproverNames(defaultText(nodeApproverNames.get(ApprovalNodeCode.LEVEL4_VISIT_DEPT)));
@@ -723,8 +805,7 @@
             return null;
         }
         if ("OTHER".equalsIgnoreCase(code)) {
-            String other = trimToNull(row.getVisitReasonOther());
-            return StringUtils.hasText(other) ? "其他-" + other : "其他";
+            return "其他";
         }
         String reasonName = reasonNameMap.get(code);
         return StringUtils.hasText(reasonName) ? reasonName : code;
@@ -861,8 +942,50 @@
         return StringUtils.hasText(trimmed) ? trimmed : null;
     }
 
+    private List<Long> resolveAccessAreaFilterIds(Long accessAreaId) {
+        if (accessAreaId == null) {
+            return List.of();
+        }
+        List<MpPassAccessArea> allRows =
+                mpPassAccessAreaMapper.selectList(
+                        new LambdaQueryWrapper<MpPassAccessArea>()
+                                .eq(MpPassAccessArea::getDelFlag, 0));
+        if (allRows == null || allRows.isEmpty()) {
+            return List.of(accessAreaId);
+        }
+        Map<Long, List<Long>> childrenByPid = new LinkedHashMap<>();
+        for (MpPassAccessArea row : allRows) {
+            if (row.getId() == null) {
+                continue;
+            }
+            Long pid = row.getPid() == null ? 0L : row.getPid();
+            childrenByPid.computeIfAbsent(pid, key -> new ArrayList<>()).add(row.getId());
+        }
+        LinkedHashSet<Long> idSet = new LinkedHashSet<>();
+        ArrayList<Long> queue = new ArrayList<>();
+        queue.add(accessAreaId);
+        for (int i = 0; i < queue.size(); i++) {
+            Long current = queue.get(i);
+            if (current == null || !idSet.add(current)) {
+                continue;
+            }
+            List<Long> children = childrenByPid.get(current);
+            if (children != null && !children.isEmpty()) {
+                queue.addAll(children);
+            }
+        }
+        return idSet.isEmpty() ? List.of(accessAreaId) : new ArrayList<>(idSet);
+    }
+
     private static String defaultText(String value) {
         return StringUtils.hasText(value) ? value : "-";
+    }
+
+    private String resolveSubmitterName(MpUser submitter) {
+        if (submitter == null) {
+            return null;
+        }
+        return firstNonBlank(submitter.getNickName(), submitter.getMobile());
     }
 
     @SafeVarargs
@@ -883,4 +1006,11 @@
             Map<Long, ApprovalInstance> approvalInstanceMap,
             Map<Long, String> deptNameMap,
             Map<String, String> reasonNameMap) {}
+
+    private enum VisitorFieldType {
+        NAME,
+        ID_CARD,
+        MOBILE,
+        COMPANY
+    }
 }

--
Gitblit v1.7.1