package jnpf.bizcommon.audit.service;
|
|
import com.alibaba.fastjson.JSON;
|
import jnpf.bizcommon.audit.entity.AuditEventEntity;
|
import jnpf.audit.diff.AuditFieldDiff;
|
import jnpf.bizcommon.audit.entity.model.AuditFieldDiffViewVO;
|
import jnpf.bizcommon.audit.service.support.AuditQueryResponseSanitizer;
|
import jnpf.bizcommon.audit.service.value.AuditFieldValueContext;
|
import jnpf.bizcommon.audit.service.value.AuditFieldValueResolver;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.stereotype.Service;
|
|
import java.time.Instant;
|
import java.time.ZoneId;
|
import java.time.format.DateTimeFormatter;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
|
/** 将兼容字符串 fieldDiffs 装配成结构化、可展示且可脱敏的字段变更。 */
|
@Service
|
@Slf4j
|
public class AuditFieldDiffViewService {
|
|
private final List<AuditFieldValueResolver> resolvers;
|
private final ZoneId businessZone;
|
|
@Autowired
|
public AuditFieldDiffViewService(
|
List<AuditFieldValueResolver> resolvers,
|
@Value("${audit.query.business-time-zone:Asia/Shanghai}") String businessTimeZone) {
|
this.resolvers = resolvers == null ? Collections.emptyList() : new ArrayList<>(resolvers);
|
this.businessZone = parseZone(businessTimeZone);
|
}
|
|
/**
|
* sanitizedFieldDiffs 已由查询脱敏器过滤无意义项并处理凭证引用,原始事件不会被修改。
|
*/
|
@SuppressWarnings("unchecked")
|
public List<AuditFieldDiffViewVO> build(AuditEventEntity event,
|
String sanitizedFieldDiffs,
|
Map<String, AuditFieldDiff.FieldMeta> currentMetadata) {
|
if (sanitizedFieldDiffs == null || sanitizedFieldDiffs.trim().isEmpty()) {
|
return Collections.emptyList();
|
}
|
Object parsed;
|
try {
|
parsed = JSON.parse(sanitizedFieldDiffs);
|
} catch (Throwable ignored) {
|
return Collections.emptyList();
|
}
|
if (!(parsed instanceof List)) {
|
return Collections.emptyList();
|
}
|
|
List<AuditFieldDiffViewVO> views = new ArrayList<>();
|
Map<String, String> dictionaryTypes = new LinkedHashMap<>();
|
for (Object raw : (List<?>) parsed) {
|
if (!(raw instanceof Map)) {
|
continue;
|
}
|
Map<String, Object> item = (Map<String, Object>) raw;
|
AuditFieldDiffViewVO view = toView(item,
|
currentMetadata, event == null ? null : event.getActionCode());
|
if (view != null) {
|
views.add(view);
|
AuditFieldDiff.FieldMeta meta = metadata(currentMetadata, view.getField());
|
String dictionaryType = firstText(item.get("dictionaryType"),
|
meta == null ? null : meta.dictionaryType());
|
if (dictionaryType != null) {
|
dictionaryTypes.put(view.getField().toLowerCase(Locale.ROOT), dictionaryType);
|
}
|
}
|
}
|
|
AuditFieldValueContext context = new AuditFieldValueContext(
|
event == null ? null : event.getTargetTable(),
|
event == null ? null : event.getTenantId(),
|
event == null ? null : event.getEventTime(),
|
dictionaryTypes);
|
List<AuditFieldDiffViewVO> resolvedViews = views;
|
for (AuditFieldValueResolver resolver : resolvers) {
|
if (resolver != null) {
|
try {
|
if (!resolver.supports(context.getTargetTable())) {
|
continue;
|
}
|
List<AuditFieldDiffViewVO> candidate = copyViews(resolvedViews);
|
resolver.resolve(context, candidate);
|
resolvedViews = candidate;
|
} catch (RuntimeException ex) {
|
// 解析失败只能降级当前 resolver,日志不携带字段值或详情正文。
|
log.warn("[AUDIT-QUERY] 字段值解析器失败 resolver={} err={}",
|
resolver.getClass().getSimpleName(), ex.getClass().getSimpleName());
|
}
|
}
|
}
|
for (AuditFieldDiffViewVO view : resolvedViews) {
|
finishDisplay(view);
|
}
|
return resolvedViews;
|
}
|
|
/**
|
* 为新事件固化可可靠解析的类型和展示值,同时保留旧 field_diffs 的 type/jnpfKey 等键。
|
* 解析失败时原样返回,不能因为展示增强造成 Layer 0 事件丢失。
|
*/
|
@SuppressWarnings("unchecked")
|
public String enrichForPersistence(AuditEventEntity event,
|
String fieldDiffs,
|
Map<String, AuditFieldDiff.FieldMeta> metadata) {
|
if (fieldDiffs == null || fieldDiffs.trim().isEmpty()) {
|
return fieldDiffs;
|
}
|
try {
|
Object parsed = JSON.parse(fieldDiffs);
|
if (!(parsed instanceof List)) {
|
return fieldDiffs;
|
}
|
List<AuditFieldDiffViewVO> views = build(event, fieldDiffs, metadata);
|
Map<String, AuditFieldDiffViewVO> byField = new LinkedHashMap<>();
|
for (AuditFieldDiffViewVO view : views) {
|
if (view.getField() != null) {
|
byField.put(view.getField().toLowerCase(Locale.ROOT), view);
|
}
|
}
|
for (Object raw : (List<?>) parsed) {
|
if (!(raw instanceof Map)) {
|
continue;
|
}
|
Map<String, Object> item = (Map<String, Object>) raw;
|
String field = text(item.get("field"));
|
AuditFieldDiffViewVO view = field == null
|
? null : byField.get(field.toLowerCase(Locale.ROOT));
|
if (view == null) {
|
continue;
|
}
|
putIfMissing(item, "valueType", view.getValueType());
|
putIfMissing(item, "componentType", view.getComponentType());
|
if (Boolean.TRUE.equals(view.getMasked())) {
|
item.put("masked", true);
|
item.put("valueType", "masked");
|
item.put("componentType", "sign");
|
if (item.containsKey("oldData")) {
|
item.put("oldData", AuditFieldDiff.MASKED);
|
}
|
if (item.containsKey("newData")) {
|
item.put("newData", AuditFieldDiff.MASKED);
|
}
|
item.remove("oldDisplay");
|
item.remove("newDisplay");
|
continue;
|
}
|
if (Boolean.TRUE.equals(view.getTechnical())) {
|
item.put("technical", true);
|
}
|
if (snapshotDisplayType(view.getValueType())) {
|
putDisplayIfResolved(item, "oldDisplay", view.getOldDisplay());
|
putDisplayIfResolved(item, "newDisplay", view.getNewDisplay());
|
}
|
}
|
return JSON.toJSONString(parsed);
|
} catch (Throwable ex) {
|
log.warn("[AUDIT-L0] 字段展示快照固化失败 err={}", ex.getClass().getSimpleName());
|
return fieldDiffs;
|
}
|
}
|
|
private AuditFieldDiffViewVO toView(Map<String, Object> item,
|
Map<String, AuditFieldDiff.FieldMeta> currentMetadata,
|
String actionCode) {
|
String field = text(item.get("field"));
|
if (field == null) {
|
return null;
|
}
|
AuditFieldDiff.FieldMeta meta = metadata(currentMetadata, field);
|
boolean credential = AuditQueryResponseSanitizer.isCredentialReference(field, item, meta);
|
boolean masked = credential
|
|| AuditFieldDiff.MASKED.equals(text(item.get("oldData")))
|
|| AuditFieldDiff.MASKED.equals(text(item.get("newData")));
|
|
AuditFieldDiffViewVO view = new AuditFieldDiffViewVO();
|
view.setField(field);
|
view.setTargetTable(text(item.get("targetTable")));
|
view.setFieldName(credential ? "电子签名" : fieldName(item, meta));
|
view.setChangeType(changeType(item, actionCode, masked));
|
String componentType = credential ? "sign" : firstText(item.get("componentType"),
|
item.get("jnpfKey"), meta == null ? null : meta.componentType());
|
String valueType = masked ? "masked" : firstText(item.get("valueType"),
|
meta == null ? null : meta.valueType());
|
if (valueType == null && isTemporalComponent(componentType)) {
|
valueType = inferValueType(componentType);
|
}
|
view.setValueType(valueType);
|
view.setComponentType(componentType);
|
view.setJnpfKey(firstText(item.get("jnpfKey"), componentType));
|
view.setNameModified(bool(item.get("nameModified")));
|
view.setType(integer(item.get("type")));
|
view.setChidData(mapList(item.get("chidData")));
|
view.setChidField(mapList(item.get("chidField")));
|
view.setMasked(masked);
|
view.setTechnical(bool(item.get("technical")));
|
if (masked) {
|
view.setOldData(null);
|
view.setNewData(null);
|
view.setOldDisplay(null);
|
view.setNewDisplay(null);
|
return view;
|
}
|
|
String oldData = rawText(item.get("oldData"));
|
String newData = rawText(item.get("newData"));
|
view.setOldData(oldData);
|
view.setNewData(newData);
|
view.setOldDisplay(firstTextPreserveEmpty(item.get("oldDisplay"), optionDisplay(meta, oldData),
|
temporalDisplay(view.getValueType(), item, meta, oldData)));
|
view.setNewDisplay(firstTextPreserveEmpty(item.get("newDisplay"), optionDisplay(meta, newData),
|
temporalDisplay(view.getValueType(), item, meta, newData)));
|
return view;
|
}
|
|
private void finishDisplay(AuditFieldDiffViewVO view) {
|
if (Boolean.TRUE.equals(view.getMasked())) {
|
return;
|
}
|
if (view.getValueType() == null) {
|
view.setValueType(inferValueType(view.getComponentType()));
|
}
|
if (view.getComponentType() == null) {
|
view.setComponentType("input");
|
}
|
view.setOldDisplay(fallbackDisplay(view.getValueType(), view.getOldData(), view.getOldDisplay()));
|
view.setNewDisplay(fallbackDisplay(view.getValueType(), view.getNewData(), view.getNewDisplay()));
|
if (view.getTechnical() == null) {
|
view.setTechnical(Boolean.FALSE);
|
}
|
if (view.getMasked() == null) {
|
view.setMasked(Boolean.FALSE);
|
}
|
}
|
|
private String fallbackDisplay(String valueType, String raw, String display) {
|
if (display != null) {
|
return display;
|
}
|
if (raw == null || raw.trim().isEmpty()) {
|
return "";
|
}
|
if ("reference".equals(valueType) || "multiReference".equals(valueType)
|
|| "enum".equals(valueType) || "multiEnum".equals(valueType)) {
|
return "名称暂不可用";
|
}
|
return raw;
|
}
|
|
private String temporalDisplay(String valueType, Map<String, Object> item,
|
AuditFieldDiff.FieldMeta meta, String raw) {
|
if (!("date".equals(valueType) || "datetime".equals(valueType))
|
|| raw == null || raw.trim().isEmpty()) {
|
return null;
|
}
|
try {
|
long epochMillis = Long.parseLong(raw.trim());
|
String pattern = firstText(item.get("format"), meta == null ? null : meta.format(),
|
"date".equals(valueType) ? "yyyy-MM-dd" : "yyyy-MM-dd HH:mm:ss");
|
return DateTimeFormatter.ofPattern(pattern, Locale.ROOT)
|
.withZone(businessZone).format(Instant.ofEpochMilli(epochMillis));
|
} catch (RuntimeException ignored) {
|
return null;
|
}
|
}
|
|
private static String optionDisplay(AuditFieldDiff.FieldMeta meta, String raw) {
|
return meta == null ? null : meta.optionDisplay(raw);
|
}
|
|
private static boolean snapshotDisplayType(String valueType) {
|
return "enum".equals(valueType) || "multiEnum".equals(valueType)
|
|| "reference".equals(valueType) || "multiReference".equals(valueType)
|
|| "date".equals(valueType) || "datetime".equals(valueType);
|
}
|
|
private static void putIfMissing(Map<String, Object> item, String key, String value) {
|
if (!item.containsKey(key) && value != null) {
|
item.put(key, value);
|
}
|
}
|
|
private static void putDisplayIfResolved(Map<String, Object> item, String key, String display) {
|
if (!item.containsKey(key) && display != null && !display.trim().isEmpty()
|
&& !"名称暂不可用".equals(display)) {
|
item.put(key, display);
|
}
|
}
|
|
private static AuditFieldDiff.FieldMeta metadata(
|
Map<String, AuditFieldDiff.FieldMeta> metadata, String field) {
|
if (metadata == null || field == null) {
|
return null;
|
}
|
AuditFieldDiff.FieldMeta exact = metadata.get(field);
|
if (exact != null) {
|
return exact;
|
}
|
AuditFieldDiff.FieldMeta normalized = metadata.get(field.toLowerCase(Locale.ROOT));
|
if (normalized != null) {
|
return normalized;
|
}
|
StringBuilder snake = new StringBuilder(field.length() + 8);
|
for (int i = 0; i < field.length(); i++) {
|
char ch = field.charAt(i);
|
if (Character.isUpperCase(ch)) {
|
snake.append('_').append(Character.toLowerCase(ch));
|
} else {
|
snake.append(Character.toLowerCase(ch));
|
}
|
}
|
return metadata.get(snake.toString());
|
}
|
|
private static String fieldName(Map<String, Object> item, AuditFieldDiff.FieldMeta meta) {
|
String name = text(item.get("fieldName"));
|
if (name != null) {
|
return name;
|
}
|
return meta != null && meta.label() != null && !meta.label().trim().isEmpty()
|
? meta.label() : text(item.get("field"));
|
}
|
|
private static String changeType(Map<String, Object> item, String actionCode, boolean masked) {
|
if (masked) {
|
return "updated";
|
}
|
String explicit = text(item.get("changeType"));
|
if ("created".equals(explicit) || "updated".equals(explicit) || "deleted".equals(explicit)) {
|
return explicit;
|
}
|
if (actionCode != null && actionCode.toUpperCase(Locale.ROOT).contains("DELETE")) {
|
return "deleted";
|
}
|
Object type = item.get("type");
|
return type != null && "0".equals(String.valueOf(type)) ? "created" : "updated";
|
}
|
|
private static String inferValueType(String component) {
|
if (component == null) {
|
return "text";
|
}
|
String key = component.toLowerCase(Locale.ROOT);
|
if (key.contains("time")) {
|
return "datetime";
|
}
|
if (key.contains("date")) {
|
return "date";
|
}
|
if (key.contains("number") || "slider".equals(key) || "rate".equals(key)) {
|
return "number";
|
}
|
if ("switch".equals(key)) {
|
return "boolean";
|
}
|
if ("checkbox".equals(key)) {
|
return "multiEnum";
|
}
|
if (key.contains("select") || "radio".equals(key)) {
|
return "enum";
|
}
|
if ("table".equals(key) || key.contains("upload")) {
|
return "json";
|
}
|
return "text";
|
}
|
|
private static boolean isTemporalComponent(String component) {
|
if (component == null) {
|
return false;
|
}
|
String key = component.toLowerCase(Locale.ROOT);
|
return key.contains("date") || key.contains("time");
|
}
|
|
private static List<AuditFieldDiffViewVO> copyViews(List<AuditFieldDiffViewVO> source) {
|
List<AuditFieldDiffViewVO> copy = new ArrayList<>(source.size());
|
for (AuditFieldDiffViewVO value : source) {
|
AuditFieldDiffViewVO item = new AuditFieldDiffViewVO();
|
item.setField(value.getField());
|
item.setTargetTable(value.getTargetTable());
|
item.setFieldName(value.getFieldName());
|
item.setChangeType(value.getChangeType());
|
item.setValueType(value.getValueType());
|
item.setComponentType(value.getComponentType());
|
item.setJnpfKey(value.getJnpfKey());
|
item.setNameModified(value.getNameModified());
|
item.setType(value.getType());
|
item.setChidData(value.getChidData());
|
item.setChidField(value.getChidField());
|
item.setOldData(value.getOldData());
|
item.setNewData(value.getNewData());
|
item.setOldDisplay(value.getOldDisplay());
|
item.setNewDisplay(value.getNewDisplay());
|
item.setMasked(value.getMasked());
|
item.setTechnical(value.getTechnical());
|
copy.add(item);
|
}
|
return copy;
|
}
|
|
private static ZoneId parseZone(String zone) {
|
try {
|
return ZoneId.of(zone == null || zone.trim().isEmpty() ? "Asia/Shanghai" : zone.trim());
|
} catch (RuntimeException ignored) {
|
return ZoneId.of("Asia/Shanghai");
|
}
|
}
|
|
private static boolean bool(Object value) {
|
return value instanceof Boolean ? (Boolean) value
|
: value != null && "true".equalsIgnoreCase(String.valueOf(value));
|
}
|
|
private static Integer integer(Object value) {
|
if (value instanceof Number) {
|
return ((Number) value).intValue();
|
}
|
try {
|
return value == null ? null : Integer.valueOf(String.valueOf(value));
|
} catch (NumberFormatException ignored) {
|
return null;
|
}
|
}
|
|
@SuppressWarnings("unchecked")
|
private static List<Map<String, Object>> mapList(Object value) {
|
if (!(value instanceof List)) {
|
return null;
|
}
|
List<Map<String, Object>> result = new ArrayList<>();
|
for (Object item : (List<?>) value) {
|
if (item instanceof Map) {
|
result.add(new LinkedHashMap<>((Map<String, Object>) item));
|
}
|
}
|
return result.isEmpty() ? null : result;
|
}
|
|
private static String rawText(Object value) {
|
return value == null ? "" : String.valueOf(value);
|
}
|
|
private static String text(Object value) {
|
if (value == null) {
|
return null;
|
}
|
String result = String.valueOf(value).trim();
|
return result.isEmpty() ? null : result;
|
}
|
|
private static String firstText(Object... values) {
|
if (values != null) {
|
for (Object value : values) {
|
String result = text(value);
|
if (result != null) {
|
return result;
|
}
|
}
|
}
|
return null;
|
}
|
|
private static String firstTextPreserveEmpty(Object... values) {
|
if (values != null) {
|
for (Object value : values) {
|
if (value != null) {
|
return String.valueOf(value);
|
}
|
}
|
}
|
return null;
|
}
|
}
|