From 937f99a139bedbacdf15e25cfd55aa592f56082c Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期二, 02 六月 2026 15:01:59 +0800
Subject: [PATCH] feat(appointment): 添加访客预约系统敏感词库功能

---
 backend/src/main/java/com/sinata/appointment/service/MiniHandlerApprovalApplicationService.java |  201 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 191 insertions(+), 10 deletions(-)

diff --git a/backend/src/main/java/com/sinata/appointment/service/MiniHandlerApprovalApplicationService.java b/backend/src/main/java/com/sinata/appointment/service/MiniHandlerApprovalApplicationService.java
index 6eb5035..b9fea3e 100644
--- a/backend/src/main/java/com/sinata/appointment/service/MiniHandlerApprovalApplicationService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/MiniHandlerApprovalApplicationService.java
@@ -15,16 +15,24 @@
 import com.sinata.appointment.dto.MiniHandlerApprovalActionRequest;
 import com.sinata.appointment.dto.MiniHandlerApprovalTodoVo;
 import com.sinata.appointment.entity.AppointmentOrder;
+import com.sinata.appointment.entity.AppointmentVehicle;
+import com.sinata.appointment.entity.AppointmentVisitor;
 import com.sinata.appointment.entity.ApprovalInstance;
 import com.sinata.appointment.entity.ApprovalTask;
 import com.sinata.appointment.entity.BizApprovalNodeConfig;
+import com.sinata.appointment.entity.BizVisitReason;
 import com.sinata.appointment.entity.MpPassAccessArea;
 import com.sinata.appointment.entity.MpPassLocalUnit;
+import com.sinata.appointment.entity.MpVisitor;
 import com.sinata.appointment.exception.BizException;
 import com.sinata.appointment.mapper.AppointmentOrderMapper;
+import com.sinata.appointment.mapper.AppointmentVehicleMapper;
+import com.sinata.appointment.mapper.AppointmentVisitorMapper;
 import com.sinata.appointment.mapper.ApprovalInstanceMapper;
 import com.sinata.appointment.mapper.ApprovalTaskMapper;
 import com.sinata.appointment.mapper.BizApprovalNodeConfigMapper;
+import com.sinata.appointment.mapper.BizVisitReasonMapper;
+import com.sinata.appointment.mapper.MpVisitorMapper;
 import com.sinata.appointment.mapper.MpPassAccessAreaMapper;
 import com.sinata.appointment.mapper.MpPassLocalUnitMapper;
 import java.time.LocalDate;
@@ -33,8 +41,10 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Stream;
 import java.util.stream.Collectors;
+import java.util.LinkedHashMap;
 import lombok.extern.slf4j.Slf4j;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -50,6 +60,10 @@
     private final ApprovalTaskMapper approvalTaskMapper;
     private final ApprovalInstanceMapper approvalInstanceMapper;
     private final AppointmentOrderMapper appointmentOrderMapper;
+    private final AppointmentVisitorMapper appointmentVisitorMapper;
+    private final AppointmentVehicleMapper appointmentVehicleMapper;
+    private final MpVisitorMapper mpVisitorMapper;
+    private final BizVisitReasonMapper bizVisitReasonMapper;
     private final ApprovalEngineService approvalEngineService;
     private final IntegrationWechatService integrationWechatService;
     private final PostApprovalOrchestrationService postApprovalOrchestrationService;
@@ -87,11 +101,14 @@
                         .filter(Objects::nonNull)
                         .toList();
         Map<Long, AppointmentOrder> orderMap = loadOrderMap(orderIds);
+        Map<Long, String> visitorDisplayMap = loadVisitorDisplayMap(orderIds);
+        Map<Long, String> plateTextMap = loadPlateTextMap(orderIds);
+        Map<String, String> reasonNameMap = loadReasonNameMap(orderMap.values());
         Map<String, String> nodeNameMap = loadNodeNameMap();
         List<MiniApprovalTodoVo> rowsAll =
                 tasks.stream()
-                        .filter(t -> matchTodoFilters(t, instanceMap, orderMap, visitedUserNameKeyword, visitDate))
-                        .map(t -> toMiniTodoVo(t, instanceMap, orderMap, nodeNameMap))
+                        .filter(t -> matchTodoFilters(t, instanceMap, orderMap, visitorDisplayMap, visitedUserNameKeyword, visitDate))
+                        .map(t -> toMiniTodoVo(t, instanceMap, orderMap, nodeNameMap, visitorDisplayMap, plateTextMap, reasonNameMap))
                         .toList();
         return slicePage(page, size, rowsAll);
     }
