package jnpf.dmsService.search; import jnpf.dmsEntity.permission.enums.DmsPermissionAction; import jnpf.dmsEntity.permission.enums.DmsResourceType; import jnpf.dmsEntity.permission.form.PermissionDecisionForm; import jnpf.dmsEntity.permission.vo.DmsPermissionDecisionVO; import jnpf.dmsEntity.search.form.DmsFullTextSearchForm; import jnpf.dmsEntity.search.vo.DmsFullTextSearchItemVO; import jnpf.dmsEntity.search.vo.DmsFullTextSearchPageVO; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.dmsMapper.DmsSearchIndexSourceMapper; import jnpf.dmsPermission.DmsPermissionError; import jnpf.dmsPermission.DmsPermissionException; import jnpf.dmsService.DmsPermissionDecisionService; import jnpf.dmsService.search.meili.DmsMeilisearchClient; import jnpf.dmsService.search.meili.DmsMeilisearchSearchHit; import jnpf.dmsService.search.meili.DmsMeilisearchSearchPage; import jnpf.dmsService.search.meili.DmsSearchIndexDocument; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.UUID; @Service @RequiredArgsConstructor public class DmsFullTextSearchService { private static final int DEFAULT_PAGE_SIZE = 20; private static final int MAX_PAGE_SIZE = 100; private static final int CANDIDATE_BATCH_SIZE = 100; private static final int MAX_CANDIDATES_PER_REQUEST = 1_000; private final DmsCurrentIdentity currentIdentity; private final DmsPermissionDecisionService permissionDecisionService; private final DmsMeilisearchClient meilisearchClient; private final DmsSearchIndexSourceMapper sourceMapper; public DmsFullTextSearchPageVO search(DmsFullTextSearchForm form) throws IOException { String keyword = form.getKeyword().trim(); int pageSize = pageSize(form.getPageSize()); int candidateOffset = cursor(form.getCursor()); int scanned = 0; String tenantId = currentIdentity.tenantId(); List results = new ArrayList<>(); while (scanned < MAX_CANDIDATES_PER_REQUEST) { int batchSize = Math.min(CANDIDATE_BATCH_SIZE, MAX_CANDIDATES_PER_REQUEST - scanned); DmsMeilisearchSearchPage candidates = meilisearchClient.search( tenantId, keyword, candidateOffset, batchSize); if (candidates.getHits().isEmpty()) { break; } List allowed = allowed(candidates.getHits()); java.util.Set current = new java.util.HashSet<>(sourceMapper.currentVersionIds(tenantId, candidates.getHits().stream().map(hit -> hit.getDocument().getFileVersionId()).toList())); for (int index = 0; index < candidates.getHits().size(); index++) { int currentOffset = candidateOffset + index; if (!allowed.get(index) || !current.contains(candidates.getHits().get(index).getDocument().getFileVersionId())) { continue; } if (results.size() == pageSize) { return page(results, Integer.toString(currentOffset), true); } results.add(toItem(candidates.getHits().get(index))); } int consumed = candidates.getHits().size(); candidateOffset += consumed; scanned += consumed; if (candidateOffset >= candidates.getEstimatedTotalHits() || consumed < batchSize) { break; } } if (scanned >= MAX_CANDIDATES_PER_REQUEST) { return page(results, Integer.toString(candidateOffset), true); } return page(results, null, false); } private List allowed(List hits) { List requests = new ArrayList<>(); for (DmsMeilisearchSearchHit hit : hits) { PermissionDecisionForm request = new PermissionDecisionForm(); request.setResourceType(DmsResourceType.FILE_VERSION); request.setResourceId(UUID.fromString(hit.getDocument().getFileVersionId())); request.setActionCode(DmsPermissionAction.READ); requests.add(request); } try { List decisions = permissionDecisionService.evaluate(requests); List result = new ArrayList<>(); for (DmsPermissionDecisionVO decision : decisions) { result.add(decision.isAllowed()); } return result; } catch (RuntimeException batchFailure) { List result = new ArrayList<>(); for (PermissionDecisionForm request : requests) { try { result.add(permissionDecisionService.evaluate(request).isAllowed()); } catch (RuntimeException ignored) { result.add(false); } } return result; } } private DmsFullTextSearchItemVO toItem(DmsMeilisearchSearchHit hit) { DmsSearchIndexDocument source = hit.getDocument(); DmsFullTextSearchItemVO item = new DmsFullTextSearchItemVO(); item.setId(UUID.fromString(source.getFileVersionId())); item.setDocumentId(UUID.fromString(source.getDocumentId())); item.setFolderId(uuid(source.getFolderId())); item.setFolderPath(source.getFolderPath()); item.setDocumentNo(source.getDocumentNo()); item.setTitle(source.getTitle()); item.setHighlightedTitle(highlight(hit.getFormattedTitle())); item.setVersionLabel(source.getVersionLabel()); item.setFileName(source.getFileName()); item.setMimeType(source.getMimeType()); item.setLifecycleStatus(source.getLifecycleStatus()); item.setCreatedAt(source.getCreatedAt()); item.setHighlightedSnippet(highlight(hit.getFormattedContent())); return item; } private DmsFullTextSearchPageVO page(List list, String nextCursor, boolean hasMore) { DmsFullTextSearchPageVO page = new DmsFullTextSearchPageVO(); page.setList(list); page.setNextCursor(nextCursor); page.setHasMore(hasMore); return page; } private int pageSize(Integer requested) { int value = requested == null ? DEFAULT_PAGE_SIZE : requested; if (value < 1 || value > MAX_PAGE_SIZE) { throw invalid("pageSize 必须在 1 到 " + MAX_PAGE_SIZE + " 之间"); } return value; } private int cursor(String value) { if (value == null || value.trim().isEmpty()) { return 0; } try { int offset = Integer.parseInt(value.trim()); if (offset < 0) { throw invalid("cursor 无效"); } return offset; } catch (NumberFormatException exception) { throw invalid("cursor 无效"); } } private UUID uuid(String value) { return value == null || value.trim().isEmpty() ? null : UUID.fromString(value); } private String highlight(String value) { String escaped = value == null ? "" : value .replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); return escaped .replace(DmsMeilisearchClient.HIGHLIGHT_START, "") .replace(DmsMeilisearchClient.HIGHLIGHT_END, ""); } private DmsPermissionException invalid(String message) { return DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, message); } }