刘光辉
5 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
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<DmsFullTextSearchItemVO> 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<Boolean> allowed = allowed(candidates.getHits());
            java.util.Set<String> 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<Boolean> allowed(List<DmsMeilisearchSearchHit> hits) {
        List<PermissionDecisionForm> 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<DmsPermissionDecisionVO> decisions = permissionDecisionService.evaluate(requests);
            List<Boolean> result = new ArrayList<>();
            for (DmsPermissionDecisionVO decision : decisions) {
                result.add(decision.isAllowed());
            }
            return result;
        } catch (RuntimeException batchFailure) {
            List<Boolean> 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<DmsFullTextSearchItemVO> 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("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&#39;");
        return escaped
                .replace(DmsMeilisearchClient.HIGHLIGHT_START, "<mark>")
                .replace(DmsMeilisearchClient.HIGHLIGHT_END, "</mark>");
    }
 
    private DmsPermissionException invalid(String message) {
        return DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, message);
    }
}