@@ -126,9 +143,10 @@
                         .filter(Objects::nonNull)
                         .toList();
         Map<Long, AppointmentOrder> orderMap = loadOrderMap(orderIds);
+        Map<Long, String> visitorDisplayMap = loadVisitorDisplayMap(orderIds);
         List<MiniHandlerApprovalTodoVo> rowsAll =
                 tasks.stream()
-                        .filter(t -> matchTodoFilters(t, instanceMap, orderMap, visitedUserNameKeyword, visitDate))
+                        .filter(t -> matchTodoFilters(t, instanceMap, orderMap, visitorDisplayMap, visitedUserNameKeyword, visitDate))
                         .map(t -> toTodoVo(t, instanceMap, orderMap))
                         .toList();
         return slicePage(page, size, rowsAll);
@@ -151,10 +169,7 @@
         if (!Objects.equals(task.getAssigneeUserId(), uid)) {
             throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "无权处理该任务");
         }
-        String comment = req.getComment().trim();
-        if (!StringUtils.hasText(comment)) {
-            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "审批意见不能为空");
-        }
+        String comment = req.getComment() == null ? null : req.getComment().trim();
 
         ApprovalInstance inst = approvalInstanceMapper.selectById(task.getInstanceId());
         if (inst == null || (inst.getDelFlag() != null && inst.getDelFlag() == 1)) {
@@ -166,6 +181,10 @@
         }
 
         if (Boolean.FALSE.equals(req.getApprove())) {
+            if (!StringUtils.hasText(comment)) {
+                throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "审批意见不能为空");
+            }
+            validateRejectCommentLength(comment);
             task.setTaskStatus(ApprovalTaskStatus.REJECTED);
             task.setComment(comment);
             approvalTaskMapper.updateById(task);
@@ -227,6 +246,9 @@
         String comment = req.getComment() == null ? null : req.getComment().trim();
         appendNoticeIdsIfNeeded(task, req.getNoticeIds());
         if (Boolean.FALSE.equals(req.getApprove())) {
+            if (StringUtils.hasText(comment)) {
+                validateRejectCommentLength(comment);
+            }
             String reason = StringUtils.hasText(comment) ? comment : "拒绝";
             task.setTaskStatus(ApprovalTaskStatus.REJECTED);
             task.setComment(reason);
@@ -358,7 +380,10 @@
             ApprovalTask task,
             Map<Long, ApprovalInstance> instanceMap,
             Map<Long, AppointmentOrder> orderMap,
-            Map<String, String> nodeNameMap) {
+            Map<String, String> nodeNameMap,
+            Map<Long, String> visitorDisplayMap,
+            Map<Long, String> plateTextMap,
+            Map<String, String> reasonNameMap) {
         MiniApprovalTodoVo vo = new MiniApprovalTodoVo();
         vo.setTaskId(task.getId());
         vo.setTaskUuid(task.getUuid());
@@ -377,9 +402,14 @@
         vo.setOrderUuid(order.getUuid());
         vo.setOrderNo(order.getOrderNo());
         vo.setVisitedUserName(order.getVisitedUserName());
+        vo.setVisitorDisplayText(visitorDisplayMap.get(order.getId()));
+        vo.setPlateNoText(plateTextMap.get(order.getId()));
         vo.setVisitStartDate(order.getVisitStartDate());
         vo.setVisitEndDate(order.getVisitEndDate());
         vo.setVisitReasonCode(order.getVisitReasonCode());
+        vo.setVisitReasonText(buildVisitReasonText(order, reasonNameMap));
+        vo.setVisitReasonOther(order.getVisitReasonOther());
+        vo.setVisitTimeText(buildVisitTimeText(order));
         return vo;
     }
 
@@ -453,6 +483,12 @@
         return out.stream().toList();
     }
 
