package jnpf.bizcommon.audit.service; import jnpf.audit.AuditConsts; import jnpf.audit.model.AuditEventDTO; import jnpf.bizcommon.audit.service.sign.AuditSignEvidence; import jnpf.bizcommon.audit.service.sign.AuditSignEvidenceReader; import jnpf.bizcommon.audit.service.sign.AuditSignMetaData; import jnpf.util.JsonUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; /** 为数据接口操作事实补充请求所携带的电子签名事实。 */ @Slf4j @Service @RequiredArgsConstructor public class DataInterfaceSignAuditService { private static final String SIGN_ACTION = "SIGN"; private static final String REQUEST_SIGN_FIELD = "__request_biz_sign__"; private final AuditSignEvidenceReader signEvidenceReader; private final AuditEventService auditEventService; /** * 只处理显式携带 relatedSignId 的数据接口事件。异常向上交给审计投递器进入 spool 重试, * 调用发生在审计服务内部,不会影响原始业务请求。 */ public void recordRelatedSign(AuditEventDTO businessEvent) { if (businessEvent == null || isBlank(businessEvent.getRelatedSignId()) || !AuditConsts.TYPE_DATA_CHANGE.equals(businessEvent.getEventType()) || !"DATA_INTERFACE".equals(businessEvent.getBizType())) { return; } String signId = businessEvent.getRelatedSignId().trim(); Map evidences = signEvidenceReader.read( Collections.singletonList(signId), businessEvent.getTenantId()); AuditSignEvidence evidence = evidences.get(signId); if (evidence == null) { log.warn("[DATA-INTERFACE-AUDIT] 未查询到同租户签名记录,跳过签名事件 clientEventId={}", businessEvent.getClientEventId()); return; } if (isBlank(evidence.operatorId()) || !evidence.operatorId().equals(businessEvent.getOperatorId())) { log.warn("[DATA-INTERFACE-AUDIT] 签名归属与当前操作人不一致,跳过签名事件 clientEventId={}", businessEvent.getClientEventId()); return; } AuditSignMetaData metaData = AuditSignMetaData.parse(evidence.metaData()); String signatureRole = metaData.isReviewButton() ? "REVIEW" : "PRIMARY"; Map extra = new LinkedHashMap<>(); extra.put("signField", REQUEST_SIGN_FIELD); extra.put("signatureRole", signatureRole); extra.put("auditSource", "DATA_INTERFACE"); if (!isBlank(evidence.accountName())) { extra.put("accountName", evidence.accountName()); } if (!isBlank(evidence.opType())) { extra.put("opType", evidence.opType()); } AuditEventDTO signEvent = AuditEventDTO.builder() .clientEventId(signClientEventId(businessEvent.getOperationId(), signId)) .operationId(businessEvent.getOperationId()) .eventTime(evidence.signedAt() == null ? businessEvent.getEventTime() : evidence.signedAt()) .tenantId(businessEvent.getTenantId()) .operatorId(evidence.operatorId()) .operatorName(businessEvent.getOperatorName()) .operatorOrgId(businessEvent.getOperatorOrgId()) .appName(businessEvent.getAppName()) .bizModule(businessEvent.getBizModule()) .ip(businessEvent.getIp()) .ipRegion(businessEvent.getIpRegion()) .browser(businessEvent.getBrowser()) .os(businessEvent.getOs()) .requestUri(businessEvent.getRequestUri()) .eventType(AuditConsts.TYPE_E_SIGNATURE) .actionCode(SIGN_ACTION) .actionLabel(metaData.isReviewButton() ? "复审签名" : "电子签名") .sourceLayer(3) .targetTable(AuditSignEvidenceReader.SIGN_TABLE) .targetId(signId) .bizType(businessEvent.getBizType()) .bizCode(businessEvent.getBizCode()) .reason(evidence.note()) .extra(JsonUtil.getObjectToString(extra)) .recordTitle(businessEvent.getRecordTitle()) .entryType(businessEvent.getEntryType()) .entryId(businessEvent.getEntryId()) .entryName(businessEvent.getEntryName()) .build(); auditEventService.saveIdempotent(signEvent); } private static String signClientEventId(String operationId, String signId) { String source = value(operationId) + "\n" + value(signId); try { byte[] digest = MessageDigest.getInstance("SHA-256") .digest(source.getBytes(StandardCharsets.UTF_8)); StringBuilder hex = new StringBuilder("DIS-"); for (byte item : digest) { hex.append(String.format("%02x", item & 0xff)); } return hex.toString(); } catch (Exception e) { throw new IllegalStateException("生成数据接口签名事件幂等键失败", e); } } private static String value(String text) { return text == null ? "" : text; } private static boolean isBlank(String text) { return text == null || text.trim().isEmpty(); } }