| | |
| | | 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; |
| | |
| | | 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 { |
| | |
| | | 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; |
| | |
| | | .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); |
| | | } |
| | |
| | | .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); |
| | |
| | | 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)) { |
| | |
| | | } |
| | | |
| | | 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); |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | 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); |
| | |
| | | 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) { |
| | |
| | | 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()); |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | 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); |
| | |
| | | ApprovalTask task, |
| | | Map<Long, ApprovalInstance> instanceMap, |
| | | Map<Long, AppointmentOrder> orderMap, |
| | | Map<Long, String> visitorDisplayMap, |
| | | String visitedUserNameKeyword, |
| | | LocalDate visitDate) { |
| | | ApprovalInstance inst = instanceMap.get(task.getInstanceId()); |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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); |