刘光辉
3 小时以前 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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
package jnpf.limsService.impl;
 
import jnpf.audit.AuditConsts;
import jnpf.audit.model.AuditEventDTO;
import jnpf.audit.sdk.AuditClient;
import jnpf.base.ModuleApi;
import jnpf.base.entity.ModuleEntity;
import jnpf.limsEntity.print.LimsPrintAuditParam;
import jnpf.limsEntity.print.LimsPrintAuditTarget;
import jnpf.limsService.LimsPrintAuditService;
import jnpf.util.JsonUtil;
import jnpf.util.ServletUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
 
@Slf4j
@Service
public class LimsPrintAuditServiceImpl implements LimsPrintAuditService {
 
    private static final String ACTION_PRINT = "PRINT";
    private static final String DEFAULT_BIZ_TYPE = "LIMS_PRINT";
    private static final Pattern REQUEST_ID = Pattern.compile("^[0-9a-fA-F]{32}$");
    private static final Pattern TARGET_TABLE = Pattern.compile("^[A-Za-z][A-Za-z0-9_]{0,99}$");
    private static final long MENU_CACHE_MILLIS = 5 * 60 * 1000L;
    private static final Set<String> SOURCES = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(
            "CUSTOM_PRINT", "LIST_PRINT", "REPORT_VIEW")));
 
    private final ObjectProvider<AuditClient> auditClientProvider;
    private final ObjectProvider<ModuleApi> moduleApiProvider;
    private final Map<String, MenuCacheEntry> menuCache = new ConcurrentHashMap<>();
 
    public LimsPrintAuditServiceImpl(ObjectProvider<AuditClient> auditClientProvider,
                                     ObjectProvider<ModuleApi> moduleApiProvider) {
        this.auditClientProvider = auditClientProvider;
        this.moduleApiProvider = moduleApiProvider;
    }
 
    @Override
    public void record(LimsPrintAuditParam param) {
        AuditEventDTO event = buildEvent(param, resolveCurrentMenu());
        auditClientProvider.ifAvailable(client -> client.record(event));
    }
 
    static AuditEventDTO buildEvent(LimsPrintAuditParam param) {
        return buildEvent(param, MenuContext.missing());
    }
 
    static AuditEventDTO buildEvent(LimsPrintAuditParam param, MenuContext menu) {
        if (param == null) {
            throw new IllegalArgumentException("打印审计参数不能为空");
        }
        String requestId = normalizeRequestId(param.getRequestId());
        String printName = requiredText(param.getPrintName(), 200, "printName");
        String templateId = optionalText(param.getTemplateId(), 200, "templateId");
        String recordTitle = optionalText(param.getRecordTitle(), 500, "recordTitle");
        String targetTable = optionalText(param.getTargetTable(), 100, "targetTable");
        if (targetTable != null && !TARGET_TABLE.matcher(targetTable).matches()) {
            throw new IllegalArgumentException("targetTable 格式不合法");
        }
        String bizType = optionalText(param.getBizType(), 50, "bizType");
        String source = requiredText(param.getSource(), 50, "source").toUpperCase(Locale.ROOT);
        if (!SOURCES.contains(source)) {
            throw new IllegalArgumentException("source 不在允许范围内: " + source);
        }
 
        List<NormalizedTarget> targets = normalizeTargets(param.getTargets());
        boolean batch = targets.size() > 1;
        String titleBase = recordTitle == null ? printName : recordTitle;
        String finalTitle = batch ? appendBatchCount(titleBase, targets.size()) : titleBase;
        String eventId = "PRINT-" + requestId;
 
        Map<String, Object> extra = new LinkedHashMap<>();
        if (templateId != null) {
            extra.put("templateId", templateId);
        }
        extra.put("printName", printName);
        if (targetTable != null) {
            extra.put("targetTable", targetTable);
        }
        extra.put("bizType", bizType == null ? DEFAULT_BIZ_TYPE : bizType);
        extra.put("source", source);
        if (menu.menuId != null) {
            extra.put("menuId", menu.menuId);
        }
        if (menu.menuName != null) {
            extra.put("menuName", menu.menuName);
        }
        extra.put("menuResolution", menu.resolution);
        extra.put("printMode", batch ? "BATCH" : "SINGLE");
        extra.put("printCount", targets.size());
        extra.put("printTargets", toTargetMaps(targets));
        extra.put("recordSubTitles", toSubTitles(targets));
 
        NormalizedTarget single = batch ? null : targets.get(0);
        return AuditEventDTO.builder()
                .clientEventId(eventId)
                .operationId(eventId)
                .eventType(AuditConsts.TYPE_PRINT)
                .actionCode(ACTION_PRINT)
                .actionLabel("发起打印")
                .sourceLayer(3)
                .bizModule(menu.menuName == null ? sourceLabel(source) : menu.menuName)
                .bizType(bizType == null ? DEFAULT_BIZ_TYPE : bizType)
                .bizCode(single == null ? null : single.bizCode)
                .targetTable(targetTable)
                .targetId(single == null ? null : single.id)
                .recordTitle(finalTitle)
                .entryType(menu.menuId == null ? null : "MENU")
                .entryId(menu.menuId)
                .entryName(menu.menuName)
                .extra(JsonUtil.getObjectToString(extra))
                .build();
    }
 
    private MenuContext resolveCurrentMenu() {
        String menuId;
        try {
            menuId = optionalText(ServletUtil.getRequest().getHeader("Jnpf-Menu-Id"), 100, "Jnpf-Menu-Id");
        } catch (Throwable t) {
            log.warn("[PRINT-AUDIT] 读取或校验菜单请求头失败,按无菜单降级记录", t);
            return MenuContext.invalid();
        }
        if (menuId == null) {
            return MenuContext.missing();
        }
 
        long now = System.currentTimeMillis();
        MenuCacheEntry cached = menuCache.get(menuId);
        if (cached != null && cached.expiresAt > now) {
            return MenuContext.resolved(menuId, cached.menuName);
        }
        if (cached != null) {
            menuCache.remove(menuId, cached);
        }
 
        ModuleApi moduleApi = moduleApiProvider.getIfAvailable();
        if (moduleApi == null) {
            log.warn("[PRINT-AUDIT] ModuleApi 未装配,菜单名称降级记录 menuId={}", menuId);
            return MenuContext.failed(menuId);
        }
        try {
            ModuleEntity menu = moduleApi.getModuleByList(menuId);
            String menuName = menu == null || Integer.valueOf(1).equals(menu.getDeleteMark())
                    ? null : optionalText(menu.getFullName(), 200, "menuName");
            if (menuName == null) {
                return MenuContext.unresolved(menuId);
            }
            menuCache.put(menuId, new MenuCacheEntry(menuName, now + MENU_CACHE_MILLIS));
            return MenuContext.resolved(menuId, menuName);
        } catch (Throwable t) {
            log.warn("[PRINT-AUDIT] 查询菜单名称失败,审计事件降级记录 menuId={}", menuId, t);
            return MenuContext.failed(menuId);
        }
    }
 
    private static String sourceLabel(String source) {
        if ("REPORT_VIEW".equals(source)) {
            return "表单详情打印";
        }
        if ("LIST_PRINT".equals(source)) {
            return "列表打印";
        }
        return "自定义打印";
    }
 
    private static String normalizeRequestId(String raw) {
        String value = requiredText(raw, 36, "requestId").replace("-", "");
        if (!REQUEST_ID.matcher(value).matches()) {
            throw new IllegalArgumentException("requestId 必须是 UUID");
        }
        return value.toLowerCase(Locale.ROOT);
    }
 
    private static List<NormalizedTarget> normalizeTargets(List<LimsPrintAuditTarget> rawTargets) {
        if (rawTargets == null || rawTargets.isEmpty()) {
            throw new IllegalArgumentException("targets 不能为空");
        }
        Map<String, NormalizedTarget> unique = new LinkedHashMap<>();
        for (int i = 0; i < rawTargets.size(); i++) {
            LimsPrintAuditTarget raw = rawTargets.get(i);
            if (raw == null) {
                throw new IllegalArgumentException("targets[" + i + "] 不能为空");
            }
            String id = requiredText(raw.getId(), 64, "targets[" + i + "].id");
            if (!unique.containsKey(id)) {
                unique.put(id, new NormalizedTarget(
                        id,
                        optionalText(raw.getBizCode(), 100, "targets[" + i + "].bizCode"),
                        optionalText(raw.getTitle(), 500, "targets[" + i + "].title")));
            }
        }
        return new ArrayList<>(unique.values());
    }
 
    private static List<Map<String, Object>> toTargetMaps(List<NormalizedTarget> targets) {
        List<Map<String, Object>> result = new ArrayList<>();
        for (NormalizedTarget target : targets) {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("id", target.id);
            if (target.bizCode != null) {
                item.put("bizCode", target.bizCode);
            }
            if (target.title != null) {
                item.put("title", target.title);
            }
            result.add(item);
        }
        return result;
    }
 
    private static List<String> toSubTitles(List<NormalizedTarget> targets) {
        List<String> result = new ArrayList<>();
        for (NormalizedTarget target : targets) {
            result.add(target.bizCode != null ? target.bizCode
                    : (target.title != null ? target.title : target.id));
        }
        return result;
    }
 
    private static String appendBatchCount(String title, int count) {
        String suffix = "(批量打印 " + count + " 条)";
        int maxBaseLength = Math.max(0, 500 - suffix.length());
        return (title.length() > maxBaseLength ? title.substring(0, maxBaseLength) : title) + suffix;
    }
 
    private static String requiredText(String raw, int maxLength, String field) {
        String value = optionalText(raw, maxLength, field);
        if (value == null) {
            throw new IllegalArgumentException(field + " 不能为空");
        }
        return value;
    }
 
    private static String optionalText(String raw, int maxLength, String field) {
        if (raw == null) {
            return null;
        }
        String value = raw.replace('\r', ' ').replace('\n', ' ').trim();
        if (value.isEmpty()) {
            return null;
        }
        if (value.length() > maxLength) {
            throw new IllegalArgumentException(field + " 超过 " + maxLength + " 字符");
        }
        return value;
    }
 
    private static final class NormalizedTarget {
        private final String id;
        private final String bizCode;
        private final String title;
 
        private NormalizedTarget(String id, String bizCode, String title) {
            this.id = id;
            this.bizCode = bizCode;
            this.title = title;
        }
    }
 
    static final class MenuContext {
        private final String menuId;
        private final String menuName;
        private final String resolution;
 
        private MenuContext(String menuId, String menuName, String resolution) {
            this.menuId = menuId;
            this.menuName = menuName;
            this.resolution = resolution;
        }
 
        static MenuContext resolved(String menuId, String menuName) {
            return new MenuContext(menuId, menuName, "RESOLVED");
        }
 
        static MenuContext unresolved(String menuId) {
            return new MenuContext(menuId, null, "NOT_FOUND");
        }
 
        static MenuContext failed(String menuId) {
            return new MenuContext(menuId, null, "FAILED");
        }
 
        static MenuContext missing() {
            return new MenuContext(null, null, "MISSING");
        }
 
        static MenuContext invalid() {
            return new MenuContext(null, null, "INVALID");
        }
    }
 
    private static final class MenuCacheEntry {
        private final String menuName;
        private final long expiresAt;
 
        private MenuCacheEntry(String menuName, long expiresAt) {
            this.menuName = menuName;
            this.expiresAt = expiresAt;
        }
    }
}