+    private void validateRejectCommentLength(String comment) {
+        if (comment.length() > 30) {
+            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "驳回原因最多30个字");
+        }
+    }
+
     private String writeJson(Object value) {
         try {
             return objectMapper.writeValueAsString(value);
@@ -465,6 +501,7 @@
             ApprovalTask task,
             Map<Long, ApprovalInstance> instanceMap,
             Map<Long, AppointmentOrder> orderMap,
+            Map<Long, String> visitorDisplayMap,
             String visitedUserNameKeyword,
             LocalDate visitDate) {
         ApprovalInstance inst = instanceMap.get(task.getInstanceId());
@@ -476,8 +513,9 @@
             return false;
         }
         if (StringUtils.hasText(visitedUserNameKeyword)) {
-            String name = order.getVisitedUserName();
-            if (!StringUtils.hasText(name) || !name.contains(visitedUserNameKeyword.trim())) {
+            String keyword = visitedUserNameKeyword.trim();
+            String visitorDisplayText = visitorDisplayMap.get(order.getId());
+            if (!StringUtils.hasText(visitorDisplayText) || !visitorDisplayText.contains(keyword)) {
                 return false;
             }
         }
@@ -492,6 +530,149 @@
         return true;
     }
 
+    private Map<Long, String> loadVisitorDisplayMap(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 = loadVisitorCompanyMap(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 joined = entry.getValue().stream().distinct().collect(Collectors.joining(","));
+            if (StringUtils.hasText(joined)) {
+                result.put(entry.getKey(), joined);
+            }
+        }
+        return result;
+    }
+
+    private Map<Long, String> loadVisitorCompanyMap(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> loadPlateTextMap(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 joined = entry.getValue().stream().distinct().collect(Collectors.joining(","));
+            if (StringUtils.hasText(joined)) {
+                result.put(entry.getKey(), joined);
+            }
+        }
+        return result;
+    }
+
+    private Map<String, String> loadReasonNameMap(java.util.Collection<AppointmentOrder> orders) {
+        Set<String> codes =
+                orders.stream()
+                        .map(AppointmentOrder::getVisitReasonCode)
+                        .filter(StringUtils::hasText)
+                        .map(String::trim)
+                        .filter(code -> !"OTHER".equalsIgnoreCase(code))
+                        .collect(Collectors.toCollection(LinkedHashSet::new));
+        if (codes.isEmpty()) {
+            return Map.of();
+        }
+        List<BizVisitReason> rows =
+                bizVisitReasonMapper.selectList(
+                        new LambdaQueryWrapper<BizVisitReason>()
+                                .eq(BizVisitReason::getDelFlag, 0)
+                                .in(BizVisitReason::getReasonCode, codes));
+        return rows.stream().collect(Collectors.toMap(BizVisitReason::getReasonCode, BizVisitReason::getReasonName, (a, b) -> a));
+    }
+
+    private String buildVisitReasonText(AppointmentOrder order, Map<String, String> reasonNameMap) {
+        String reasonCode = trimToNull(order.getVisitReasonCode());
+        String reasonName = reasonNameMap.get(reasonCode);
+        return StringUtils.hasText(reasonName) ? reasonName : reasonCode;
+    }
+
+    private String buildVisitTimeText(AppointmentOrder order) {
+        if ("SINGLE".equals(order.getVisitTimeMode())) {
+            String slotLabel = switch (trimToNull(order.getVisitTimeSlot())) {
+                case "ALL_DAY" -> "00:00-23:59";
+                case "AM" -> "00:00-11:59";
+                case "PM" -> "12:00-23:59";
+                default -> "";
+            };
+            if (order.getVisitStartDate() == null) {
+                return slotLabel;
+            }
+            return StringUtils.hasText(slotLabel)
+                    ? order.getVisitStartDate() + " " + slotLabel
+                    : order.getVisitStartDate().toString();
+        }
+        if (order.getVisitStartDate() == null && order.getVisitEndDate() == null) {
+            return "-";
+        }
+        if (Objects.equals(order.getVisitStartDate(), order.getVisitEndDate()) || order.getVisitEndDate() == null) {
+            return String.valueOf(order.getVisitStartDate());
+        }
+        return order.getVisitStartDate() + " 至 " + order.getVisitEndDate();
+    }
+
+    private String trimToNull(String text) {
+        if (text == null) {
+            return null;
+        }
+        String t = text.trim();
+        return t.isEmpty() ? null : t;
+    }
+
     private <T> IPage<T> slicePage(long page, long size, List<T> rowsAll) {
         long total = rowsAll == null ? 0 : rowsAll.size();
         long current = Math.max(1L, page);

--
Gitblit v1.7.1