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;
|
}
|
}
|
}
|