| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | import org.springframework.context.ApplicationEventPublisher; |
| | | import com.sinata.appointment.service.event.AppointmentApprovedEvent; |
| | | |
| | | @Slf4j |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class MiniAppointmentApplicationService { |
| | |
| | | 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; |
| | |
| | | order.setPersonEntryFlag(personEntry); |
| | | order.setVehicleEntryFlag(vehicleEntry); |
| | | order.setSubmitterId(current.getId()); |
| | | order.setSubmitterVisibleFlag(1); |
| | | order.setHolidayFlag(holidayFlag); |
| | | order.setStatus(AppointmentOrderStatus.PENDING_APPROVAL); |
| | | appointmentOrderMapper.insert(order); |
| | |
| | | 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(); |
| | |
| | | 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( |
| | |
| | | } |
| | | |
| | | 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(); |
| | |
| | | .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(); |
| | | } |
| | |
| | | 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); |
| | |
| | | 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; |
| | |
| | | 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())) { |
| | |
| | | } |
| | | |
| | | 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); |
| | | }; |
| | | } |
| | | |
| | |
| | | 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(); |