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<LimsSignUsageTarget> normalizeTargets(Collection<LimsSignUsageTarget> targets) {
|
if (targets == null || targets.isEmpty()) {
|
return Collections.emptyList();
|
}
|
List<LimsSignUsageTarget> normalized = new ArrayList<>();
|
Set<String> 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<UsageGroup> groupByBizCode(Collection<LimsSignUsageTarget> targets) {
|
Map<String, UsageGroup> 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<String> 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<String, Object> signExtra(Collection<String> requestedTargetIds,
|
Collection<LimsSignUsageTarget> actualTargets) {
|
List<LimsSignUsageTarget> targets = normalizeTargets(actualTargets);
|
targets.sort(Comparator.comparing(LimsSignUsageTarget::getTargetTable)
|
.thenComparing(LimsSignUsageTarget::getTargetId)
|
.thenComparing(LimsSignUsageTarget::getBizType)
|
.thenComparing(LimsSignUsageTarget::getBizModule)
|
.thenComparing(LimsSignUsageTarget::getBizCode));
|
Map<String, Object> extra = new LinkedHashMap<>();
|
extra.put("schemaVersion", SCHEMA_VERSION);
|
extra.put("requestedTargets", normalizeIds(requestedTargetIds));
|
List<Map<String, Object>> affectedTargets = new ArrayList<>();
|
for (LimsSignUsageTarget target : targets) {
|
Map<String, Object> 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<String, Object> eventExtra(LimsSignEntity sign, SignOpType opType, UsageGroup group) {
|
Map<String, Object> 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<String> tables = new LinkedHashSet<>();
|
List<String> 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<String> normalizeIds(Collection<String> ids) {
|
if (ids == null || ids.isEmpty()) {
|
return Collections.emptyList();
|
}
|
Set<String> normalized = new LinkedHashSet<>();
|
for (String id : ids) {
|
if (!isBlank(id)) {
|
normalized.add(id.trim());
|
}
|
}
|
List<String> 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<LimsSignUsageTarget> 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<LimsSignUsageTarget> getTargets() {
|
return Collections.unmodifiableList(targets);
|
}
|
}
|
}
|