longz
2026-07-13 5882ba8863d77fdcc168b176c3ffb30e8a20556a
backend/src/main/java/com/sinata/appointment/service/MiniHandlerApprovalApplicationService.java
@@ -6,6 +6,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sinata.appointment.config.MiniStpUtil;
import com.sinata.appointment.constants.AppointmentBizConstants;
import com.sinata.appointment.constants.ApprovalInstanceStatus;
import com.sinata.appointment.constants.ApprovalNodeCode;
import com.sinata.appointment.constants.ApprovalTaskStatus;
@@ -15,16 +16,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,13 +42,18 @@
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;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import com.sinata.appointment.util.UuidAssert;
@Slf4j
@Service
@RequiredArgsConstructor
public class MiniHandlerApprovalApplicationService {
@@ -47,7 +61,12 @@
    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;
    private final BizApprovalNodeConfigMapper bizApprovalNodeConfigMapper;
    private final MpPassAccessAreaMapper mpPassAccessAreaMapper;
@@ -83,11 +102,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);
    }
@@ -122,9 +144,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);
@@ -147,10 +170,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)) {
@@ -162,6 +182,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);
@@ -182,6 +206,14 @@
        if (ApprovalInstanceStatus.APPROVED.equals(latest.getStatus())) {
            AppointmentOrder fresh = appointmentOrderMapper.selectById(order.getId());
            postApprovalOrchestrationService.onOrderFullyApproved(fresh);
            return;
        }
        if (ApprovalInstanceStatus.IN_PROGRESS.equals(latest.getStatus())) {
            try {
                integrationWechatService.notifyCurrentApprovers(order, latest.getId());
            } catch (Exception ex) {
                log.warn("notify current approvers failed, orderId={}", order.getId(), ex);
            }
        }
    }
@@ -213,7 +245,11 @@
        }
        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);
@@ -233,8 +269,49 @@
            if (ApprovalInstanceStatus.APPROVED.equals(latest.getStatus())) {
                AppointmentOrder fresh = appointmentOrderMapper.selectById(order.getId());
                postApprovalOrchestrationService.onOrderFullyApproved(fresh);
                return;
            }
            if (ApprovalInstanceStatus.IN_PROGRESS.equals(latest.getStatus())) {
                try {
                    integrationWechatService.notifyCurrentApprovers(order, latest.getId());
                } catch (Exception ex) {
                    log.warn("notify current approvers failed, orderId={}", order.getId(), ex);
                }
            }
        }
    }
    private void appendNoticeIdsIfNeeded(ApprovalTask task, List<Long> noticeIds) {
        if (!ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER.equals(task.getNodeCode())) {
            return;
        }
        List<Long> normalized = normalizeIds(noticeIds);
        if (normalized.isEmpty()) {
            return;
        }
        LinkedHashSet<Long> merged = new LinkedHashSet<>(parseNoticeIds(task.getNoticeIds()));
        merged.addAll(normalized);
        task.setNoticeIds(merged.stream().map(String::valueOf).collect(Collectors.joining(",")));
    }
    private List<Long> parseNoticeIds(String raw) {
        if (!StringUtils.hasText(raw)) {
            return List.of();
        }
        return Stream.of(raw.split(","))
                .filter(StringUtils::hasText)
                .map(String::trim)
                .map(value -> {
                    try {
                        return Long.parseLong(value);
                    } catch (NumberFormatException ex) {
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .filter(id -> id > 0)
                .distinct()
                .toList();
    }
    private Map<Long, AppointmentOrder> loadOrderMap(List<Long> orderIds) {
@@ -304,7 +381,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());
@@ -323,9 +403,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;
    }
@@ -399,6 +484,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);
@@ -411,6 +502,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());
@@ -422,8 +514,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;
            }
        }
@@ -438,6 +531,152 @@
        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());
        if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(reasonCode)) {
            return "其他";
        }
        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);