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/IntegrationWechatService.java |  245 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 243 insertions(+), 2 deletions(-)

diff --git a/backend/src/main/java/com/sinata/appointment/service/IntegrationWechatService.java b/backend/src/main/java/com/sinata/appointment/service/IntegrationWechatService.java
index 5b0768c..4b111c0 100644
--- a/backend/src/main/java/com/sinata/appointment/service/IntegrationWechatService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/IntegrationWechatService.java
@@ -1,22 +1,263 @@
 package com.sinata.appointment.service;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.sinata.appointment.constants.AppointmentBizConstants;
 import com.sinata.appointment.constants.AppointmentOrderStatus;
+import com.sinata.appointment.constants.ApprovalNodeCode;
+import com.sinata.appointment.constants.ApprovalTaskStatus;
+import com.sinata.appointment.entity.ApprovalTask;
 import com.sinata.appointment.entity.AppointmentOrder;
+import com.sinata.appointment.entity.BizVisitReason;
+import com.sinata.appointment.entity.MpUser;
+import com.sinata.appointment.integration.WechatMiniGateway;
+import com.sinata.appointment.mapper.ApprovalTaskMapper;
+import com.sinata.appointment.mapper.BizVisitReasonMapper;
+import com.sinata.appointment.mapper.MpUserMapper;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 
 /**
- * 微信公众号模板消息适配桩。
+ * 小程序订阅消息发送。
  */
 @Slf4j
 @Service
