package com.sinata.appointment.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.sinata.appointment.constants.AppointmentOrderStatus; import com.sinata.appointment.constants.AppointmentBizConstants; import com.sinata.appointment.constants.AppointmentFieldCodes; import com.sinata.appointment.constants.ApprovalInstanceStatus; import com.sinata.appointment.constants.ApprovalTaskStatus; import com.sinata.appointment.constants.BizErrorCode; import com.sinata.appointment.constants.QrcodeTicketStatus; import com.sinata.appointment.constants.SystemSettingKeys; import com.sinata.appointment.dto.MiniAppointmentCreateRequest; import com.sinata.appointment.dto.MiniAppointmentCreateResultVo; import com.sinata.appointment.dto.MiniAppointmentListVo; import com.sinata.appointment.dto.MiniApprovalHistoryRow; import com.sinata.appointment.dto.MiniAppointmentVehicleItemRequest; import com.sinata.appointment.dto.MiniApprovalDetailVisitorVo; 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 com.sinata.appointment.entity.ApprovalInstance; import com.sinata.appointment.entity.ApprovalTask; import com.sinata.appointment.entity.AppointmentVehicle; import com.sinata.appointment.entity.AppointmentVisitor; import com.sinata.appointment.entity.BizFieldConfig; import com.sinata.appointment.entity.BizVisitReason; import com.sinata.appointment.entity.MpUser; import com.sinata.appointment.entity.MpVisitor; import com.sinata.appointment.entity.QrcodeTicket; import com.sinata.appointment.entity.SysDept; import com.sinata.appointment.exception.BizException; import com.sinata.appointment.mapper.AppointmentOrderMapper; import com.sinata.appointment.mapper.ApprovalInstanceMapper; import com.sinata.appointment.mapper.ApprovalTaskMapper; import com.sinata.appointment.mapper.AppointmentVehicleMapper; import com.sinata.appointment.mapper.AppointmentVisitorMapper; import com.sinata.appointment.mapper.BizFieldConfigMapper; import com.sinata.appointment.mapper.BizVisitReasonMapper; import com.sinata.appointment.mapper.MpUserMapper; import com.sinata.appointment.mapper.MpVisitorMapper; import com.sinata.appointment.mapper.QrcodeTicketMapper; import com.sinata.appointment.mapper.SysDeptMapper; import com.sinata.appointment.util.OrderNoGenerator; import com.sinata.appointment.util.MobileMaskUtil; import com.sinata.appointment.util.SensitiveDataUtil; import com.sinata.appointment.util.UuidAssert; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; 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.util.StringUtils; import org.springframework.context.ApplicationEventPublisher; import com.sinata.appointment.service.event.AppointmentApprovedEvent; import com.sinata.appointment.service.event.AppointmentApproversNotifyEvent; @Slf4j @Service @RequiredArgsConstructor public class MiniAppointmentApplicationService { private static final String CANCEL_REASON_BY_SUBMITTER = "预约人取消"; private static final String DISPLAY_STATUS_PENDING_VISIT = "PENDING_VISIT"; private static final String DISPLAY_STATUS_EFFECTIVE = "EFFECTIVE"; private static final String DISPLAY_STATUS_INVALID = "INVALID"; private static final TypeReference> STRING_LIST_TYPE = new TypeReference<>() {}; private final MiniAuthApplicationService miniAuthApplicationService; private final MpUserMapper mpUserMapper; private final MpVisitorMapper mpVisitorMapper; private final BizVisitReasonMapper bizVisitReasonMapper; private final AppointmentOrderMapper appointmentOrderMapper; private final ApprovalTaskMapper approvalTaskMapper; private final ApprovalInstanceMapper approvalInstanceMapper; private final AppointmentVisitorMapper appointmentVisitorMapper; private final AppointmentVehicleMapper appointmentVehicleMapper; private final QrcodeTicketMapper qrcodeTicketMapper; private final BizFieldConfigMapper bizFieldConfigMapper; private final SysDeptMapper sysDeptMapper; private final BizHolidayDateCacheService bizHolidayDateCacheService; private final SysBizSettingCacheService sysBizSettingCacheService; private final ApprovalEngineService approvalEngineService; private final PostApprovalOrchestrationService postApprovalOrchestrationService; private final ApprovalFlowViewApplicationService approvalFlowViewApplicationService; private final SensitiveWordFilterService sensitiveWordFilterService; private final IntegrationHikvisionService integrationHikvisionService; private final ObjectMapper objectMapper; private final ApplicationEventPublisher applicationEventPublisher; @Transactional(rollbackFor = Exception.class) public MiniAppointmentCreateResultVo create(MiniAppointmentCreateRequest req) { MpUser current = miniAuthApplicationService.currentMpUser(); FieldRequiredContext required = loadFieldRequiredContext(); MpUser visited = loadVisitedUser(req.getVisitedUserId()); validateByFieldConfig(req, required); validateVisitReason(req.getVisitReasonCode(), req.getVisitReasonOther()); String mode = normalizeMode(req.getVisitTimeMode()); validateAgainstSystemRangeMode(mode); DateRange range = resolveDateRange(req, mode); boolean holidayFlag = calcHolidayFlag(range.start(), range.end()); boolean personEntry = req.getPersonEntry() == null || Boolean.TRUE.equals(req.getPersonEntry()); boolean vehicleEntry = Boolean.TRUE.equals(req.getVehicleEntry()); Set personIds = new LinkedHashSet<>(); if (personEntry && req.getVisitorIds() != null) { personIds.addAll(req.getVisitorIds().stream().filter(Objects::nonNull).toList()); } List vehicles = vehicleEntry ? normalizeVehicles(req.getVehicles()) : List.of(); for (MiniAppointmentVehicleItemRequest v : vehicles) { personIds.add(v.getDriverVisitorId()); } if (personIds.isEmpty()) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "至少选择一名进场人员"); } Map visitorMap = loadCurrentUserVisitors(current.getId(), personIds); validateVisitorDataByFieldConfig(visitorMap.values(), required); AppointmentOrder order = new AppointmentOrder(); order.setOrderNo(OrderNoGenerator.next()); order.setVisitType( vehicleEntry ? AppointmentBizConstants.VISIT_TYPE_VEHICLE : AppointmentBizConstants.VISIT_TYPE_VISITOR); order.setVisitedUserId(visited.getId()); order.setVisitedUserName(StringUtils.hasText(visited.getNickName()) ? visited.getNickName() : "员工"); order.setVisitedDeptId(visited.getDeptId()); order.setVisitReasonCode(req.getVisitReasonCode().trim()); order.setVisitReasonOther(sensitiveWordFilterService.filter(trimToNull(req.getVisitReasonOther()))); order.setVisitorOrg(resolveVisitorOrg(visitorMap.values())); order.setVisitTimeMode(mode); order.setVisitTimeSlot( AppointmentBizConstants.TIME_MODE_SINGLE.equals(mode) ? normalizeSlot(req.getVisitTimeSlot()) : null); order.setVisitStartDate(range.start()); order.setVisitEndDate(range.end()); order.setPersonEntryFlag(personEntry); order.setVehicleEntryFlag(vehicleEntry); order.setSubmitterId(current.getId()); order.setSubmitterVisibleFlag(1); order.setHolidayFlag(holidayFlag); order.setStatus(AppointmentOrderStatus.PENDING_APPROVAL); appointmentOrderMapper.insert(order); for (Long visitorId : personIds) { MpVisitor v = visitorMap.get(visitorId); AppointmentVisitor av = new AppointmentVisitor(); av.setOrderId(order.getId()); av.setMpVisitorId(v.getId()); av.setName(v.getVisitorName()); av.setIdCardCipher(v.getIdCardCipher()); av.setMobile(v.getVisitorMobile()); appointmentVisitorMapper.insert(av); } for (MiniAppointmentVehicleItemRequest v : vehicles) { MpVisitor driver = visitorMap.get(v.getDriverVisitorId()); AppointmentVehicle row = new AppointmentVehicle(); row.setOrderId(order.getId()); row.setPlateNo(v.getPlateNo().trim()); row.setDriverVisitorId(driver.getId()); row.setDriverName(driver.getVisitorName()); appointmentVehicleMapper.insert(row); } List chain = approvalEngineService.resolveEnabledNodeChain(order); if (chain.isEmpty()) { applicationEventPublisher.publishEvent(new AppointmentApprovedEvent(order.getId())); } else { ApprovalInstance inst = approvalEngineService.startFlow(order); if (inst == null) { applicationEventPublisher.publishEvent(new AppointmentApprovedEvent(order.getId())); } else { applicationEventPublisher.publishEvent( new AppointmentApproversNotifyEvent(order.getId(), inst.getId())); } } MiniAppointmentCreateResultVo result = new MiniAppointmentCreateResultVo(); result.setId(order.getId()); result.setUuid(order.getUuid()); return result; } @Transactional(rollbackFor = Exception.class) public void cancelBySubmitter(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 (!isCancelableStatus(order.getStatus())) { throw new BizException(BizErrorCode.ORDER_STATE_INVALID, "当前状态不可取消"); } ApprovalInstance latestInst = loadLatestInstanceByOrderId(order.getId()); if (latestInst != null && ApprovalInstanceStatus.IN_PROGRESS.equals(latestInst.getStatus())) { 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 pageMine( long page, long size, String status, String visitedUserNameKeyword, LocalDate visitDate) { MpUser current = miniAuthApplicationService.currentMpUser(); Page rowPage = new Page<>(Math.max(1L, page), Math.max(1L, size)); IPage pagedRows = approvalTaskMapper.pageMiniApprovalHistory( rowPage, current.getId(), trimToNull(status), trimToNull(visitedUserNameKeyword), visitDate); Page result = new Page<>(pagedRows.getCurrent(), pagedRows.getSize(), pagedRows.getTotal()); List rows = pagedRows.getRecords(); if (rows == null || rows.isEmpty()) { result.setRecords(List.of()); return result; } List orders = rows.stream().map(this::toOrderSnapshot).toList(); Map deptNameMap = loadDeptNameMap(orders); Map reasonNameMap = loadReasonNameMap(orders); List orderIds = orders.stream().map(AppointmentOrder::getId).filter(Objects::nonNull).toList(); Map visitorDisplayMap = loadVisitorDisplayMapByOrderIds(orderIds); Map plateTextMap = loadPlateTextMapByOrderIds(orderIds); List records = new ArrayList<>(); for (MiniApprovalHistoryRow row : rows) { AppointmentOrder order = toOrderSnapshot(row); MiniAppointmentListVo vo = toListVo(order); vo.setStatus(row.getTaskStatus()); vo.setVisitedDeptName(deptNameMap.get(order.getVisitedDeptId())); vo.setVisitedUserDisplay(buildVisitedUserDisplay(order.getVisitedUserName(), vo.getVisitedDeptName())); vo.setVisitTimeText(buildVisitTimeText(order)); vo.setVisitReasonText(buildVisitReasonText(order, reasonNameMap)); vo.setAccessAreaText(buildAccessAreaText(order.getPassAccessAreaNames())); vo.setVisitorDisplayText(visitorDisplayMap.get(order.getId())); vo.setPlateNoText(plateTextMap.get(order.getId())); records.add(vo); } result.setRecords(records); return result; } public IPage pageVisitRecords( long page, long size, boolean futureOnly, String visitorNameKeyword, LocalDate visitDate) { MpUser current = miniAuthApplicationService.currentMpUser(); Page orderPage = new Page<>(Math.max(1L, page), Math.max(1L, size)); IPage pagedOrders = appointmentOrderMapper.pageMiniVisitRecords( orderPage, current.getId(), futureOnly, trimToNull(visitorNameKeyword), visitDate); Page result = new Page<>(pagedOrders.getCurrent(), pagedOrders.getSize(), pagedOrders.getTotal()); List orders = pagedOrders.getRecords(); if (orders == null || orders.isEmpty()) { result.setRecords(List.of()); return result; } Map reasonNameMap = loadReasonNameMap(orders); List orderIds = orders.stream().map(AppointmentOrder::getId).filter(Objects::nonNull).toList(); Map> visitorNamesMap = loadVisitorNamesMap(orders.stream().map(AppointmentOrder::getId).toList()); Map visitorDisplayMap = loadVisitorDisplayMapByOrderIds(orderIds); Map plateTextMap = loadPlateTextMapByOrderIds(orderIds); List records = new ArrayList<>(); for (AppointmentOrder order : orders) { MiniVisitRecordListVo vo = new MiniVisitRecordListVo(); vo.setOrderId(order.getId()); vo.setOrderNo(order.getOrderNo()); vo.setVisitorNames(visitorNamesMap.getOrDefault(order.getId(), List.of())); vo.setVisitorDisplayText(visitorDisplayMap.get(order.getId())); vo.setPlateNoText(plateTextMap.get(order.getId())); vo.setVisitReasonText(buildVisitReasonText(order, reasonNameMap)); vo.setVisitStartDate(order.getVisitStartDate()); vo.setVisitEndDate(order.getVisitEndDate()); vo.setVisitTimeMode(order.getVisitTimeMode()); vo.setVisitTimeSlot(order.getVisitTimeSlot()); vo.setCreateTime(order.getCreateTime()); records.add(vo); } result.setRecords(records); return result; } public IPage pageMyOrders( long page, long size, String status, String keyword) { MpUser current = miniAuthApplicationService.currentMpUser(); Page orderPage = new Page<>(Math.max(1L, page), Math.max(1L, size)); IPage pagedOrders = appointmentOrderMapper.pageMiniMyOrders( orderPage, current.getId(), trimToNull(status), trimToNull(keyword)); Page result = new Page<>(pagedOrders.getCurrent(), pagedOrders.getSize(), pagedOrders.getTotal()); List orders = pagedOrders.getRecords(); if (orders == null || orders.isEmpty()) { result.setRecords(List.of()); return result; } Map deptNameMap = loadDeptNameMap(orders); Map reasonNameMap = loadReasonNameMap(orders); List records = new ArrayList<>(); for (AppointmentOrder order : orders) { records.add(toMyOrderListVo(order, deptNameMap, reasonNameMap)); } result.setRecords(records); return result; } public List listMyVisitorCodes() { MpUser current = miniAuthApplicationService.currentMpUser(); List orders = appointmentOrderMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentOrder::getDelFlag, 0) .eq(AppointmentOrder::getSubmitterId, current.getId()) .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 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); } public MiniMyVisitorCodeOrderVo getSharedVisitorCodeByTicketUuid(String ticketUuid) { QrcodeTicket ticket = loadTicketByUuid(ticketUuid); AppointmentOrder order = loadApprovedOrderById(ticket.getOrderId()); String deptName = loadDeptName(order.getVisitedDeptId()); MiniMyVisitorCodeOrderVo vo = new MiniMyVisitorCodeOrderVo(); vo.setOrderId(order.getId()); vo.setOrderUuid(order.getUuid()); vo.setVisitedUserDisplay(buildVisitedUserDisplay(order.getVisitedUserName(), deptName)); vo.setAccessAreaText(buildAccessAreaText(order.getPassAccessAreaNames())); vo.setVisitTimeText(buildVisitTimeText(order)); vo.setValidStartTime(ticket.getGenerateTime()); vo.setValidEndTime(ticket.getExpireTime()); if (!QrcodeTicketStatus.UNUSED.equals(ticket.getTicketStatus())) { vo.setQrCodes(List.of()); return vo; } Map visitorMap = loadMpVisitorMap( ticket.getMpVisitorId() == null ? Set.of() : Set.of(ticket.getMpVisitorId())); List orderVisitors = loadOrderVisitorRows(List.of(order.getId())).getOrDefault(order.getId(), List.of()); vo.setQrCodes(List.of(toMyVisitorCodeTicketVo(ticket, orderVisitors, visitorMap))); return vo; } private List buildVisitorCodeOrders( List orders, boolean futureOnly) { if (orders.isEmpty()) { return List.of(); } List eligibleOrders = orders; if (futureOnly) { LocalDateTime now = LocalDateTime.now(); eligibleOrders = orders.stream() .filter(order -> resolveVisitStartAt(order).isAfter(now)) .toList(); } if (eligibleOrders.isEmpty()) { return List.of(); } List orderIds = eligibleOrders.stream().map(AppointmentOrder::getId).toList(); List tickets = qrcodeTicketMapper.selectList( new LambdaQueryWrapper() .eq(QrcodeTicket::getDelFlag, 0) .in(QrcodeTicket::getOrderId, orderIds) .eq(QrcodeTicket::getTicketStatus, QrcodeTicketStatus.UNUSED) .orderByAsc(QrcodeTicket::getOrderId) .orderByAsc(QrcodeTicket::getId)); if (tickets.isEmpty()) { return List.of(); } Map> ticketMap = tickets.stream().collect(Collectors.groupingBy(QrcodeTicket::getOrderId, LinkedHashMap::new, Collectors.toList())); Set visitorIds = tickets.stream() .map(QrcodeTicket::getMpVisitorId) .filter(Objects::nonNull) .collect(Collectors.toCollection(LinkedHashSet::new)); Map visitorMap = loadMpVisitorMap(visitorIds); Map> orderVisitorRows = loadOrderVisitorRows(orderIds); Map deptNameMap = loadDeptNameMap(eligibleOrders); List result = new ArrayList<>(); for (AppointmentOrder order : eligibleOrders) { List orderTickets = ticketMap.get(order.getId()); if (orderTickets == null || orderTickets.isEmpty()) { continue; } MiniMyVisitorCodeOrderVo vo = new MiniMyVisitorCodeOrderVo(); vo.setOrderId(order.getId()); vo.setOrderUuid(order.getUuid()); String deptName = deptNameMap.get(order.getVisitedDeptId()); vo.setVisitedUserDisplay(buildVisitedUserDisplay(order.getVisitedUserName(), deptName)); vo.setAccessAreaText(buildAccessAreaText(order.getPassAccessAreaNames())); vo.setVisitTimeText(buildVisitTimeText(order)); vo.setValidStartTime(orderTickets.get(0).getGenerateTime()); vo.setValidEndTime(orderTickets.get(0).getExpireTime()); vo.setQrCodes( orderTickets.stream() .map(ticket -> toMyVisitorCodeTicketVo(ticket, orderVisitorRows.get(order.getId()), visitorMap)) .toList()); result.add(vo); } 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() .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; } private AppointmentOrder loadApprovedOrderById(Long orderId) { if (orderId == null || orderId <= 0) { throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "预约单不存在"); } AppointmentOrder order = appointmentOrderMapper.selectOne( new LambdaQueryWrapper() .eq(AppointmentOrder::getDelFlag, 0) .eq(AppointmentOrder::getId, orderId) .eq(AppointmentOrder::getStatus, AppointmentOrderStatus.APPROVED) .last("limit 1")); if (order == null) { throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "预约单不存在"); } return order; } private QrcodeTicket loadTicketByUuid(String ticketUuid) { String normalizedTicketUuid = trimToNull(ticketUuid); if (!StringUtils.hasText(normalizedTicketUuid)) { throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "访客码不存在"); } QrcodeTicket ticket = qrcodeTicketMapper.selectOne( new LambdaQueryWrapper() .eq(QrcodeTicket::getDelFlag, 0) .eq(QrcodeTicket::getUuid, normalizedTicketUuid) .last("limit 1")); if (ticket == null) { throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "访客码不存在"); } return ticket; } 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.selectMiniVisibleOrderById(orderId, current.getId()); if (order == null) { throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "预约单不存在"); } UuidAssert.match(order, uuid); 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(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.setVisitedUserDisplay(buildVisitedUserDisplay(order.getVisitedUserName(), 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 flowTree = approvalFlowViewApplicationService.treeByOrderId(order.getId()); vo.setFlowTree(flowTree == null ? List.of() : flowTree); return vo; } private MpUser loadVisitedUser(Long visitedUserId) { MpUser visited = mpUserMapper.selectById(visitedUserId); if (visited == null || (visited.getDelFlag() != null && visited.getDelFlag() == 1)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "被访人不存在"); } if (visited.getStatus() != null && visited.getStatus() == 0) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "被访人已禁用"); } if (visited.getDeptId() == null || visited.getDeptId() <= 0) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "被访人未绑定部门"); } return visited; } private void validateVisitReason(String code, String other) { String c = trimToNull(code); if (!StringUtils.hasText(c)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "拜访事由不能为空"); } String txt = trimToNull(other); if (!StringUtils.hasText(txt)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "请输入事由说明"); } if (txt.length() > 15) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "事由说明不能超过15个字"); } if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(c)) { return; } Long cnt = bizVisitReasonMapper.selectCount( new LambdaQueryWrapper() .eq(BizVisitReason::getDelFlag, 0) .eq(BizVisitReason::getEnabled, 1) .eq(BizVisitReason::getReasonCode, c)); if (cnt == null || cnt <= 0) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "拜访事由无效"); } } private String normalizeMode(String mode) { String m = trimToNull(mode); if (!StringUtils.hasText(m)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "拜访时间类型不能为空"); } m = m.toUpperCase(); if (!AppointmentBizConstants.TIME_MODE_SINGLE.equals(m) && !AppointmentBizConstants.TIME_MODE_MULTI.equals(m)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "拜访时间类型仅支持 SINGLE/MULTI"); } return m; } private void validateAgainstSystemRangeMode(String reqMode) { String cfg = trimToNull(sysBizSettingCacheService.getValue(SystemSettingKeys.BOOKING_RANGE_MODE)); String mode = StringUtils.hasText(cfg) ? cfg.toUpperCase() : AppointmentBizConstants.TIME_MODE_SINGLE; if (!AppointmentBizConstants.TIME_MODE_SINGLE.equals(mode) && !AppointmentBizConstants.TIME_MODE_MULTI.equals(mode)) { mode = AppointmentBizConstants.TIME_MODE_SINGLE; } if (AppointmentBizConstants.TIME_MODE_SINGLE.equals(mode) && AppointmentBizConstants.TIME_MODE_MULTI.equals(reqMode)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "当前预约配置不支持该拜访时间类型"); } } private DateRange resolveDateRange(MiniAppointmentCreateRequest req, String mode) { if (AppointmentBizConstants.TIME_MODE_SINGLE.equals(mode)) { if (req.getVisitDate() == null) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "单日模式下拜访日期不能为空"); } normalizeSlot(req.getVisitTimeSlot()); return new DateRange(req.getVisitDate(), req.getVisitDate()); } if (req.getVisitStartDate() == null || req.getVisitEndDate() == null) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "多日模式下须填写起止日期"); } if (req.getVisitEndDate().isBefore(req.getVisitStartDate())) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "结束日期不能早于开始日期"); } long days = ChronoUnit.DAYS.between(req.getVisitStartDate(), req.getVisitEndDate()) + 1; int maxDays = getMaxConsecutiveDays(); if (days > maxDays) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "多日预约不能超过最大天数: " + maxDays); } return new DateRange(req.getVisitStartDate(), req.getVisitEndDate()); } private int getMaxConsecutiveDays() { String raw = trimToNull(sysBizSettingCacheService.getValue(SystemSettingKeys.BOOKING_MAX_CONSECUTIVE_DAYS)); if (!StringUtils.hasText(raw)) { return 7; } try { return Math.max(1, Integer.parseInt(raw)); } catch (NumberFormatException ex) { return 7; } } private String normalizeSlot(String slot) { String s = trimToNull(slot); if (!StringUtils.hasText(s)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "单日模式下须选择全天/上午/下午"); } s = s.toUpperCase(); if (!AppointmentBizConstants.TIME_SLOT_ALL_DAY.equals(s) && !AppointmentBizConstants.TIME_SLOT_AM.equals(s) && !AppointmentBizConstants.TIME_SLOT_PM.equals(s)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "单日时段仅支持 ALL_DAY/AM/PM"); } return s; } private List normalizeVehicles(List raw) { if (raw == null || raw.isEmpty()) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "勾选车辆进场后,进场车辆列表不能为空"); } List out = new ArrayList<>(); for (MiniAppointmentVehicleItemRequest v : raw) { if (v == null || !StringUtils.hasText(v.getPlateNo()) || v.getDriverVisitorId() == null) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "车辆信息不完整"); } if (v.getPlateNo().trim().length() > 32) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "车牌号长度不能超过32"); } out.add(v); } return out; } private Map loadCurrentUserVisitors(Long currentMpUserId, Set ids) { List rows = mpVisitorMapper.selectList( new LambdaQueryWrapper() .eq(MpVisitor::getMpUserId, currentMpUserId) .eq(MpVisitor::getDelFlag, 0) .in(MpVisitor::getId, ids)); Map map = rows.stream().collect(Collectors.toMap(MpVisitor::getId, Function.identity(), (a, b) -> a)); for (Long id : ids) { if (!map.containsKey(id)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "来访人员不存在或不属于当前账号: " + id); } } return map; } private void validateByFieldConfig(MiniAppointmentCreateRequest req, FieldRequiredContext required) { boolean personEntry = req.getPersonEntry() == null || Boolean.TRUE.equals(req.getPersonEntry()); boolean vehicleEntry = Boolean.TRUE.equals(req.getVehicleEntry()); if (required.isRequired(AppointmentFieldCodes.RESPONDENT) && req.getVisitedUserId() == null) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "被访人不能为空"); } if (required.isRequired(AppointmentFieldCodes.VISIT_REASON) && !StringUtils.hasText(trimToNull(req.getVisitReasonCode()))) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "拜访事由不能为空"); } if (required.isRequired(AppointmentFieldCodes.VISIT_START_TIME)) { if (AppointmentBizConstants.TIME_MODE_SINGLE.equalsIgnoreCase(trimToNull(req.getVisitTimeMode()))) { if (req.getVisitDate() == null) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "拜访时间不能为空"); } } else if (req.getVisitStartDate() == null) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "开始拜访时间不能为空"); } } if (required.isRequired(AppointmentFieldCodes.VISIT_END_TIME) && AppointmentBizConstants.TIME_MODE_MULTI.equalsIgnoreCase(trimToNull(req.getVisitTimeMode())) && req.getVisitEndDate() == null) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "结束拜访时间不能为空"); } if (required.isRequired(AppointmentFieldCodes.VISIT_TYPE) && !personEntry && !vehicleEntry) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "来访类型不能为空"); } if (required.isRequired(AppointmentFieldCodes.LICENSE_PLATE, AppointmentFieldCodes.DRIVER) && vehicleEntry && (req.getVehicles() == null || req.getVehicles().isEmpty())) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "车辆信息不能为空"); } } private void validateVisitorDataByFieldConfig( java.util.Collection visitors, FieldRequiredContext required) { for (MpVisitor v : visitors) { if (required.isRequired(AppointmentFieldCodes.VISITOR_NAME) && !StringUtils.hasText(trimToNull(v.getVisitorName()))) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "访客姓名不能为空"); } if (required.isRequired(AppointmentFieldCodes.ID_CARD) && !StringUtils.hasText(trimToNull(v.getIdCardCipher()))) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "身份证号码不能为空"); } if (required.isRequired(AppointmentFieldCodes.GENDER) && !StringUtils.hasText(trimToNull(v.getGender()))) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "性别不能为空"); } if (required.isRequired(AppointmentFieldCodes.COMPANY) && !StringUtils.hasText(trimToNull(v.getVisitorCompany()))) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "来访单位不能为空"); } if (required.isRequired(AppointmentFieldCodes.MOBILE) && !StringUtils.hasText(trimToNull(v.getVisitorMobile()))) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "联系电话不能为空"); } } } private FieldRequiredContext loadFieldRequiredContext() { List rows = bizFieldConfigMapper.selectList( new LambdaQueryWrapper() .eq(BizFieldConfig::getDelFlag, 0) .eq(BizFieldConfig::getVisible, 1) .eq(BizFieldConfig::getRequiredFlag, 1)); Set required = rows.stream() .map(BizFieldConfig::getFieldCode) .filter(StringUtils::hasText) .map(String::trim) .collect(Collectors.toCollection(java.util.LinkedHashSet::new)); return new FieldRequiredContext(required); } private String resolveVisitorOrg(java.util.Collection visitors) { for (MpVisitor v : visitors) { String company = trimToNull(v.getVisitorCompany()); if (StringUtils.hasText(company)) { return company; } } return null; } private boolean calcHolidayFlag(LocalDate start, LocalDate end) { Set holidays = bizHolidayDateCacheService.getAllEnabledHolidayDates(); if (holidays.isEmpty()) { return false; } LocalDate d = start; while (!d.isAfter(end)) { if (holidays.contains(d)) { return true; } d = d.plusDays(1); } return false; } private static String trimToNull(String s) { if (s == null) { return null; } String t = s.trim(); return t.isEmpty() ? null : t; } private MiniAppointmentListVo toListVo(AppointmentOrder row) { MiniAppointmentListVo vo = new MiniAppointmentListVo(); vo.setId(row.getId()); vo.setUuid(row.getUuid()); vo.setOrderNo(row.getOrderNo()); vo.setStatus(row.getStatus()); vo.setVisitedUserName(row.getVisitedUserName()); vo.setVisitStartDate(row.getVisitStartDate()); vo.setVisitEndDate(row.getVisitEndDate()); vo.setVisitReasonCode(row.getVisitReasonCode()); vo.setVisitReasonOther(row.getVisitReasonOther()); vo.setCreateTime(row.getCreateTime()); return vo; } private MiniAppointmentListVo toMyOrderListVo( AppointmentOrder row, Map deptNameMap, Map reasonNameMap) { MiniAppointmentListVo vo = toListVo(row); String deptName = deptNameMap.get(row.getVisitedDeptId()); vo.setVisitedDeptName(deptName); vo.setVisitedUserDisplay(buildVisitedUserDisplay(row.getVisitedUserName(), deptName)); vo.setDisplayStatus(resolveDisplayStatus(row)); vo.setVisitTimeText(buildVisitTimeText(row)); vo.setVisitReasonText(buildVisitReasonText(row, reasonNameMap)); vo.setAccessAreaText(buildAccessAreaText(row.getPassAccessAreaNames())); return vo; } private AppointmentOrder toOrderSnapshot(MiniApprovalHistoryRow row) { AppointmentOrder order = new AppointmentOrder(); order.setId(row.getId()); order.setUuid(row.getUuid()); order.setOrderNo(row.getOrderNo()); order.setVisitedUserName(row.getVisitedUserName()); order.setVisitedDeptId(row.getVisitedDeptId()); order.setVisitStartDate(row.getVisitStartDate()); order.setVisitEndDate(row.getVisitEndDate()); order.setVisitReasonCode(row.getVisitReasonCode()); order.setVisitReasonOther(row.getVisitReasonOther()); order.setVisitTimeMode(row.getVisitTimeMode()); order.setVisitTimeSlot(row.getVisitTimeSlot()); order.setPassAccessAreaNames(row.getPassAccessAreaNames()); order.setStatus(row.getStatus()); order.setCreateTime(row.getCreateTime()); return order; } private Map loadDeptNameMap(List orders) { Set deptIds = orders.stream() .map(AppointmentOrder::getVisitedDeptId) .filter(Objects::nonNull) .collect(Collectors.toCollection(LinkedHashSet::new)); if (deptIds.isEmpty()) { return Map.of(); } List rows = sysDeptMapper.selectList( new LambdaQueryWrapper() .eq(SysDept::getDelFlag, 0) .in(SysDept::getId, deptIds)); return rows.stream() .collect(Collectors.toMap(SysDept::getId, SysDept::getDeptName, (a, b) -> a, LinkedHashMap::new)); } private Map loadReasonNameMap(List orders) { Set codes = orders.stream() .map(AppointmentOrder::getVisitReasonCode) .filter(StringUtils::hasText) .map(String::trim) .filter(code -> !AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(code)) .collect(Collectors.toCollection(LinkedHashSet::new)); if (codes.isEmpty()) { return Map.of(); } List rows = bizVisitReasonMapper.selectList( new LambdaQueryWrapper() .eq(BizVisitReason::getDelFlag, 0) .in(BizVisitReason::getReasonCode, codes)); return rows.stream() .collect( Collectors.toMap( BizVisitReason::getReasonCode, BizVisitReason::getReasonName, (a, b) -> a, LinkedHashMap::new)); } private Map> loadVisitorNamesMap(List orderIds) { if (orderIds == null || orderIds.isEmpty()) { return Map.of(); } List visitors = appointmentVisitorMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentVisitor::getDelFlag, 0) .in(AppointmentVisitor::getOrderId, orderIds) .orderByAsc(AppointmentVisitor::getId)); Map> result = new LinkedHashMap<>(); for (AppointmentVisitor row : visitors) { result.computeIfAbsent(row.getOrderId(), k -> new ArrayList<>()); if (StringUtils.hasText(row.getName())) { result.get(row.getOrderId()).add(row.getName().trim()); } } return result; } private String buildVisitedUserDisplay(String userName, String deptName) { String name = trimToNull(userName); String dept = trimToNull(deptName); if (StringUtils.hasText(name) && StringUtils.hasText(dept)) { return name + " " + dept; } if (StringUtils.hasText(name)) { return name; } return StringUtils.hasText(dept) ? dept : "-"; } private Map loadVisitorDisplayMapByOrderIds(List orderIds) { if (orderIds == null || orderIds.isEmpty()) { return Map.of(); } List rows = appointmentVisitorMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentVisitor::getDelFlag, 0) .in(AppointmentVisitor::getOrderId, orderIds) .orderByAsc(AppointmentVisitor::getId)); Set visitorIds = rows.stream() .map(AppointmentVisitor::getMpVisitorId) .filter(Objects::nonNull) .collect(Collectors.toCollection(LinkedHashSet::new)); Map companyMap = loadMpVisitorCompanyMap(visitorIds); Map> 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 result = new LinkedHashMap<>(); for (Map.Entry> entry : grouped.entrySet()) { String text = entry.getValue().stream().distinct().collect(Collectors.joining("、")); if (StringUtils.hasText(text)) { result.put(entry.getKey(), text); } } return result; } private Map loadMpVisitorCompanyMap(Set visitorIds) { if (visitorIds == null || visitorIds.isEmpty()) { return Map.of(); } List rows = mpVisitorMapper.selectList( new LambdaQueryWrapper() .eq(MpVisitor::getDelFlag, 0) .in(MpVisitor::getId, visitorIds)); Map result = new LinkedHashMap<>(); for (MpVisitor row : rows) { result.put(row.getId(), trimToNull(row.getVisitorCompany())); } return result; } private Map loadPlateTextMapByOrderIds(List orderIds) { if (orderIds == null || orderIds.isEmpty()) { return Map.of(); } List rows = appointmentVehicleMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentVehicle::getDelFlag, 0) .in(AppointmentVehicle::getOrderId, orderIds) .orderByAsc(AppointmentVehicle::getId)); Map> 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 result = new LinkedHashMap<>(); for (Map.Entry> entry : grouped.entrySet()) { String text = entry.getValue().stream().distinct().collect(Collectors.joining("、")); if (StringUtils.hasText(text)) { result.put(entry.getKey(), text); } } return result; } private String resolveOrderStatusText(AppointmentOrder row) { return switch (trimToNull(row.getStatus())) { case AppointmentOrderStatus.PENDING_APPROVAL -> "审批中"; case AppointmentOrderStatus.APPROVED, AppointmentOrderStatus.VISITED, AppointmentOrderStatus.EXPIRED -> "审批通过"; case AppointmentOrderStatus.REJECTED -> "审批驳回"; case AppointmentOrderStatus.CANCELLED -> "已取消"; default -> "已失效"; }; } private String resolveDisplayStatus(AppointmentOrder row) { LocalDate today = LocalDate.now(); if (AppointmentOrderStatus.PENDING_APPROVAL.equals(row.getStatus())) { return DISPLAY_STATUS_PENDING_VISIT; } if (AppointmentOrderStatus.APPROVED.equals(row.getStatus())) { if (row.getVisitStartDate() != null && today.isBefore(row.getVisitStartDate())) { return DISPLAY_STATUS_PENDING_VISIT; } if (row.getVisitStartDate() != null && row.getVisitEndDate() != null && (today.isEqual(row.getVisitStartDate()) || today.isAfter(row.getVisitStartDate())) && (today.isEqual(row.getVisitEndDate()) || today.isBefore(row.getVisitEndDate()))) { return DISPLAY_STATUS_EFFECTIVE; } } 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())) { case AppointmentBizConstants.TIME_SLOT_ALL_DAY -> "00:00-23:59"; case AppointmentBizConstants.TIME_SLOT_AM -> "00:00-11:59"; case AppointmentBizConstants.TIME_SLOT_PM -> "12:00-23:59"; default -> ""; }; if (row.getVisitStartDate() == null) { return slotLabel; } return StringUtils.hasText(slotLabel) ? row.getVisitStartDate() + " " + slotLabel : row.getVisitStartDate().toString(); } if (row.getVisitStartDate() == null && row.getVisitEndDate() == null) { return "-"; } if (Objects.equals(row.getVisitStartDate(), row.getVisitEndDate()) || row.getVisitEndDate() == null) { return String.valueOf(row.getVisitStartDate()); } return row.getVisitStartDate() + " 至 " + row.getVisitEndDate(); } private String buildVisitReasonText(AppointmentOrder row, Map reasonNameMap) { String reasonName = reasonNameMap.get(trimToNull(row.getVisitReasonCode())); return StringUtils.hasText(reasonName) ? reasonName : trimToNull(row.getVisitReasonCode()); } private String buildAccessAreaText(String rawJson) { String raw = trimToNull(rawJson); if (!StringUtils.hasText(raw)) { return null; } try { List rows = objectMapper.readValue(raw, STRING_LIST_TYPE); String joined = rows.stream() .filter(StringUtils::hasText) .map(String::trim) .collect(Collectors.joining("、")); return StringUtils.hasText(joined) ? joined : null; } catch (Exception ignore) { return raw; } } private LocalDateTime resolveVisitStartAt(AppointmentOrder row) { 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.MAX); } return switch (trimToNull(row.getVisitTimeSlot())) { case AppointmentBizConstants.TIME_SLOT_AM -> LocalDateTime.of(date, LocalTime.of(12, 0)); default -> LocalDateTime.of(date, LocalTime.MAX); }; } private Map loadMpVisitorMap(Set visitorIds) { if (visitorIds == null || visitorIds.isEmpty()) { return Map.of(); } List rows = mpVisitorMapper.selectList( new LambdaQueryWrapper() .eq(MpVisitor::getDelFlag, 0) .in(MpVisitor::getId, visitorIds)); return rows.stream().collect(Collectors.toMap(MpVisitor::getId, Function.identity(), (a, b) -> a, LinkedHashMap::new)); } private Map> loadOrderVisitorRows(List orderIds) { if (orderIds == null || orderIds.isEmpty()) { return Map.of(); } List rows = appointmentVisitorMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentVisitor::getDelFlag, 0) .in(AppointmentVisitor::getOrderId, orderIds) .orderByAsc(AppointmentVisitor::getId)); return rows.stream() .collect(Collectors.groupingBy(AppointmentVisitor::getOrderId, LinkedHashMap::new, Collectors.toList())); } private MiniMyVisitorCodeTicketVo toMyVisitorCodeTicketVo( QrcodeTicket ticket, List orderVisitors, Map visitorMap) { MiniMyVisitorCodeTicketVo vo = new MiniMyVisitorCodeTicketVo(); vo.setTicketId(ticket.getId()); vo.setTicketUuid(ticket.getUuid()); vo.setMpVisitorId(ticket.getMpVisitorId()); vo.setPayload(integrationHikvisionService.encryptQrcodePayload(ticket.getPayload())); vo.setGenerateTime(ticket.getGenerateTime()); vo.setExpireTime(ticket.getExpireTime()); vo.setVisitorName(resolveVisitorName(ticket.getMpVisitorId(), orderVisitors, visitorMap)); return vo; } private String resolveVisitorName( Long mpVisitorId, List orderVisitors, Map visitorMap) { if (orderVisitors != null && !orderVisitors.isEmpty()) { for (AppointmentVisitor row : orderVisitors) { if (Objects.equals(mpVisitorId, row.getMpVisitorId()) && StringUtils.hasText(row.getName())) { return row.getName().trim(); } } } MpVisitor visitor = mpVisitorId == null ? null : visitorMap.get(mpVisitorId); if (visitor != null && StringUtils.hasText(visitor.getVisitorName())) { return visitor.getVisitorName().trim(); } return "访客"; } private String buildVisitTypeText(AppointmentOrder order) { boolean personEntry = order.getPersonEntryFlag() == null || Boolean.TRUE.equals(order.getPersonEntryFlag()); boolean vehicleEntry = Boolean.TRUE.equals(order.getVehicleEntryFlag()); if (personEntry && vehicleEntry) { return "访客+车辆预约"; } if (vehicleEntry) { return "车辆预约"; } return "访客预约"; } private List loadOrderVisitors(Long orderId) { List rows = appointmentVisitorMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentVisitor::getDelFlag, 0) .eq(AppointmentVisitor::getOrderId, orderId) .orderByAsc(AppointmentVisitor::getId)); if (rows.isEmpty()) { return List.of(); } List visitorIds = rows.stream() .map(AppointmentVisitor::getMpVisitorId) .filter(Objects::nonNull) .toList(); Map visitorMap = loadVisitorMap(visitorIds); Map plateMap = loadVisitorPlateMap(orderId); List result = new ArrayList<>(); for (AppointmentVisitor row : rows) { MpVisitor visitor = visitorMap.get(row.getMpVisitorId()); MiniApprovalDetailVisitorVo vo = new MiniApprovalDetailVisitorVo(); vo.setVisitorId(row.getMpVisitorId()); vo.setName(firstNonBlank(row.getName(), visitor == null ? null : visitor.getVisitorName(), "-")); vo.setGender(visitor == null ? null : visitor.getGender()); vo.setCompanyName(visitor == null ? null : visitor.getVisitorCompany()); vo.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); vo.setIdCardMasked( StringUtils.hasText(idCardPlain) ? SensitiveDataUtil.maskIdCard(idCardPlain) : null); vo.setPlateNo(plateMap.get(row.getMpVisitorId())); result.add(vo); } return result; } private List loadRebookVisitors(Long orderId) { List rows = appointmentVisitorMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentVisitor::getDelFlag, 0) .eq(AppointmentVisitor::getOrderId, orderId) .orderByAsc(AppointmentVisitor::getId)); if (rows.isEmpty()) { return List.of(); } List visitorIds = rows.stream() .map(AppointmentVisitor::getMpVisitorId) .filter(Objects::nonNull) .toList(); Map visitorMap = loadVisitorMap(visitorIds); List 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 loadRebookVehicles(Long orderId) { List rows = appointmentVehicleMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentVehicle::getDelFlag, 0) .eq(AppointmentVehicle::getOrderId, orderId) .orderByAsc(AppointmentVehicle::getId)); if (rows.isEmpty()) { return List.of(); } List 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 loadVisitorMap(List visitorIds) { if (visitorIds.isEmpty()) { return Map.of(); } List rows = mpVisitorMapper.selectList( new LambdaQueryWrapper() .eq(MpVisitor::getDelFlag, 0) .in(MpVisitor::getId, visitorIds)); return rows.stream().collect(Collectors.toMap(MpVisitor::getId, Function.identity(), (a, b) -> a)); } private Map loadVisitorPlateMap(Long orderId) { List rows = appointmentVehicleMapper.selectList( new LambdaQueryWrapper() .eq(AppointmentVehicle::getDelFlag, 0) .eq(AppointmentVehicle::getOrderId, orderId) .orderByAsc(AppointmentVehicle::getId)); if (rows.isEmpty()) { return Map.of(); } Map> plateMap = new LinkedHashMap<>(); for (AppointmentVehicle row : rows) { if (row.getDriverVisitorId() == null) { continue; } plateMap.computeIfAbsent(row.getDriverVisitorId(), key -> new ArrayList<>()); if (StringUtils.hasText(row.getPlateNo())) { plateMap.get(row.getDriverVisitorId()).add(row.getPlateNo().trim()); } } Map result = new LinkedHashMap<>(); for (Map.Entry> entry : plateMap.entrySet()) { result.put(entry.getKey(), String.join("、", entry.getValue())); } return result; } private String loadDeptName(Long deptId) { if (deptId == null || deptId <= 0) { return null; } SysDept dept = sysDeptMapper.selectById(deptId); return dept == null ? null : dept.getDeptName(); } private String loadReasonName(String reasonCode) { String code = trimToNull(reasonCode); if (!StringUtils.hasText(code) || AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(code)) { return null; } List rows = bizVisitReasonMapper.selectList( new LambdaQueryWrapper() .eq(BizVisitReason::getDelFlag, 0) .eq(BizVisitReason::getReasonCode, code) .last("limit 1")); if (rows.isEmpty()) { return null; } return trimToNull(rows.get(0).getReasonName()); } private String firstNonBlank(String... values) { if (values == null) { return null; } for (String value : values) { if (StringUtils.hasText(value)) { return value.trim(); } } return null; } private boolean containsIgnoreCase(String src, String keyword) { if (!StringUtils.hasText(src) || !StringUtils.hasText(keyword)) { return false; } return src.toLowerCase().contains(keyword.toLowerCase()); } private IPage slicePage(long page, long size, List rowsAll) { long total = rowsAll == null ? 0 : rowsAll.size(); long current = Math.max(1L, page); long pageSize = Math.max(1L, size); int from = (int) Math.min(total, (current - 1) * pageSize); int to = (int) Math.min(total, from + pageSize); List records = from >= to ? List.of() : rowsAll.subList(from, to); Page out = new Page<>(current, pageSize); out.setTotal(total); out.setRecords(records); return out; } private IPage slicePageVisitRecord( long page, long size, List rowsAll) { long total = rowsAll == null ? 0 : rowsAll.size(); long current = Math.max(1L, page); long pageSize = Math.max(1L, size); int from = (int) Math.min(total, (current - 1) * pageSize); int to = (int) Math.min(total, from + pageSize); List records = from >= to ? List.of() : rowsAll.subList(from, to); Page out = new Page<>(current, pageSize); out.setTotal(total); out.setRecords(records); return out; } private int slotSortWeight(String slot) { if (AppointmentBizConstants.TIME_SLOT_ALL_DAY.equals(slot)) { return 3; } if (AppointmentBizConstants.TIME_SLOT_PM.equals(slot)) { return 2; } if (AppointmentBizConstants.TIME_SLOT_AM.equals(slot)) { return 1; } return 0; } private boolean isCancelableStatus(String status) { if (!StringUtils.hasText(status)) { return false; } if (AppointmentOrderStatus.REJECTED.equals(status) || AppointmentOrderStatus.APPROVED.equals(status) || AppointmentOrderStatus.VISITED.equals(status) || AppointmentOrderStatus.EXPIRED.equals(status) || AppointmentOrderStatus.CANCELLED.equals(status)) { return false; } return true; } private ApprovalInstance loadLatestInstanceByOrderId(Long orderId) { List rows = approvalInstanceMapper.selectList( new LambdaQueryWrapper() .eq(ApprovalInstance::getDelFlag, 0) .eq(ApprovalInstance::getOrderId, orderId) .orderByDesc(ApprovalInstance::getId) .last("limit 1")); return rows.isEmpty() ? null : rows.get(0); } private void expireUnusedTicketsByOrderId(Long orderId) { List tickets = qrcodeTicketMapper.selectList( new LambdaQueryWrapper() .eq(QrcodeTicket::getDelFlag, 0) .eq(QrcodeTicket::getOrderId, orderId) .eq(QrcodeTicket::getTicketStatus, QrcodeTicketStatus.UNUSED)); for (QrcodeTicket ticket : tickets) { ticket.setTicketStatus(QrcodeTicketStatus.EXPIRED); qrcodeTicketMapper.updateById(ticket); } } private int singleDaySlotSortWeight(MiniVisitRecordListVo row) { if (!AppointmentBizConstants.TIME_MODE_SINGLE.equals(row.getVisitTimeMode())) { return 0; } return slotSortWeight(row.getVisitTimeSlot()); } private record FieldRequiredContext(Set requiredCodes) { private boolean isRequired(String... codes) { for (String c : codes) { if (requiredCodes.contains(c)) { return true; } } return false; } } private record DateRange(LocalDate start, LocalDate end) {} }