|
|
@@ -1,12 +1,20 @@
|
|
|
package com.jzg.commons.util.comparble;
|
|
|
|
|
|
-
|
|
|
import cn.hutool.core.util.ObjectUtil;
|
|
|
+import com.alibaba.fastjson2.JSON;
|
|
|
+import com.alibaba.fastjson2.JSONArray;
|
|
|
import com.alibaba.fastjson2.JSONObject;
|
|
|
import com.jzg.commons.entity.po.OperatorTrajectory;
|
|
|
+import com.jzg.commons.util.GetSelectIdDataUtils;
|
|
|
import com.jzg.commons.util.StringUtils;
|
|
|
+import com.jzg.commons.util.spring.SpringUtils;
|
|
|
+import io.swagger.v3.oas.annotations.media.Schema;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.lang.reflect.ParameterizedType;
|
|
|
+import java.lang.reflect.Type;
|
|
|
import java.time.LocalDate;
|
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
@@ -23,29 +31,149 @@ public class CompatibleUtils {
|
|
|
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
|
|
|
+ // ============ 需要忽略的内部字段 ============
|
|
|
private static final Set<String> IGNORED_FIELDS = new HashSet<>(Arrays.asList(
|
|
|
- "id", "createBy", "createTime", "updateBy", "updateTime","dataMark", "systemCode","password","pages","size","current","size","total"
|
|
|
+ "id", "createBy", "createTime", "updateBy", "updateTime", "isDelete", "dataMark", "systemCode", "password", "pages", "size", "current", "size", "total",
|
|
|
+ "ptlAgreementCostId", "ptlAgreementId", "attrId", "rulesId", "batch", "serialNumber",
|
|
|
+ // ★ 前端 DTO 元数据字段,DB 不存储,参与对比会产生误报
|
|
|
+ "attrType", "dictLabal", "commission",
|
|
|
+ // ★ DTO 批量操作容器字段,非真实 DB 子表,子表数据已由 flattenNestedSubFields 提取到顶层
|
|
|
+ "costAddList"
|
|
|
));
|
|
|
|
|
|
+ // ============ 字典映射常量 ============
|
|
|
+ private static final Map<String, String> OPERATOR_MAP = new HashMap<>();
|
|
|
+ static {
|
|
|
+ OPERATOR_MAP.put("1", "等于");
|
|
|
+ OPERATOR_MAP.put("2", "小于");
|
|
|
+ OPERATOR_MAP.put("3", "小于等于");
|
|
|
+ OPERATOR_MAP.put("4", "大于");
|
|
|
+ OPERATOR_MAP.put("5", "大于等于");
|
|
|
+ OPERATOR_MAP.put("eq", "等于");
|
|
|
+ OPERATOR_MAP.put("lt", "小于");
|
|
|
+ OPERATOR_MAP.put("lte", "小于等于");
|
|
|
+ OPERATOR_MAP.put("gt", "大于");
|
|
|
+ OPERATOR_MAP.put("gte", "大于等于");
|
|
|
+ OPERATOR_MAP.put("in", "包含于");
|
|
|
+ OPERATOR_MAP.put("not_in", "不包含于");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final Map<String, String> ATTR_TYPE_MAP = new HashMap<>();
|
|
|
+ static {
|
|
|
+ ATTR_TYPE_MAP.put("input", "输入框");
|
|
|
+ ATTR_TYPE_MAP.put("inputNumber", "数字输入");
|
|
|
+ ATTR_TYPE_MAP.put("select", "下拉选择");
|
|
|
+ ATTR_TYPE_MAP.put("multiSelect", "多选下拉");
|
|
|
+ ATTR_TYPE_MAP.put("radio", "单选");
|
|
|
+ ATTR_TYPE_MAP.put("checkbox", "多选");
|
|
|
+ ATTR_TYPE_MAP.put("date", "日期");
|
|
|
+ ATTR_TYPE_MAP.put("datetime", "日期时间");
|
|
|
+ ATTR_TYPE_MAP.put("text", "文本域");
|
|
|
+ ATTR_TYPE_MAP.put("switch", "开关");
|
|
|
+ ATTR_TYPE_MAP.put("cascader", "级联选择");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 费用类型映射 */
|
|
|
+ private static final Map<String, String> COST_TYPE_MAP = new HashMap<>();
|
|
|
+ static {
|
|
|
+ COST_TYPE_MAP.put("1", "交强险");
|
|
|
+ COST_TYPE_MAP.put("2", "商业险");
|
|
|
+ COST_TYPE_MAP.put("3", "驾意险");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 常用字段描述映射(作为 PO 类没有 @Schema 注解时的回退) */
|
|
|
+ private static final Map<String, String> FIELD_DESCRIPTION_MAP = new HashMap<>();
|
|
|
+ static {
|
|
|
+ // PtlAgreementCostType 字段
|
|
|
+ FIELD_DESCRIPTION_MAP.put("costType", "费用类型");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("inlet", "入口比例");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("export", "出口比例");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("addThrow", "加投比例");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("keepPointRatio", "留点比例");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("exportKeepPointRatio", "出口留点比例");
|
|
|
+ // PtlAgreementCostRule 字段
|
|
|
+ FIELD_DESCRIPTION_MAP.put("attrCode", "属性编码");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("attrType", "属性类型");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("operator", "操作符");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("min", "左操作数");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("max", "右操作数");
|
|
|
+ // 通用字段
|
|
|
+ FIELD_DESCRIPTION_MAP.put("productName", "险种");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("effectiveTime", "生效时间");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("failureTime", "失效时间");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("priorityLevel", "优先级");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("auditMethod", "审核方式");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("costDescribe", "费用描述");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("approved", "审核状态");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("isValid", "是否有效");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("isEnabled", "是否启用");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("costRules", "费用规则");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("auditTime", "审核时间");
|
|
|
+ FIELD_DESCRIPTION_MAP.put("auditPerson", "审核人");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============ 字典解析器(由外部注入) ============
|
|
|
+ private static DictLabelResolver dictLabelResolver;
|
|
|
+
|
|
|
+ // ============ 字典数据缓存(5 分钟 TTL,避免每条记录重复查 DB) ============
|
|
|
+ private static volatile Map<String, String> cachedAttrNameMap;
|
|
|
+ private static volatile Map<String, Map<String, String>> cachedDictLabelMap;
|
|
|
+ private static volatile long cacheLoadTime = 0;
|
|
|
+ private static final long CACHE_TTL_MS = 5 * 60 * 1000L;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 注入字典解析器(由业务模块在启动时调用)
|
|
|
+ */
|
|
|
+ public static void setDictLabelResolver(DictLabelResolver resolver) {
|
|
|
+ dictLabelResolver = resolver;
|
|
|
+ cacheLoadTime = 0;
|
|
|
+ log.info("DictLabelResolver 已注入");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 刷新字典缓存(当缓存过期或不存在时调用) */
|
|
|
+ private static void refreshDictCacheIfNeeded() {
|
|
|
+ if (System.currentTimeMillis() - cacheLoadTime < CACHE_TTL_MS
|
|
|
+ && cachedAttrNameMap != null && cachedDictLabelMap != null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ synchronized (CompatibleUtils.class) {
|
|
|
+ if (System.currentTimeMillis() - cacheLoadTime < CACHE_TTL_MS
|
|
|
+ && cachedAttrNameMap != null && cachedDictLabelMap != null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ cachedAttrNameMap = loadAttrNameMap();
|
|
|
+ cachedDictLabelMap = loadDictLabelMap();
|
|
|
+ cacheLoadTime = System.currentTimeMillis();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 比较两个 JSON 字符串的字段差异
|
|
|
- *
|
|
|
- * @param operatorTrajectory 操作轨迹对象
|
|
|
- * @return 比较结果列表
|
|
|
*/
|
|
|
public static List<CompareResult> compareFieldsMap(OperatorTrajectory operatorTrajectory) {
|
|
|
String afterJson = operatorTrajectory.getAfterJson();
|
|
|
String source = operatorTrajectory.getBeforeJson();
|
|
|
|
|
|
- // 如果两个字符串均为空,直接返回空列表
|
|
|
if (StringUtils.isEmpty(afterJson) && StringUtils.isEmpty(source)) {
|
|
|
return Collections.emptyList();
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
+ refreshDictCacheIfNeeded();
|
|
|
+ Map<String, String> attrNameMap = cachedAttrNameMap;
|
|
|
+ Map<String, Map<String, String>> dictLabelMap = cachedDictLabelMap;
|
|
|
+
|
|
|
Map<String, Object> oldMap = parseJsonToMap(afterJson);
|
|
|
Map<String, Object> sourceMap = parseJsonToMap(source);
|
|
|
- List<CompareResult> compareResults = compareMaps(oldMap, sourceMap, operatorTrajectory.getEntityName());
|
|
|
+
|
|
|
+ fillMissingSubTableData(oldMap, sourceMap, operatorTrajectory.getEntityName(),
|
|
|
+ operatorTrajectory.getPrimaryValue());
|
|
|
+
|
|
|
+ // ★ 归一化 ruleConditions 中的 min:用 minArray 重建,消除前端/DB格式差异
|
|
|
+ normalizeRuleConditionsMin(oldMap);
|
|
|
+ normalizeRuleConditionsMin(sourceMap);
|
|
|
+
|
|
|
+ List<CompareResult> compareResults = compareMaps(oldMap, sourceMap, operatorTrajectory.getEntityName(), attrNameMap, dictLabelMap);
|
|
|
List<CompareResult> distinctPeople = compareResults.stream()
|
|
|
.collect(Collectors.toMap(
|
|
|
p -> Arrays.asList(p.getFieldName(), p.getFieldContent()),
|
|
|
@@ -63,15 +191,211 @@ public class CompatibleUtils {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 对历史记录:beforeJson 可能缺少子表数据,自动从 DB 补查
|
|
|
+ */
|
|
|
+ private static void fillMissingSubTableData(Map<String, Object> oldMap, Map<String, Object> sourceMap,
|
|
|
+ String entityName, String primaryValue) {
|
|
|
+ if (StringUtils.isEmpty(entityName) || StringUtils.isEmpty(primaryValue)) return;
|
|
|
+
|
|
|
+ String[] classNames = entityName.split(",");
|
|
|
+ if (classNames.length < 2) return;
|
|
|
+
|
|
|
+ String foreignKey = detectForeignKey(classNames);
|
|
|
+ log.warn("[fillSubTable] entityName={}, primaryValue={}, foreignKey={}", entityName, primaryValue, foreignKey);
|
|
|
+
|
|
|
+ Class<?> mainDtoClass = getMainDtoClass(entityName);
|
|
|
+ List<String> missingListFields = new ArrayList<>();
|
|
|
+ if (mainDtoClass != null) {
|
|
|
+ for (Field field : mainDtoClass.getDeclaredFields()) {
|
|
|
+ if (Collection.class.isAssignableFrom(field.getType())) {
|
|
|
+ String fn = field.getName();
|
|
|
+ if (oldMap.containsKey(fn) && !sourceMap.containsKey(fn)) {
|
|
|
+ missingListFields.add(fn);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.warn("[fillSubTable] mainDtoClass={}, missingListFields={}",
|
|
|
+ mainDtoClass != null ? mainDtoClass.getSimpleName() : "null", missingListFields);
|
|
|
+ if (missingListFields.isEmpty()) return;
|
|
|
+
|
|
|
+ List<String> subClassNames = new ArrayList<>();
|
|
|
+ for (int i = 1; i < classNames.length; i++) {
|
|
|
+ subClassNames.add(classNames[i].trim());
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<Integer> usedSubIndices = new HashSet<>();
|
|
|
+ for (String fieldName : missingListFields) {
|
|
|
+ int matchedIdx = findMatchingSubIndex(fieldName, mainDtoClass, subClassNames, usedSubIndices);
|
|
|
+ if (matchedIdx < 0 || matchedIdx >= subClassNames.size()) continue;
|
|
|
+ usedSubIndices.add(matchedIdx);
|
|
|
+
|
|
|
+ String subEntitySimpleName = getSimpleName(subClassNames.get(matchedIdx));
|
|
|
+ String mapperBeanName = Character.toLowerCase(subEntitySimpleName.charAt(0))
|
|
|
+ + subEntitySimpleName.substring(1) + "Mapper";
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (!SpringUtils.containsBean(mapperBeanName)) {
|
|
|
+ log.warn("[fillSubTable] Mapper bean 不存在: {}", mapperBeanName);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Object mapperBean = SpringUtils.getBean(mapperBeanName);
|
|
|
+ Class<?> mapperClass = resolveMapperInterface(mapperBean);
|
|
|
+ if (mapperClass == null) {
|
|
|
+ log.warn("[fillSubTable] 无法解析 Mapper 接口: bean={}, class={}",
|
|
|
+ mapperBeanName, mapperBean.getClass().getName());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ log.warn("[fillSubTable] 查询子表: fieldName={}, mapper={}, foreignKey={}, primaryValue={}",
|
|
|
+ fieldName, mapperClass.getSimpleName(), foreignKey, primaryValue);
|
|
|
+ List<?> subData = GetSelectIdDataUtils.selectListByField(mapperClass, foreignKey, primaryValue);
|
|
|
+ if (subData != null && !subData.isEmpty()) {
|
|
|
+ sourceMap.put(fieldName, subData);
|
|
|
+ log.warn("[fillSubTable] 补查成功: fieldName={}, count={}", fieldName, subData.size());
|
|
|
+ } else {
|
|
|
+ log.warn("[fillSubTable] 查询结果为空: fieldName={}", fieldName);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("[fillSubTable] 补查子表异常: fieldName={}, mapperBean={}, error={}",
|
|
|
+ fieldName, mapperBeanName, e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 归一化 ruleConditions 中的 min 字段
|
|
|
+ * 前端发送 min 为单值、minArray 为完整数组;DB 存储 min 为逗号分隔全量字符串
|
|
|
+ * 用 minArray 重建 min,使 before/after 格式一致,避免误报
|
|
|
+ */
|
|
|
+ private static void normalizeRuleConditionsMin(Map<String, Object> dataMap) {
|
|
|
+ Object ruleConditions = dataMap.get("ruleConditions");
|
|
|
+ if (ruleConditions == null) return;
|
|
|
+ Collection<?> list = toCollection(ruleConditions);
|
|
|
+ if (list == null) return;
|
|
|
+ for (Object item : list) {
|
|
|
+ Map<String, Object> itemMap;
|
|
|
+ if (item instanceof Map) {
|
|
|
+ itemMap = (Map<String, Object>) item;
|
|
|
+ } else if (item instanceof JSONObject) {
|
|
|
+ itemMap = (JSONObject) item;
|
|
|
+ } else {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Object minArray = itemMap.get("minArray");
|
|
|
+ if (minArray instanceof Collection) {
|
|
|
+ Collection<?> coll = (Collection<?>) minArray;
|
|
|
+ if (!coll.isEmpty()) {
|
|
|
+ itemMap.put("min", String.join(",",
|
|
|
+ coll.stream().map(String::valueOf).collect(Collectors.toList())));
|
|
|
+ }
|
|
|
+ } else if (minArray instanceof JSONArray) {
|
|
|
+ JSONArray arr = (JSONArray) minArray;
|
|
|
+ if (!arr.isEmpty()) {
|
|
|
+ itemMap.put("min", String.join(",",
|
|
|
+ arr.stream().map(String::valueOf).collect(Collectors.toList())));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 从子表实体类检测外键字段名 */
|
|
|
+ private static String detectForeignKey(String[] classNames) {
|
|
|
+ try {
|
|
|
+ Class<?> firstSubClass = Class.forName(classNames[1].trim());
|
|
|
+ for (Field field : firstSubClass.getDeclaredFields()) {
|
|
|
+ String fn = field.getName();
|
|
|
+ if (fn.endsWith("Id") && !"id".equals(fn)) {
|
|
|
+ return fn;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {}
|
|
|
+
|
|
|
+ String mainSimpleName = getSimpleName(classNames[0].trim());
|
|
|
+ String baseName = mainSimpleName
|
|
|
+ .replaceAll("End$", "").replaceAll("Po$", "").replaceAll("DTO$", "");
|
|
|
+ if (baseName.length() < mainSimpleName.length()) {
|
|
|
+ return Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1) + "Id";
|
|
|
+ }
|
|
|
+ return Character.toLowerCase(mainSimpleName.charAt(0)) + mainSimpleName.substring(1) + "Id";
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 为字段名匹配子表实体索引 */
|
|
|
+ private static int findMatchingSubIndex(String fieldName, Class<?> mainDtoClass,
|
|
|
+ List<String> subClassNames, Set<Integer> usedIndices) {
|
|
|
+ for (int i = 0; i < subClassNames.size(); i++) {
|
|
|
+ if (usedIndices.contains(i)) continue;
|
|
|
+ String subSimpleName = getSimpleName(subClassNames.get(i));
|
|
|
+ String camelSub = Character.toLowerCase(subSimpleName.charAt(0))
|
|
|
+ + subSimpleName.substring(1);
|
|
|
+ if (fieldName.contains(camelSub)) return i;
|
|
|
+ }
|
|
|
+ if (mainDtoClass != null) {
|
|
|
+ Class<?> genericType = findListGenericType(mainDtoClass, fieldName);
|
|
|
+ if (genericType != null) {
|
|
|
+ String gtName = genericType.getSimpleName().replaceAll("Dto$", "").replaceAll("DTO$", "");
|
|
|
+ for (int i = 0; i < subClassNames.size(); i++) {
|
|
|
+ if (usedIndices.contains(i)) continue;
|
|
|
+ String subSimpleName = getSimpleName(subClassNames.get(i));
|
|
|
+ if (subSimpleName.equals(gtName) || subSimpleName.equals(genericType.getSimpleName())) return i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (int i = 0; i < subClassNames.size(); i++) {
|
|
|
+ if (!usedIndices.contains(i)) return i;
|
|
|
+ }
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 获取类的简单名(去掉包名) */
|
|
|
+ private static String getSimpleName(String className) {
|
|
|
+ return className.contains(".")
|
|
|
+ ? className.substring(className.lastIndexOf('.') + 1)
|
|
|
+ : className;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 Spring 容器中获取的 Mapper bean 对象解析出实际的 Mapper 接口 Class
|
|
|
+ */
|
|
|
+ private static Class<?> resolveMapperInterface(Object mapperBean) {
|
|
|
+ if (mapperBean == null) return null;
|
|
|
+ Class<?> beanClass = mapperBean.getClass();
|
|
|
+ if (java.lang.reflect.Proxy.isProxyClass(beanClass)) {
|
|
|
+ for (Class<?> iface : beanClass.getInterfaces()) {
|
|
|
+ if (iface.getName().contains("Mapper")) {
|
|
|
+ return iface;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (beanClass.isInterface()) {
|
|
|
+ return beanClass;
|
|
|
+ }
|
|
|
+ for (Class<?> iface : beanClass.getInterfaces()) {
|
|
|
+ if (iface.getName().contains("Mapper")) {
|
|
|
+ return iface;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Class<?> superClass = beanClass.getSuperclass();
|
|
|
+ while (superClass != null && superClass != Object.class) {
|
|
|
+ if (superClass.isInterface()) return superClass;
|
|
|
+ for (Class<?> iface : superClass.getInterfaces()) {
|
|
|
+ if (iface.getName().contains("Mapper")) {
|
|
|
+ return iface;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ superClass = superClass.getSuperclass();
|
|
|
+ }
|
|
|
+ return beanClass;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 递归比较两个 Map 的字段差异
|
|
|
- *
|
|
|
- * @param oldMap 新数据 Map
|
|
|
- * @param sourceMap 原始数据 Map
|
|
|
- * @param entityName 实体名称
|
|
|
- * @return 比较结果列表
|
|
|
*/
|
|
|
- private static List<CompareResult> compareMaps(Map<String, Object> oldMap, Map<String, Object> sourceMap, String entityName) {
|
|
|
+ private static List<CompareResult> compareMaps(Map<String, Object> oldMap, Map<String, Object> sourceMap,
|
|
|
+ String entityName,
|
|
|
+ Map<String, String> attrNameMap,
|
|
|
+ Map<String, Map<String, String>> dictLabelMap) {
|
|
|
List<CompareResult> results = new ArrayList<>();
|
|
|
|
|
|
for (Map.Entry<String, Object> entry : oldMap.entrySet()) {
|
|
|
@@ -84,30 +408,484 @@ public class CompatibleUtils {
|
|
|
Object oldData = sourceMap.get(key);
|
|
|
|
|
|
if (isJSONObject(newData)) {
|
|
|
- results.addAll(compareMaps(parseJsonToMap(newData), parseJsonToMap(oldData), entityName));
|
|
|
+ results.addAll(compareMaps(parseJsonToMap(newData), parseJsonToMap(oldData), entityName, attrNameMap, dictLabelMap));
|
|
|
+ } else if (isCollectionType(newData) || isCollectionType(oldData)) {
|
|
|
+ String newValue = formatListToReadable(newData, entityName, key, attrNameMap, dictLabelMap);
|
|
|
+ String oldValue = formatListToReadable(oldData, entityName, key, attrNameMap, dictLabelMap);
|
|
|
+ String displayOld = (oldValue == null || oldValue.isEmpty()) ? "无" : oldValue;
|
|
|
+ String displayNew = (newValue == null || newValue.isEmpty()) ? "无" : newValue;
|
|
|
+ if (!Objects.equals(displayOld, displayNew)) {
|
|
|
+ results.add(buildCompareResult(entityName, key, displayOld, displayNew));
|
|
|
+ }
|
|
|
} else {
|
|
|
String newValue = getValue(newData);
|
|
|
String oldValue = getValue(oldData);
|
|
|
-
|
|
|
if (StringUtils.isNotEmpty(newValue) && !Objects.equals(oldValue, newValue)) {
|
|
|
- CompareResult compareResult = new CompareResult();
|
|
|
- compareResult.setName(SchemaAnnotationUtils.getSchemaAnnotation(entityName, key));
|
|
|
- compareResult.setFieldName(key);
|
|
|
- compareResult.setFieldContent(oldValue);
|
|
|
- compareResult.setNewFieldContent(newValue);
|
|
|
- compareResult.setHandedType(""); // 默认值,可根据需求调整
|
|
|
- results.add(compareResult);
|
|
|
+ String displayOld = (oldValue == null) ? "(空)" : oldValue;
|
|
|
+ results.add(buildCompareResult(entityName, key, displayOld, newValue));
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return results;
|
|
|
}
|
|
|
|
|
|
+ /** 构建 CompareResult 公共方法 */
|
|
|
+ private static CompareResult buildCompareResult(String entityName, String key, String oldValue, String newValue) {
|
|
|
+ CompareResult cr = new CompareResult();
|
|
|
+ cr.setName(SchemaAnnotationUtils.getSchemaAnnotation(entityName, key));
|
|
|
+ cr.setFieldName(key);
|
|
|
+ cr.setFieldContent(oldValue);
|
|
|
+ cr.setNewFieldContent(newValue);
|
|
|
+ cr.setHandedType("");
|
|
|
+ return cr;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 判断是否为集合/数组类型 */
|
|
|
+ private static boolean isCollectionType(Object obj) {
|
|
|
+ if (obj == null) return false;
|
|
|
+ return obj instanceof JSONArray || obj instanceof Collection || obj.getClass().isArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将 List 格式化为可读的多行格式
|
|
|
+ */
|
|
|
+ private static String formatListToReadable(Object listObj, String entityName, String fieldName,
|
|
|
+ Map<String, String> attrNameMap,
|
|
|
+ Map<String, Map<String, String>> dictLabelMap) {
|
|
|
+ if (listObj == null) return null;
|
|
|
+ if (!isCollectionType(listObj)) return null;
|
|
|
+
|
|
|
+ Collection<?> list = toCollection(listObj);
|
|
|
+ if (list == null || list.isEmpty()) return null;
|
|
|
+
|
|
|
+ Class<?> mainDtoClass = getMainDtoClass(entityName);
|
|
|
+ Class<?> dtoInnerClass = findListGenericType(mainDtoClass, fieldName);
|
|
|
+ Class<?> subEntityClass = dtoInnerClass != null ? dtoInnerClass : findEntityClass(entityName, fieldName);
|
|
|
+
|
|
|
+ StringBuilder result = new StringBuilder();
|
|
|
+ int count = 0;
|
|
|
+ for (Object item : list) {
|
|
|
+ if (count > 0) result.append("<br>---<br>");
|
|
|
+ result.append(formatItemToReadable(item, subEntityClass, attrNameMap, dictLabelMap));
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+ return result.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取 entityName 中的第一个类(主表DTO类)
|
|
|
+ */
|
|
|
+ private static Class<?> getMainDtoClass(String entityName) {
|
|
|
+ if (StringUtils.isEmpty(entityName)) return null;
|
|
|
+ String firstClassName = entityName.split(",")[0].trim();
|
|
|
+ try {
|
|
|
+ return Class.forName(firstClassName);
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
+ log.warn("主表DTO类未找到: {}", firstClassName);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从DTO类中找到List字段的泛型类型
|
|
|
+ */
|
|
|
+ private static Class<?> findListGenericType(Class<?> dtoClass, String fieldName) {
|
|
|
+ if (dtoClass == null) return null;
|
|
|
+ try {
|
|
|
+ Field field = dtoClass.getDeclaredField(fieldName);
|
|
|
+ Type genericType = field.getGenericType();
|
|
|
+ if (genericType instanceof ParameterizedType) {
|
|
|
+ ParameterizedType pt = (ParameterizedType) genericType;
|
|
|
+ Type[] typeArgs = pt.getActualTypeArguments();
|
|
|
+ if (typeArgs.length > 0 && typeArgs[0] instanceof Class) {
|
|
|
+ return (Class<?>) typeArgs[0];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (NoSuchFieldException e) {
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将单个对象格式化为可读格式
|
|
|
+ * 支持字典值转换,支持展开 List 字段
|
|
|
+ */
|
|
|
+ private static String formatItemToReadable(Object item, Class<?> entityClass,
|
|
|
+ Map<String, String> attrNameMap,
|
|
|
+ Map<String, Map<String, String>> dictLabelMap) {
|
|
|
+ if (item == null) return "";
|
|
|
+
|
|
|
+ Map<String, Object> itemMap;
|
|
|
+ if (item instanceof Map) {
|
|
|
+ itemMap = (Map<String, Object>) item;
|
|
|
+ } else if (item instanceof JSONObject) {
|
|
|
+ itemMap = (JSONObject) item;
|
|
|
+ } else {
|
|
|
+ try {
|
|
|
+ String jsonStr = JSON.toJSONString(item);
|
|
|
+ if (jsonStr != null && jsonStr.trim().startsWith("{")) {
|
|
|
+ itemMap = JSONObject.parseObject(jsonStr, Map.class);
|
|
|
+ } else {
|
|
|
+ itemMap = new HashMap<>();
|
|
|
+ itemMap.put("value", String.valueOf(item));
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ itemMap = new HashMap<>();
|
|
|
+ itemMap.put("value", String.valueOf(item));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, String> codeToNameMap = attrNameMap;
|
|
|
+ Map<String, Map<String, String>> codeDictMap = dictLabelMap;
|
|
|
+
|
|
|
+ // ★ RuleConditionDto:不显示字段标签,只显示值;按固定顺序输出
|
|
|
+ boolean showLabels = entityClass == null || !"RuleConditionDto".equals(entityClass.getSimpleName());
|
|
|
+
|
|
|
+ // RuleConditionDto 字段显示顺序:attrCode → operator → min → max
|
|
|
+ String[] ruleConditionFieldOrder = {"attrCode", "operator", "min", "max"};
|
|
|
+
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ int fieldCount = 0;
|
|
|
+
|
|
|
+ // 决定迭代顺序:RuleConditionDto 按固定顺序,其他按 Map 自然顺序
|
|
|
+ Iterable<Map.Entry<String, Object>> entries;
|
|
|
+ if (!showLabels) {
|
|
|
+ // 按预定义顺序构造 entries
|
|
|
+ List<Map.Entry<String, Object>> ordered = new ArrayList<>();
|
|
|
+ for (String key : ruleConditionFieldOrder) {
|
|
|
+ if (itemMap.containsKey(key)) {
|
|
|
+ ordered.add(new AbstractMap.SimpleEntry<>(key, itemMap.get(key)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ entries = ordered;
|
|
|
+ } else {
|
|
|
+ entries = itemMap.entrySet();
|
|
|
+ }
|
|
|
+
|
|
|
+ for (Map.Entry<String, Object> entry : entries) {
|
|
|
+ String fieldKey = entry.getKey();
|
|
|
+ Object fieldValue = entry.getValue();
|
|
|
+
|
|
|
+ if ("id".equals(fieldKey) || fieldValue == null) continue;
|
|
|
+ if (IGNORED_FIELDS.contains(fieldKey)) continue;
|
|
|
+ if (fieldValue instanceof Map || fieldValue instanceof JSONObject) continue;
|
|
|
+ // ★ RuleConditionDto:跳过前端DTO与DB结构不一致的元数据字段,避免误报
|
|
|
+ // min 已通过 normalizeRuleConditionsMin 归一化,可以正常显示和对比
|
|
|
+ if (!showLabels && ("dictLabal".equals(fieldKey) || "attrType".equals(fieldKey)
|
|
|
+ || "minArray".equals(fieldKey))) continue;
|
|
|
+
|
|
|
+ String description = getFieldDescription(entityClass, fieldKey);
|
|
|
+
|
|
|
+ if (fieldValue instanceof List) {
|
|
|
+ String listStr = formatDictLabelList((List<?>) fieldValue);
|
|
|
+ if (listStr != null && !listStr.isEmpty()) {
|
|
|
+ if (fieldCount > 0) sb.append(showLabels ? "<br> " : " ");
|
|
|
+ if (showLabels) {
|
|
|
+ sb.append(description).append(": ");
|
|
|
+ }
|
|
|
+ sb.append(listStr);
|
|
|
+ fieldCount++;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ String displayValue = convertDictValue(fieldKey, fieldValue, codeToNameMap, codeDictMap);
|
|
|
+
|
|
|
+ if (fieldCount > 0) sb.append(showLabels ? "<br> " : " ");
|
|
|
+ if (showLabels) {
|
|
|
+ sb.append(description).append(": ");
|
|
|
+ }
|
|
|
+ sb.append(displayValue);
|
|
|
+ fieldCount++;
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载属性映射(attrCode → attrName)
|
|
|
+ */
|
|
|
+ private static Map<String, String> loadAttrNameMap() {
|
|
|
+ if (dictLabelResolver != null) {
|
|
|
+ return dictLabelResolver.getAttrNameMap();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ Class<?> serviceClass = Class.forName(
|
|
|
+ "com.jzg.organization.service.PtlAgreementUndwrtRulesAttrService"
|
|
|
+ );
|
|
|
+ Object service = SpringUtils.getBean(serviceClass);
|
|
|
+ Method listMethod = serviceClass.getMethod("list");
|
|
|
+ List<?> list = (List<?>) listMethod.invoke(service);
|
|
|
+
|
|
|
+ Map<String, String> result = new HashMap<>();
|
|
|
+ for (Object item : list) {
|
|
|
+ String attrCode = (String) item.getClass().getMethod("getAttrCode").invoke(item);
|
|
|
+ String attrName = (String) item.getClass().getMethod("getAttrName").invoke(item);
|
|
|
+ result.put(attrCode, attrName);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("通过反射加载属性映射失败: {}", e.getMessage());
|
|
|
+ return new HashMap<>();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载字典标签映射(dictId → {code → name})
|
|
|
+ */
|
|
|
+ private static Map<String, Map<String, String>> loadDictLabelMap() {
|
|
|
+ if (dictLabelResolver != null) {
|
|
|
+ return dictLabelResolver.getDictLabelMap();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ Class<?> serviceClass = Class.forName(
|
|
|
+ "com.jzg.organization.service.PtlAgreementUndwrtRulesDictLabalService"
|
|
|
+ );
|
|
|
+ Object service = SpringUtils.getBean(serviceClass);
|
|
|
+ Method listMethod = serviceClass.getMethod("list");
|
|
|
+ List<?> list = (List<?>) listMethod.invoke(service);
|
|
|
+
|
|
|
+ Map<String, Map<String, String>> result = new HashMap<>();
|
|
|
+ for (Object item : list) {
|
|
|
+ Object dictIdObj = item.getClass().getMethod("getDictId").invoke(item);
|
|
|
+ String dictId = String.valueOf(dictIdObj);
|
|
|
+ String code = (String) item.getClass().getMethod("getCode").invoke(item);
|
|
|
+ String name = (String) item.getClass().getMethod("getName").invoke(item);
|
|
|
+
|
|
|
+ result.computeIfAbsent(dictId, k -> new HashMap<>())
|
|
|
+ .put(code, name);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("通过反射加载字典标签映射失败: {}", e.getMessage());
|
|
|
+ return new HashMap<>();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化字典标签列表,显示为 "name, name"
|
|
|
+ * 支持两种元素类型:
|
|
|
+ * 1. 字典对象(Map/JSONObject,含 code/name)→ 取 name
|
|
|
+ * 2. 纯字符串/数值(如 "05")→ 通过字典转换code为label
|
|
|
+ */
|
|
|
+ private static String formatDictLabelList(List<?> dictList) {
|
|
|
+ if (dictList == null || dictList.isEmpty()) return null;
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (Object dictItem : dictList) {
|
|
|
+ String label;
|
|
|
+ if (dictItem instanceof Map || dictItem instanceof JSONObject) {
|
|
|
+ Map<String, Object> dictMap = toDictMap(dictItem);
|
|
|
+ if (dictMap == null) continue;
|
|
|
+ label = String.valueOf(dictMap.getOrDefault("name", ""));
|
|
|
+ } else {
|
|
|
+ // ★ 纯字符串/数值元素:当作 dict code,尝试通过所有字典转换为 label
|
|
|
+ String code = String.valueOf(dictItem);
|
|
|
+ label = code; // 默认显示原始值
|
|
|
+ if (cachedDictLabelMap != null) {
|
|
|
+ for (Map<String, String> dictValues : cachedDictLabelMap.values()) {
|
|
|
+ if (dictValues.containsKey(code)) {
|
|
|
+ label = dictValues.get(code);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (sb.length() > 0) sb.append(", ");
|
|
|
+ sb.append(label);
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将字典项对象安全地转为 Map(提取 code/name)
|
|
|
+ */
|
|
|
+ private static Map<String, Object> toDictMap(Object dictItem) {
|
|
|
+ if (dictItem == null) return null;
|
|
|
+ if (dictItem instanceof Map) {
|
|
|
+ return (Map<String, Object>) dictItem;
|
|
|
+ }
|
|
|
+ if (dictItem instanceof Collection || dictItem instanceof JSONArray) {
|
|
|
+ Collection<?> coll = (dictItem instanceof Collection)
|
|
|
+ ? (Collection<?>) dictItem
|
|
|
+ : (JSONArray) dictItem;
|
|
|
+ if (coll.isEmpty()) return null;
|
|
|
+ return toDictMap(coll.iterator().next());
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String jsonStr = JSON.toJSONString(dictItem);
|
|
|
+ if (jsonStr == null) return null;
|
|
|
+ jsonStr = jsonStr.trim();
|
|
|
+ if (jsonStr.startsWith("[")) {
|
|
|
+ JSONArray arr = JSONArray.parseArray(jsonStr);
|
|
|
+ if (arr.isEmpty()) return null;
|
|
|
+ return toDictMap(arr.get(0));
|
|
|
+ }
|
|
|
+ if (jsonStr.startsWith("{")) {
|
|
|
+ return JSONObject.parseObject(jsonStr, Map.class);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("字典项解析失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将技术码值转换为可读的中文描述
|
|
|
+ */
|
|
|
+ private static String convertDictValue(String fieldKey, Object rawValue,
|
|
|
+ Map<String, String> codeToNameMap,
|
|
|
+ Map<String, Map<String, String>> codeDictMap) {
|
|
|
+ if (rawValue == null) return "";
|
|
|
+ String value = String.valueOf(rawValue);
|
|
|
+ // ★ 归一化数值:9.00 → 9,避免 BigDecimal 精度差异导致误报
|
|
|
+ if (rawValue instanceof Number || value.matches("-?\\d+\\.\\d+")) {
|
|
|
+ try {
|
|
|
+ value = new java.math.BigDecimal(value).stripTrailingZeros().toPlainString();
|
|
|
+ } catch (NumberFormatException ignored) { }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1) 操作符
|
|
|
+ if ("operator".equals(fieldKey)) {
|
|
|
+ return OPERATOR_MAP.getOrDefault(value, value);
|
|
|
+ }
|
|
|
+ // 2) 属性类型
|
|
|
+ if ("attrType".equals(fieldKey)) {
|
|
|
+ return ATTR_TYPE_MAP.getOrDefault(value, value);
|
|
|
+ }
|
|
|
+ // 3) attrCode → 转成 attrName
|
|
|
+ if ("attrCode".equals(fieldKey)) {
|
|
|
+ return codeToNameMap.getOrDefault(value, value);
|
|
|
+ }
|
|
|
+ // 4) costType → 转成中文
|
|
|
+ if ("costType".equals(fieldKey)) {
|
|
|
+ return COST_TYPE_MAP.getOrDefault(value, value);
|
|
|
+ }
|
|
|
+ // 5) min / max → 在所有字典中查找匹配的 code
|
|
|
+ if ("min".equals(fieldKey) || "max".equals(fieldKey)) {
|
|
|
+ if (value.contains(",")) {
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (String v : value.split(",")) {
|
|
|
+ String vTrim = v.trim();
|
|
|
+ String cn = vTrim;
|
|
|
+ for (Map.Entry<String, Map<String, String>> dictEntry : codeDictMap.entrySet()) {
|
|
|
+ Map<String, String> dictValues = dictEntry.getValue();
|
|
|
+ if (dictValues.containsKey(vTrim)) {
|
|
|
+ cn = dictValues.get(vTrim);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (sb.length() > 0) sb.append(",");
|
|
|
+ sb.append(cn);
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+ for (Map.Entry<String, Map<String, String>> dictEntry : codeDictMap.entrySet()) {
|
|
|
+ Map<String, String> dictValues = dictEntry.getValue();
|
|
|
+ if (dictValues.containsKey(value)) {
|
|
|
+ return dictValues.get(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6) 非字典字段直接返回原始值,不做通用字典匹配
|
|
|
+ // 避免数值字段(如 inlet=2)被其他字典的 code 错误匹配
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 entityName 中找到包含指定字段的实体类
|
|
|
+ */
|
|
|
+ private static Class<?> findEntityClass(String entityName, String fieldName) {
|
|
|
+ if (StringUtils.isEmpty(entityName)) return null;
|
|
|
+ String[] classNames = entityName.split(",");
|
|
|
+ for (String className : classNames) {
|
|
|
+ try {
|
|
|
+ Class<?> clazz = Class.forName(className.trim());
|
|
|
+ try {
|
|
|
+ clazz.getDeclaredField(fieldName);
|
|
|
+ return clazz;
|
|
|
+ } catch (NoSuchFieldException e) {
|
|
|
+ }
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取字段的 @Schema description
|
|
|
+ */
|
|
|
+ private static String getFieldDescription(Class<?> entityClass, String fieldName) {
|
|
|
+ if (entityClass != null) {
|
|
|
+ String desc = getSchemaFromClass(entityClass, fieldName);
|
|
|
+ if (desc != null) return desc;
|
|
|
+ }
|
|
|
+ // 回退:使用内置字段描述映射
|
|
|
+ if (FIELD_DESCRIPTION_MAP.containsKey(fieldName)) {
|
|
|
+ return FIELD_DESCRIPTION_MAP.get(fieldName);
|
|
|
+ }
|
|
|
+ return fieldName;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从指定类(包括内部类)获取字段的Schema描述
|
|
|
+ */
|
|
|
+ private static String getSchemaFromClass(Class<?> clazz, String fieldName) {
|
|
|
+ try {
|
|
|
+ Field field = clazz.getDeclaredField(fieldName);
|
|
|
+ Schema schema = field.getAnnotation(Schema.class);
|
|
|
+ if (schema != null && StringUtils.isNotEmpty(schema.description())) {
|
|
|
+ return cleanSchemaDescription(schema.description());
|
|
|
+ }
|
|
|
+ } catch (NoSuchFieldException e) {
|
|
|
+ }
|
|
|
+ Class<?>[] innerClasses = clazz.getDeclaredClasses();
|
|
|
+ for (Class<?> inner : innerClasses) {
|
|
|
+ try {
|
|
|
+ Field field = inner.getDeclaredField(fieldName);
|
|
|
+ Schema schema = field.getAnnotation(Schema.class);
|
|
|
+ if (schema != null && StringUtils.isNotEmpty(schema.description())) {
|
|
|
+ return cleanSchemaDescription(schema.description());
|
|
|
+ }
|
|
|
+ } catch (NoSuchFieldException e) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清理 @Schema description,截取中文标签部分,去掉后面的枚举值说明
|
|
|
+ * 例如 "费用类型1-交强险2-商业险3-驾意险" → "费用类型"
|
|
|
+ * "审核方式0-自动审核;1-手动审核" → "审核方式"
|
|
|
+ * "是否有效0有效1-失效" → "是否有效"
|
|
|
+ */
|
|
|
+ private static String cleanSchemaDescription(String description) {
|
|
|
+ if (description == null || description.isEmpty()) return description;
|
|
|
+ // 截取到第一个数字之前(数字通常是枚举值说明的开始)
|
|
|
+ int idx = -1;
|
|
|
+ for (int i = 0; i < description.length(); i++) {
|
|
|
+ if (Character.isDigit(description.charAt(i))) {
|
|
|
+ idx = i;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (idx > 0) {
|
|
|
+ return description.substring(0, idx).trim();
|
|
|
+ }
|
|
|
+ return description;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将不同类型的集合统一转为 Collection
|
|
|
+ */
|
|
|
+ private static Collection<?> toCollection(Object obj) {
|
|
|
+ if (obj instanceof Collection) return (Collection<?>) obj;
|
|
|
+ if (obj instanceof JSONArray) return ((JSONArray) obj);
|
|
|
+ if (obj.getClass().isArray()) return Arrays.asList((Object[]) obj);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 判断对象是否为 JSONObject
|
|
|
- *
|
|
|
- * @param obj 对象
|
|
|
- * @return 是否为 JSONObject
|
|
|
*/
|
|
|
public static boolean isJSONObject(Object obj) {
|
|
|
return obj instanceof JSONObject;
|
|
|
@@ -115,28 +893,97 @@ public class CompatibleUtils {
|
|
|
|
|
|
/**
|
|
|
* 将 JSON 字符串解析为 Map
|
|
|
- *
|
|
|
- * @param json JSON 字符串
|
|
|
- * @return 解析后的 Map
|
|
|
*/
|
|
|
private static Map<String, Object> parseJsonToMap(Object json) {
|
|
|
if (ObjectUtil.isEmpty(json)) {
|
|
|
- return Collections.emptyMap();
|
|
|
+ return new HashMap<>();
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- return JSONObject.parseObject(JSONObject.toJSONString(json), Map.class);
|
|
|
+ Map<String, Object> result;
|
|
|
+ if (json instanceof String) {
|
|
|
+ result = JSONObject.parseObject((String) json, Map.class);
|
|
|
+ } else {
|
|
|
+ result = JSONObject.parseObject(JSONObject.toJSONString(json), Map.class);
|
|
|
+ }
|
|
|
+ // ★ 防御性处理:解析 FastJSON2 产生的 $ref 循环引用
|
|
|
+ // 当 Map 中仍存在 {"$ref":"..."} 未解析条目时,手动按 JSON 路径回填实际数据
|
|
|
+ resolveRefEntries(result, result);
|
|
|
+ return result;
|
|
|
} catch (Exception e) {
|
|
|
log.warn("JSON解析失败: {}", e.getMessage());
|
|
|
- return Collections.emptyMap();
|
|
|
+ return new HashMap<>();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 递归遍历 Map,将 FastJSON2 的 $ref 占位替换为实际引用数据
|
|
|
+ */
|
|
|
+ private static void resolveRefEntries(Map<String, Object> map, Map<String, Object> root) {
|
|
|
+ if (map == null) return;
|
|
|
+ for (Map.Entry<String, Object> entry : new ArrayList<>(map.entrySet())) {
|
|
|
+ Object value = entry.getValue();
|
|
|
+ if (value instanceof Map) {
|
|
|
+ Map<String, Object> subMap = (Map<String, Object>) value;
|
|
|
+ if (subMap.containsKey("$ref")) {
|
|
|
+ String refPath = String.valueOf(subMap.get("$ref"));
|
|
|
+ Object resolved = navigateJsonPath(root, refPath);
|
|
|
+ if (resolved != null) {
|
|
|
+ map.put(entry.getKey(), resolved);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ resolveRefEntries(subMap, root);
|
|
|
+ }
|
|
|
+ } else if (value instanceof Collection) {
|
|
|
+ for (Object item : (Collection<?>) value) {
|
|
|
+ if (item instanceof Map) {
|
|
|
+ resolveRefEntries((Map<String, Object>) item, root);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按 FastJSON2 的 JSON 路径(如 $.costAddList[0].ruleConditions)从 root 中取值
|
|
|
+ */
|
|
|
+ private static Object navigateJsonPath(Map<String, Object> root, String path) {
|
|
|
+ if (path == null || !path.startsWith("$.")) return null;
|
|
|
+ String remaining = path.substring(2); // 去掉 "$."
|
|
|
+ Object current = root;
|
|
|
+ for (String token : remaining.split("\\.")) {
|
|
|
+ if (current == null) return null;
|
|
|
+ // 处理数组下标:如 costAddList[0]
|
|
|
+ int bracketIdx = token.indexOf('[');
|
|
|
+ if (bracketIdx >= 0) {
|
|
|
+ String fieldName = token.substring(0, bracketIdx);
|
|
|
+ String indexStr = token.substring(bracketIdx + 1, token.indexOf(']'));
|
|
|
+ int index = Integer.parseInt(indexStr);
|
|
|
+ if (current instanceof Map) {
|
|
|
+ Object arr = ((Map<String, Object>) current).get(fieldName);
|
|
|
+ if (arr instanceof List) {
|
|
|
+ List<?> list = (List<?>) arr;
|
|
|
+ current = (index < list.size()) ? list.get(index) : null;
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (current instanceof Map) {
|
|
|
+ current = ((Map<String, Object>) current).get(token);
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return current;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 获取对象的字符串表示形式
|
|
|
- *
|
|
|
- * @param obj 对象
|
|
|
- * @return 字符串表示形式
|
|
|
+ * 数值类型会归一化(如 9.00 → 9),避免 BigDecimal 精度差异导致误报
|
|
|
*/
|
|
|
public static String getValue(Object obj) {
|
|
|
if (obj == null) {
|
|
|
@@ -144,19 +991,45 @@ public class CompatibleUtils {
|
|
|
}
|
|
|
|
|
|
if (obj instanceof String) {
|
|
|
- return (String) obj;
|
|
|
- } else if (obj instanceof Number) {
|
|
|
- return String.valueOf(obj);
|
|
|
- } else if (obj instanceof Boolean) {
|
|
|
- return String.valueOf(obj);
|
|
|
+ String strVal = (String) obj;
|
|
|
+ if (strVal.matches("^\\d{13}$")) {
|
|
|
+ try {
|
|
|
+ long ts = Long.parseLong(strVal);
|
|
|
+ if (ts >= 946684800000L && ts <= 4102444800000L) {
|
|
|
+ return LocalDateTime.ofInstant(
|
|
|
+ java.time.Instant.ofEpochMilli(ts),
|
|
|
+ java.time.ZoneId.systemDefault()
|
|
|
+ ).format(DATE_TIME_FORMATTER);
|
|
|
+ }
|
|
|
+ } catch (NumberFormatException ignored) { }
|
|
|
+ }
|
|
|
+ // ★ 归一化数值字符串:9.00 → 9, 3.10 → 3.1,避免 BigDecimal 精度差异导致误报
|
|
|
+ if (strVal.matches("-?\\d+\\.?0*")) {
|
|
|
+ try {
|
|
|
+ return new java.math.BigDecimal(strVal).stripTrailingZeros().toPlainString();
|
|
|
+ } catch (NumberFormatException ignored) { }
|
|
|
+ }
|
|
|
+ return strVal;
|
|
|
} else if (obj instanceof LocalDateTime) {
|
|
|
return ((LocalDateTime) obj).format(DATE_TIME_FORMATTER);
|
|
|
} else if (obj instanceof LocalDate) {
|
|
|
return ((LocalDate) obj).format(DATE_FORMATTER);
|
|
|
+ } else if (obj instanceof java.math.BigDecimal) {
|
|
|
+ // ★ BigDecimal 归一化:去掉尾部零(9.00 → 9)
|
|
|
+ return ((java.math.BigDecimal) obj).stripTrailingZeros().toPlainString();
|
|
|
+ } else if (obj instanceof Number) {
|
|
|
+ long longVal = ((Number) obj).longValue();
|
|
|
+ if (longVal >= 946684800000L && longVal <= 4102444800000L) {
|
|
|
+ return LocalDateTime.ofInstant(
|
|
|
+ java.time.Instant.ofEpochMilli(longVal),
|
|
|
+ java.time.ZoneId.systemDefault()
|
|
|
+ ).format(DATE_TIME_FORMATTER);
|
|
|
+ }
|
|
|
+ return String.valueOf(obj);
|
|
|
+ } else if (obj instanceof Boolean) {
|
|
|
+ return String.valueOf(obj);
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
-
|
|
|
-}
|
|
|
-
|
|
|
+}
|