package jnpf.limsService.support; import jnpf.limsEntity.LimsSignEntity; import jnpf.limsEntity.LimsSignUsageTarget; import jnpf.limsEntity.SignOpType; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; 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.TimeZone; /** 电子签名使用事件的规范化、分组和幂等键生成。 */ public final class LimsSignUsageSupport { public static final int SCHEMA_VERSION = 1; public static final String ACTION_SIGN_USED = "SIGN_USED"; private LimsSignUsageSupport() { } /** * 清理空目标并按 table + id 去重,保持业务执行顺序。 */ public static List normalizeTargets(Collection targets) { if (targets == null || targets.isEmpty()) { return Collections.emptyList(); } List normalized = new ArrayList<>(); Set seen = new LinkedHashSet<>(); for (LimsSignUsageTarget target : targets) { if (target == null || isBlank(target.getTargetTable()) || isBlank(target.getTargetId()) || isBlank(target.getBizType()) || isBlank(target.getBizModule()) || isBlank(target.getBizCode())) { continue; } String key = target.getTargetTable().trim().toLowerCase(Locale.ROOT) + "\n" + target.getTargetId().trim(); if (!seen.add(key)) { continue; } normalized.add(LimsSignUsageTarget.builder() .targetTable(target.getTargetTable().trim().toLowerCase(Locale.ROOT)) .targetId(target.getTargetId().trim()) .bizType(target.getBizType().trim()) .bizModule(target.getBizModule().trim()) .bizCode(target.getBizCode().trim()) .build()); } return normalized; } /** 每个唯一 bizCode 形成一个时间线事件;组内保留实际目标。 */ public static List groupByBizCode(Collection targets) { Map groups = new LinkedHashMap<>(); for (LimsSignUsageTarget target : normalizeTargets(targets)) { UsageGroup group = groups.get(target.getBizCode()); if (group == null) { group = new UsageGroup(target.getBizType(), target.getBizModule(), target.getBizCode()); groups.put(target.getBizCode(), group); } group.targets.add(target); } return new ArrayList<>(groups.values()); } /** * SIGN_USED 的确定性幂等键。目标顺序与重复项不会改变结果,长度固定为 67,小于 DB varchar(70)。 */ public static String clientEventId(String signId, SignOpType opType, UsageGroup group) { List targetKeys = new ArrayList<>(); for (LimsSignUsageTarget target : group.targets) { targetKeys.add(target.getTargetTable() + ":" + target.getTargetId()); } targetKeys.sort(Comparator.naturalOrder()); String source = value(signId) + "\n" + (opType == null ? "" : opType.getCode()) + "\n" + value(group.bizType) + "\n" + value(group.bizModule) + "\n" + value(group.bizCode) + "\n" + String.join("\n", targetKeys); return "SU-" + sha256(source); } /** lims_sign.extra_json:请求目标仅作请求摘要,affectedTargets 才是执行事实。 */ public static Map signExtra(Collection requestedTargetIds, Collection actualTargets) { List targets = normalizeTargets(actualTargets); targets.sort(Comparator.comparing(LimsSignUsageTarget::getTargetTable) .thenComparing(LimsSignUsageTarget::getTargetId) .thenComparing(LimsSignUsageTarget::getBizType) .thenComparing(LimsSignUsageTarget::getBizModule) .thenComparing(LimsSignUsageTarget::getBizCode)); Map extra = new LinkedHashMap<>(); extra.put("schemaVersion", SCHEMA_VERSION); extra.put("requestedTargets", normalizeIds(requestedTargetIds)); List> affectedTargets = new ArrayList<>(); for (LimsSignUsageTarget target : targets) { Map item = new LinkedHashMap<>(); item.put("targetTable", target.getTargetTable()); item.put("targetId", target.getTargetId()); item.put("bizType", target.getBizType()); item.put("bizModule", target.getBizModule()); item.put("bizCode", target.getBizCode()); affectedTargets.add(item); } extra.put("affectedTargets", affectedTargets); extra.put("affected", targets.size()); return extra; } /** 单个 SIGN_USED 事件的 extra,仅包含该 bizCode 下的实际成功目标。 */ public static Map eventExtra(LimsSignEntity sign, SignOpType opType, UsageGroup group) { Map extra = new LinkedHashMap<>(); extra.put("schemaVersion", SCHEMA_VERSION); extra.put("opType", opType == null ? null : opType.getCode()); extra.put("bizModule", group.getBizModule()); extra.put("signerUserId", sign == null ? null : sign.getCreatorUserId()); extra.put("signerAccount", sign == null ? null : sign.getAccountName()); extra.put("signedAt", sign == null ? null : formatDate(sign.getCreatorTime())); Set tables = new LinkedHashSet<>(); List ids = new ArrayList<>(); for (LimsSignUsageTarget target : group.targets) { tables.add(target.getTargetTable()); ids.add(target.getTargetId()); } extra.put("businessTargetTable", tables.size() == 1 ? tables.iterator().next() : null); extra.put("affectedTargetIds", ids); extra.put("affected", ids.size()); return extra; } private static List normalizeIds(Collection ids) { if (ids == null || ids.isEmpty()) { return Collections.emptyList(); } Set normalized = new LinkedHashSet<>(); for (String id : ids) { if (!isBlank(id)) { normalized.add(id.trim()); } } List result = new ArrayList<>(normalized); result.sort(Comparator.naturalOrder()); return result; } private static String formatDate(Date date) { if (date == null) { return null; } SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.ROOT); format.setTimeZone(TimeZone.getDefault()); return format.format(date); } private static String sha256(String value) { try { byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); StringBuilder out = new StringBuilder(digest.length * 2); for (byte b : digest) { out.append(String.format(Locale.ROOT, "%02x", b & 0xff)); } return out.toString(); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("JDK 缺少 SHA-256", e); } } private static String value(String value) { return value == null ? "" : value; } private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } /** 一个规范业务编号对应的签名使用事件分组。 */ public static final class UsageGroup { private final String bizType; private final String bizModule; private final String bizCode; private final List targets = new ArrayList<>(); private UsageGroup(String bizType, String bizModule, String bizCode) { this.bizType = bizType; this.bizModule = bizModule; this.bizCode = bizCode; } public String getBizType() { return bizType; } public String getBizCode() { return bizCode; } public String getBizModule() { return bizModule; } public List getTargets() { return Collections.unmodifiableList(targets); } } }