+@RequiredArgsConstructor
 public class IntegrationWechatService {
 
+    private static final String APPROVAL_RESULT_TEMPLATE_ID =
+            "4FQovCBNFM7rth42AWHTusHajO788eT9VpplSN4ZG2s";
+    private static final String NOTICE_RESULT_TEMPLATE_ID =
+            "GC1S3T2qHCbz8ow130u7elRczuqW6Z8q54mKRCYE-xo";
+    private static final String APPROVAL_RESULT_PAGE = "pages/record/index";
+    private static final String APPROVAL_TODO_TEMPLATE_ID =
+            "BX49OBHvRcoYiCw0ZLqx04BiWQ9KaEQwuVth_Lh_M2A";
+    private static final String APPROVAL_TODO_PAGE = "pages/reviewList/index";
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    private final WechatMiniGateway wechatMiniGateway;
+    private final MpUserMapper mpUserMapper;
+    private final ApprovalTaskMapper approvalTaskMapper;
+    private final BizVisitReasonMapper bizVisitReasonMapper;
+
     public void notifyApprovalFinalResult(AppointmentOrder order, boolean approved) {
+        if (order == null || order.getSubmitterId() == null) {
+            return;
+        }
+        MpUser submitter =
+                mpUserMapper.selectOne(
+                        new LambdaQueryWrapper<MpUser>()
+                                .eq(MpUser::getDelFlag, 0)
+                                .eq(MpUser::getId, order.getSubmitterId())
+                                .last("limit 1"));
+        if (submitter == null || !StringUtils.hasText(submitter.getOpenId())) {
+            log.info(
+                    "Skip mini subscribe notify, submitter not found or openId empty. submitterId={} orderNo={}",
+                    order.getSubmitterId(),
+                    order.getOrderNo());
+            return;
+        }
+        Map<String, String> templateData = new LinkedHashMap<>();
+        templateData.put("thing1", approved ? "审核通过" : "审核未通过");
+        templateData.put("thing2", abbreviate(resolveSubmitterName(submitter), 20));
+        templateData.put("time3", buildVisitTimeText(order));
+        templateData.put("thing4", abbreviate(buildVisitReasonText(order), 20));
+        wechatMiniGateway.sendSubscribeMessage(
+                submitter.getOpenId(), APPROVAL_RESULT_TEMPLATE_ID, APPROVAL_RESULT_PAGE, templateData);
         log.info(
-                "WeChat stub: notify submitterId={} orderNo={} result={}",
+                "Mini subscribe notify sent submitterId={} orderNo={} result={}",
                 order.getSubmitterId(),
                 order.getOrderNo(),
                 approved ? AppointmentOrderStatus.APPROVED : AppointmentOrderStatus.REJECTED);
     }
+
+    public void notifyApprovalNoticeUsers(
+            AppointmentOrder order, boolean approved, java.util.Collection<MpUser> noticeUsers, int visitorCount) {
+        if (order == null || noticeUsers == null || noticeUsers.isEmpty()) {
+            return;
+        }
+        Map<String, String> templateData = new LinkedHashMap<>();
+        templateData.put("thing6", buildVisitorCountText(visitorCount));
+        templateData.put("time18", buildVisitTimeText(order));
+        templateData.put("thing20", abbreviate(buildNoticeBusinessTypeText(order, approved), 20));
+        for (MpUser noticeUser : noticeUsers) {
+            if (noticeUser == null || !StringUtils.hasText(noticeUser.getOpenId())) {
+                continue;
+            }
+            wechatMiniGateway.sendSubscribeMessage(
+                    noticeUser.getOpenId(), NOTICE_RESULT_TEMPLATE_ID, APPROVAL_RESULT_PAGE, templateData);
+            log.info(
+                    "Mini subscribe notice sent noticeUserId={} orderNo={} result={}",
+                    noticeUser.getId(),
+                    order.getOrderNo(),
+                    approved ? AppointmentOrderStatus.APPROVED : AppointmentOrderStatus.REJECTED);
+        }
+    }
+
+    public void notifyCurrentApprovers(AppointmentOrder order, Long instanceId) {
+        if (order == null || instanceId == null) {
+            return;
+        }
+        java.util.List<ApprovalTask> pendingTasks =
+                approvalTaskMapper.selectList(
+                        new LambdaQueryWrapper<ApprovalTask>()
+                                .eq(ApprovalTask::getDelFlag, 0)
+                                .eq(ApprovalTask::getInstanceId, instanceId)
+                                .eq(ApprovalTask::getTaskStatus, ApprovalTaskStatus.PENDING)
+                                .orderByAsc(ApprovalTask::getId));
+        if (pendingTasks.isEmpty()) {
+            return;
+        }
+        java.util.LinkedHashMap<Long, java.util.LinkedHashSet<String>> userNodeNames = new java.util.LinkedHashMap<>();
+        for (ApprovalTask task : pendingTasks) {
+            if (task.getAssigneeUserId() == null) {
+                continue;
+            }
+            userNodeNames.computeIfAbsent(task.getAssigneeUserId(), key -> new java.util.LinkedHashSet<>());
+            userNodeNames.get(task.getAssigneeUserId()).add(resolveNodeName(task.getNodeCode()));
+        }
+        if (userNodeNames.isEmpty()) {
+            return;
+        }
+        java.util.List<MpUser> users =
+                mpUserMapper.selectList(
+                        new LambdaQueryWrapper<MpUser>()
+                                .eq(MpUser::getDelFlag, 0)
+                                .in(MpUser::getId, userNodeNames.keySet()));
+        java.util.Map<Long, MpUser> userMap = new java.util.LinkedHashMap<>();
+        for (MpUser user : users) {
+            userMap.putIfAbsent(user.getId(), user);
+        }
+        for (java.util.Map.Entry<Long, java.util.LinkedHashSet<String>> entry : userNodeNames.entrySet()) {
+            MpUser user = userMap.get(entry.getKey());
+            if (user == null || !StringUtils.hasText(user.getOpenId())) {
+                continue;
+            }
+            Map<String, String> templateData = new LinkedHashMap<>();
+            templateData.put("thing1", abbreviate(buildTodoTaskName(order), 20));
+            templateData.put("thing3", abbreviate(String.join("、", entry.getValue()), 20));
+            wechatMiniGateway.sendSubscribeMessage(
+                    user.getOpenId(), APPROVAL_TODO_TEMPLATE_ID, APPROVAL_TODO_PAGE, templateData);
+            log.info(
+                    "Mini subscribe todo notify sent approverId={} orderNo={} stages={}",
+                    user.getId(),
+                    order.getOrderNo(),
+                    entry.getValue());
+        }
+    }
+
+    private String resolveSubmitterName(MpUser submitter) {
+        if (StringUtils.hasText(submitter.getNickName())) {
+            return submitter.getNickName().trim();
+        }
+        if (StringUtils.hasText(submitter.getMobile())) {
+            return submitter.getMobile().trim();
+        }
+        return "预约人";
+    }
+
+    private String buildVisitTimeText(AppointmentOrder order) {
+        LocalDate start = order.getVisitStartDate();
+        LocalDate end = order.getVisitEndDate();
+        if (start == null && end == null) {
+            return "待定";
+        }
+        if (start == null) {
+            start = end;
+        }
+        if (end == null) {
+            end = start;
+        }
+        return switch (safeTrim(order.getVisitTimeMode())) {
+            case "MULTI" -> formatDate(start);
+            default -> formatDate(start) + " " + mapTimeSlotLabel(order.getVisitTimeSlot());
+        };
+    }
+
+    private String buildVisitReasonText(AppointmentOrder order) {
+        String reasonCode = safeTrim(order.getVisitReasonCode());
+        if (!StringUtils.hasText(reasonCode)) {
+            return "拜访";
+        }
+        if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(reasonCode)) {
+            String other = safeTrim(order.getVisitReasonOther());
+            return StringUtils.hasText(other) ? other : "其他";
+        }
+        BizVisitReason reason =
+                bizVisitReasonMapper.selectOne(
+                        new LambdaQueryWrapper<BizVisitReason>()
+                                .eq(BizVisitReason::getDelFlag, 0)
+                                .eq(BizVisitReason::getReasonCode, reasonCode)
+                                .last("limit 1"));
+        if (reason != null && StringUtils.hasText(reason.getReasonName())) {
+            return reason.getReasonName().trim();
+        }
+        return reasonCode;
+    }
+
+    private String buildVisitorCountText(int visitorCount) {
+        return Math.max(visitorCount, 0) + "人";
+    }
+
+    private String buildNoticeBusinessTypeText(AppointmentOrder order, boolean approved) {
+        String reason = buildVisitReasonText(order);
+        if (!StringUtils.hasText(reason)) {
+            return approved ? "预约通过" : "预约驳回";
+        }
+        return reason;
+    }
+
+    private String buildTodoTaskName(AppointmentOrder order) {
+        if (order == null || !StringUtils.hasText(order.getOrderNo())) {
+            return "预约审核";
+        }
+        return "预约审核-" + order.getOrderNo();
+    }
+
+    private String resolveNodeName(String nodeCode) {
+        return switch (safeTrim(nodeCode)) {
+            case ApprovalNodeCode.LEVEL1_HANDLER -> "经办人(一级)";
+            case ApprovalNodeCode.LEVEL2_DEPT_LEADER -> "部门负责人(二级)";
+            case ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER -> "属地管理部门领导(三级)";
+            case ApprovalNodeCode.LEVEL4_VISIT_DEPT -> "访客主管部门(四级)";
+            case ApprovalNodeCode.LEVEL5_VISIT_DEPT_LEADER -> "访客主管部门领导(五级)";
+            default -> "审批待办";
+        };
+    }
+
+    private String mapTimeSlotLabel(String slot) {
+        return switch (safeTrim(slot)) {
+            case "AM" -> "上午";
+            case "PM" -> "下午";
+            case "ALL_DAY" -> "全天";
+            default -> "全天";
+        };
+    }
+
+    private String formatDate(LocalDate date) {
+        return date.format(DATE_FORMATTER);
+    }
+
+    private String abbreviate(String value, int maxLength) {
+        if (!StringUtils.hasText(value)) {
+            return "";
+        }
+        String trimmed = value.trim();
+        if (trimmed.length() <= maxLength) {
+            return trimmed;
+        }
+        return trimmed.substring(0, maxLength);
+    }
+
+    private String safeTrim(String value) {
+        if (!StringUtils.hasText(value)) {
+            return "";
+        }
+        return value.trim();
+    }
 }

--
Gitblit v1.7.1