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 SOURCES = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList( "CUSTOM_PRINT", "LIST_PRINT", "REPORT_VIEW"))); private final ObjectProvider auditClientProvider; private final ObjectProvider moduleApiProvider; private final Map menuCache = new ConcurrentHashMap<>(); public LimsPrintAuditServiceImpl(ObjectProvider auditClientProvider, ObjectProvider 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 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 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 normalizeTargets(List rawTargets) { if (rawTargets == null || rawTargets.isEmpty()) { throw new IllegalArgumentException("targets 不能为空"); } Map 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> toTargetMaps(List targets) { List> result = new ArrayList<>(); for (NormalizedTarget target : targets) { Map 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 toSubTitles(List targets) { List 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; } } }