From 25df88d0ab9d7444ed1d162b9efa1259ac2f6f66 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期一, 27 四月 2026 10:13:53 +0800
Subject: [PATCH] 小程序对接finish

---
 backend/src/main/java/com/sinata/appointment/service/MiniAppointmentApplicationService.java |  205 ++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 187 insertions(+), 18 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 344c06b..1436d30 100644
--- a/backend/src/main/java/com/sinata/appointment/service/MiniAppointmentApplicationService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/MiniAppointmentApplicationService.java
@@ -21,6 +21,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,6 +65,7 @@
 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;
@@ -71,6 +73,7 @@
 import org.springframework.context.ApplicationEventPublisher;
 import com.sinata.appointment.service.event.AppointmentApprovedEvent;
 
+@Slf4j
 @Service
 @RequiredArgsConstructor
 public class MiniAppointmentApplicationService {
@@ -96,6 +99,7 @@
     private final BizHolidayDateCacheService bizHolidayDateCacheService;
     private final SysBizSettingCacheService sysBizSettingCacheService;
     private final ApprovalEngineService approvalEngineService;
+    private final IntegrationWechatService integrationWechatService;
     private final PostApprovalOrchestrationService postApprovalOrchestrationService;
     private final ApprovalFlowViewApplicationService approvalFlowViewApplicationService;
     private final ObjectMapper objectMapper;
@@ -156,6 +160,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,6 +193,12 @@
             ApprovalInstance inst = approvalEngineService.startFlow(order);
             if (inst == null) {
                 applicationEventPublisher.publishEvent(new AppointmentApprovedEvent(order.getId()));
+            } else {
+                try {
+                    integrationWechatService.notifyCurrentApprovers(order, inst.getId());
+                } catch (Exception ex) {
+                    log.warn("notify current approvers failed, orderId={}", order.getId(), ex);
+                }
             }
         }
         return order.getId();
@@ -214,13 +225,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(
@@ -261,12 +293,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();
@@ -326,15 +358,51 @@
                                 .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);
+    }
+
+    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 +455,48 @@
         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;
+    }
+
     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 +504,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;
@@ -842,6 +937,13 @@
         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())) {
@@ -894,16 +996,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);
         };
     }
 
@@ -1023,6 +1125,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