| | |
| | | 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.scheduling.annotation.Async; |
| | | 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 static final DateTimeFormatter TEMPLATE_TIME_FORMATTER = |
| | | DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); |
| | | |
| | | private final WechatMiniGateway wechatMiniGateway; |
| | | private final MpUserMapper mpUserMapper; |
| | | private final ApprovalTaskMapper approvalTaskMapper; |
| | | private final BizVisitReasonMapper bizVisitReasonMapper; |
| | | |
| | | @Async |
| | | public void notifyApprovalFinalResult(AppointmentOrder order, boolean approved) { |
| | | log.info( |
| | | "WeChat stub: notify submitterId={} orderNo={} result={}", |
| | | order.getSubmitterId(), |
| | | order.getOrderNo(), |
| | | approved ? AppointmentOrderStatus.APPROVED : AppointmentOrderStatus.REJECTED); |
| | | try { |
| | | if (order == null || order.getSubmitterId() == null) { |
| | | log.info("Mini subscribe final result skipped, order or submitter empty. orderId={}", |
| | | order == null ? null : order.getId()); |
| | | 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( |
| | | "Mini subscribe final result skipped, 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", buildVisitTemplateTimeValue(order)); |
| | | templateData.put("thing4", abbreviate(buildVisitReasonText(order), 20)); |
| | | wechatMiniGateway.sendSubscribeMessage( |
| | | submitter.getOpenId(), APPROVAL_RESULT_TEMPLATE_ID, APPROVAL_RESULT_PAGE, templateData); |
| | | log.info( |
| | | "Mini subscribe final result success. submitterId={} orderNo={} result={}", |
| | | order.getSubmitterId(), |
| | | order.getOrderNo(), |
| | | approved ? AppointmentOrderStatus.APPROVED : AppointmentOrderStatus.REJECTED); |
| | | } catch (Exception ex) { |
| | | log.warn("Mini subscribe final result failed. orderId={} orderNo={}", |
| | | order == null ? null : order.getId(), order == null ? null : order.getOrderNo(), ex); |
| | | } |
| | | } |
| | | |
| | | @Async |
| | | public void notifyApprovalNoticeUsers( |
| | | AppointmentOrder order, boolean approved, java.util.Collection<MpUser> noticeUsers, int visitorCount) { |
| | | try { |
| | | if (order == null || noticeUsers == null || noticeUsers.isEmpty()) { |
| | | log.info("Mini subscribe notice skipped, order or notice users empty. orderId={}", |
| | | order == null ? null : order.getId()); |
| | | return; |
| | | } |
| | | Map<String, String> templateData = new LinkedHashMap<>(); |
| | | templateData.put("thing6", buildVisitorCountText(visitorCount)); |
| | | templateData.put("time18", buildVisitTemplateTimeValue(order)); |
| | | templateData.put("thing20", abbreviate(buildNoticeBusinessTypeText(order, approved), 20)); |
| | | int successCount = 0; |
| | | int skippedCount = 0; |
| | | int failedCount = 0; |
| | | for (MpUser noticeUser : noticeUsers) { |
| | | if (noticeUser == null || !StringUtils.hasText(noticeUser.getOpenId())) { |
| | | skippedCount++; |
| | | continue; |
| | | } |
| | | try { |
| | | wechatMiniGateway.sendSubscribeMessage( |
| | | noticeUser.getOpenId(), NOTICE_RESULT_TEMPLATE_ID, APPROVAL_RESULT_PAGE, templateData); |
| | | successCount++; |
| | | log.info( |
| | | "Mini subscribe notice success. noticeUserId={} orderNo={} result={}", |
| | | noticeUser.getId(), |
| | | order.getOrderNo(), |
| | | approved ? AppointmentOrderStatus.APPROVED : AppointmentOrderStatus.REJECTED); |
| | | } catch (Exception ex) { |
| | | failedCount++; |
| | | log.warn("Mini subscribe notice failed. noticeUserId={} orderId={} orderNo={}", |
| | | noticeUser.getId(), order.getId(), order.getOrderNo(), ex); |
| | | } |
| | | } |
| | | log.info("Mini subscribe notice completed. orderId={} orderNo={} success={} skipped={} failed={}", |
| | | order.getId(), order.getOrderNo(), successCount, skippedCount, failedCount); |
| | | } catch (Exception ex) { |
| | | log.warn("Mini subscribe notice batch failed. orderId={} orderNo={}", |
| | | order == null ? null : order.getId(), order == null ? null : order.getOrderNo(), ex); |
| | | } |
| | | } |
| | | |
| | | @Async |
| | | public void notifyCurrentApprovers(AppointmentOrder order, Long instanceId) { |
| | | try { |
| | | if (order == null || instanceId == null) { |
| | | log.info("Mini subscribe todo skipped, order or instance empty. orderId={} instanceId={}", |
| | | order == null ? null : order.getId(), instanceId); |
| | | 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()) { |
| | | log.info("Mini subscribe todo skipped, no pending tasks. orderId={} instanceId={}", |
| | | order.getId(), instanceId); |
| | | 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()) { |
| | | log.info("Mini subscribe todo skipped, no assignee users. orderId={} instanceId={}", |
| | | order.getId(), instanceId); |
| | | 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); |
| | | } |
| | | int successCount = 0; |
| | | int skippedCount = 0; |
| | | int failedCount = 0; |
| | | 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())) { |
| | | skippedCount++; |
| | | continue; |
| | | } |
| | | Map<String, String> templateData = new LinkedHashMap<>(); |
| | | templateData.put("thing1", abbreviate(buildTodoTaskName(order), 20)); |
| | | templateData.put("thing3", abbreviate(String.join("、", entry.getValue()), 20)); |
| | | try { |
| | | wechatMiniGateway.sendSubscribeMessage( |
| | | user.getOpenId(), APPROVAL_TODO_TEMPLATE_ID, APPROVAL_TODO_PAGE, templateData); |
| | | successCount++; |
| | | log.info( |
| | | "Mini subscribe todo success. approverId={} orderNo={} stages={}", |
| | | user.getId(), |
| | | order.getOrderNo(), |
| | | entry.getValue()); |
| | | } catch (Exception ex) { |
| | | failedCount++; |
| | | log.warn("Mini subscribe todo failed. approverId={} orderId={} orderNo={} stages={}", |
| | | user.getId(), order.getId(), order.getOrderNo(), entry.getValue(), ex); |
| | | } |
| | | } |
| | | log.info("Mini subscribe todo completed. orderId={} orderNo={} success={} skipped={} failed={}", |
| | | order.getId(), order.getOrderNo(), successCount, skippedCount, failedCount); |
| | | } catch (Exception ex) { |
| | | log.warn("Mini subscribe todo batch failed. orderId={} instanceId={}", |
| | | order == null ? null : order.getId(), instanceId, ex); |
| | | } |
| | | } |
| | | |
| | | 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 buildVisitTemplateTimeValue(AppointmentOrder order) { |
| | | LocalDate start = order.getVisitStartDate(); |
| | | LocalDate end = order.getVisitEndDate(); |
| | | if (start == null && end == null) { |
| | | return "2099-12-31 00:00"; |
| | | } |
| | | if (start == null) { |
| | | start = end; |
| | | } |
| | | if (end == null) { |
| | | end = start; |
| | | } |
| | | return switch (safeTrim(order.getVisitTimeMode())) { |
| | | case "MULTI" -> start.atTime(0, 0).format(TEMPLATE_TIME_FORMATTER); |
| | | default -> start.atTime(resolveTemplateHour(order.getVisitTimeSlot()), 0) |
| | | .format(TEMPLATE_TIME_FORMATTER); |
| | | }; |
| | | } |
| | | |
| | | private String buildVisitReasonText(AppointmentOrder order) { |
| | | String reasonCode = safeTrim(order.getVisitReasonCode()); |
| | | if (!StringUtils.hasText(reasonCode)) { |
| | | return "拜访"; |
| | | } |
| | | if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(reasonCode)) { |
| | | return "其他"; |
| | | } |
| | | 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 int resolveTemplateHour(String slot) { |
| | | return switch (safeTrim(slot)) { |
| | | case "AM" -> 9; |
| | | case "PM" -> 14; |
| | | case "ALL_DAY" -> 9; |
| | | default -> 9; |
| | | }; |
| | | } |
| | | |
| | | 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(); |
| | | } |
| | | } |