Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

Qchen 1 месяц назад
Родитель
Сommit
d1a012c478
17 измененных файлов с 1766 добавлено и 119 удалено
  1. 22 6
      commons/src/main/java/com/jzg/commons/aop/OperatorTrajectory.java
  2. 295 14
      commons/src/main/java/com/jzg/commons/aop/aspect/OperatorTrajectoryAspect.java
  3. 1 1
      commons/src/main/java/com/jzg/commons/entity/dto/CostAdd.java
  4. 2 1
      commons/src/main/java/com/jzg/commons/entity/dto/PtlAgreementCostEnd.java
  5. 8 0
      commons/src/main/java/com/jzg/commons/service/impl/OperatorTrajectoryServiceImpl.java
  6. 2 2
      commons/src/main/java/com/jzg/commons/util/BuildDescUtils.java
  7. 255 19
      commons/src/main/java/com/jzg/commons/util/GetSelectIdDataUtils.java
  8. 915 42
      commons/src/main/java/com/jzg/commons/util/comparble/CompatibleUtils.java
  9. 22 0
      commons/src/main/java/com/jzg/commons/util/comparble/DictLabelResolver.java
  10. 90 14
      commons/src/main/java/com/jzg/commons/util/comparble/SchemaAnnotationUtils.java
  11. 51 0
      tenant/insurance/quotation-dadi/src/main/java/com/jzg/quotation/dadi/crawler/build/DaDiCrawlerQuoteResultVoBuild.java
  12. 1 1
      tenant/insurance/quotation-hengbang/src/main/java/com/jzg/quotation/hengbang/crawler/build/HbCrawlerQuoteResultVoBuild.java
  13. 4 0
      tenant/insurance/quotation-renbaonew/src/main/java/com/jzg/quotation/renbaonew/crawler/entity/response/RenBaoNewCrawlerQuoteResponse.java
  14. 67 15
      tenant/insurance/quotation-renbaonew/src/main/java/com/jzg/quotation/renbaonew/crawler/service/impl/RenBaoNewCrawlerRequestBuilder.java
  15. 20 0
      tenant/insurance/quotation-renbaonew/src/main/java/com/jzg/quotation/renbaonew/crawler/service/impl/RenBaoNewCrawlerRequestImpl.java
  16. 7 3
      tenant/organization/src/main/java/com/jzg/organization/controller/PtlAgreementCostController.java
  17. 4 1
      tenant/organization/src/main/java/com/jzg/organization/service/impl/PtlAgreementCostServiceImpl.java

+ 22 - 6
commons/src/main/java/com/jzg/commons/aop/OperatorTrajectory.java

@@ -2,10 +2,7 @@ package com.jzg.commons.aop;
 
 import com.jzg.commons.constants.OperationTypeEnum;
 
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
+import java.lang.annotation.*;
 
 /**
  * 操作轨迹注解
@@ -30,13 +27,32 @@ public @interface OperatorTrajectory {
     //是否记录日志
     String isRecorded() default "";
 
-    Class<?> entityClass();
+    Class<?>[] entityClass();
 
-    Class<?> mapperClass();
+    Class<?>[] mapperClass();
 
     OperationTypeEnum OperationDict() default OperationTypeEnum.SAVE;
 
     String  enableDisableDict() default "";
+
+    /**
+     * 子表数据在DTO中的字段名数组(按entityClass数组顺序,跳过第0个主表)
+     * 示例:entityClass = {PtlAgreementCostEnd.class, PtlAgreementCostType.class, PtlAgreementCostRule.class}
+     *       subFieldNames = {"ptlAgreementCostTypeList", "ruleConditions"}
+     */
+    String[] subFieldNames() default {};
+
+    /**
+     * 子表外键字段名(用于按主表ID查询子表历史数据)
+     * 示例:ptlAgreementCostId
+     */
+    String subForeignKey() default "";
+    /**
+     * 关联表字段补全配置:用于从关联表中补充字段到变更前数据中
+     * 格式:mapperClassName:foreignKey:srcField1:dstField1,srcField2:dstField2
+     * 示例:com.jzg.organization.mapper.PtlAgreementMapper:ptlAgreementId:agreementCode:ptlAgreementCode,agreementTitle:ptlAgreementName
+     */
+    String enrichConfig() default "";
 }
 
 

+ 295 - 14
commons/src/main/java/com/jzg/commons/aop/aspect/OperatorTrajectoryAspect.java

@@ -2,7 +2,9 @@ package com.jzg.commons.aop.aspect;
 
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.serializer.SerializerFeature;
 import com.jzg.commons.aop.OperatorTrajectory;
 import com.jzg.commons.async.AsyncFactory;
 import com.jzg.commons.async.AsyncManager;
@@ -61,7 +63,7 @@ public class OperatorTrajectoryAspect {
         try {
             MethodContext context = methodContext.get();
             if (context == null) {
-                log.warn("Method context is null for pointcut: {}", joinPoint.getSignature());
+                log.warn("[轨迹] doAfter: Method context is null for pointcut: {}", joinPoint.getSignature());
                 return;
             }
             Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
@@ -82,11 +84,23 @@ public class OperatorTrajectoryAspect {
             }
             if (result.getCode() == 200) {
                 if ("C".equals(byId.getOperatorType())) {
-                    byId.setPrimaryValue(getId(joinPoint, annotation.primary()));
+                    String primaryVal = getId(joinPoint, annotation.primary());
+                    // ★ 优先从响应数据获取服务端生成的ID(如费用记录主键)
+                    if (StringUtils.isBlank(primaryVal) && result.getData() instanceof String
+                            && StringUtils.isNotBlank((String) result.getData())) {
+                        primaryVal = (String) result.getData();
+                        log.warn("[轨迹] doAfter: 从响应数据提取 primaryValue={}", primaryVal);
+                    }
+                    // ★ 嵌套 DTO 回退:从已保存的 afterJson 中递归提取
+                    if (StringUtils.isBlank(primaryVal) && StringUtils.isNotBlank(byId.getAfterJson())) {
+                        primaryVal = extractPrimaryFromJson(byId.getAfterJson(), annotation.primary());
+                    }
+                    byId.setPrimaryValue(primaryVal);
+                    log.warn("[轨迹] doAfter: type=C, primaryValue={}", primaryVal);
                 }
-                byId.setExecuteStatus(1); // 成功状态
+                byId.setExecuteStatus(1);
             } else {
-                byId.setExecuteStatus(2); // 失败状态
+                byId.setExecuteStatus(2);
                 byId.setErrorMsg(result.getMsg());
             }
             operatorTrajectoryService.updateById(byId);
@@ -119,7 +133,7 @@ public class OperatorTrajectoryAspect {
                 return;
             }
 
-            byId.setExecuteStatus(2); // 失败状态
+            byId.setExecuteStatus(2);
             byId.setErrorMsg(ex.getMessage());
             operatorTrajectoryService.updateById(byId);
         } catch (Exception e) {
@@ -150,9 +164,51 @@ public class OperatorTrajectoryAspect {
         return null;
     }
 
+    /**
+     * 从 JSON 字符串中递归搜索目标字段值
+     * 用于处理嵌套 DTO 结构(如 costAddList[0].ptlAgreementId)
+     */
+    private String extractPrimaryFromJson(String jsonStr, String key) {
+        if (StringUtils.isBlank(jsonStr)) return null;
+        try {
+            Object parsed = JSON.parse(jsonStr);
+            return extractFromObject(parsed, key);
+        } catch (Exception e) {
+            log.warn("[轨迹] extractPrimaryFromJson 异常: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    private String extractFromObject(Object obj, String key) {
+        if (obj instanceof JSONObject) {
+            JSONObject jsonObject = (JSONObject) obj;
+            // 先尝试当前层
+            String val = jsonObject.getString(key);
+            if (StringUtils.isNotBlank(val)) return val;
+            // 再递归子字段
+            for (Object value : jsonObject.values()) {
+                String found = extractFromObject(value, key);
+                if (found != null) return found;
+            }
+        } else if (obj instanceof JSONArray) {
+            JSONArray arr = (JSONArray) obj;
+            for (int i = 0; i < arr.size(); i++) {
+                String found = extractFromObject(arr.get(i), key);
+                if (found != null) return found;
+            }
+        } else if (obj instanceof Collection) {
+            for (Object item : (Collection<?>) obj) {
+                String found = extractFromObject(item, key);
+                if (found != null) return found;
+            }
+        }
+        return null;
+    }
+
 
     @Before("@annotation(operatorTrajectory)")
     public void doBefore(JoinPoint point,  OperatorTrajectory operatorTrajectory){
+        log.warn("[轨迹] doBefore 触发, method={}, type={}", point.getSignature().getName(), operatorTrajectory.type());
         Object obj = point.getArgs()[0];
         String id = getId(point,operatorTrajectory.primary());
         List<String> ids = new ArrayList<>();
@@ -172,10 +228,13 @@ public class OperatorTrajectoryAspect {
         trajectory.setServiceTag(className + "." + methodName + "()");
         trajectory.setModuleName(operatorTrajectory.moduleName());
         trajectory.setCreateTime(LocalDateTime.now());
-        trajectory.setCreateBy(baseController.getUserName());
-        trajectory.setEntityName(operatorTrajectory.entityClass().getName());
+        String userName = baseController.getUserName();
+        trajectory.setCreateBy(userName);
+        trajectory.setCreateByName(userName);
+        trajectory.setEntityName(Arrays.stream(operatorTrajectory.entityClass())
+                .map(Class::getName).collect(Collectors.joining(",")));
         if(!Objects.isNull(obj)) {
-            trajectory.setAfterJson(JSON.toJSONString(obj));
+            trajectory.setAfterJson(JSON.toJSONString(obj, SerializerFeature.DisableCircularReferenceDetect));
         }
         Object beforeData;
         String operatorName = operatorTrajectory.OperationDict().getName();
@@ -189,17 +248,38 @@ public class OperatorTrajectoryAspect {
         switch(operatorTrajectory.type()){
             //创建
             case "C":
-                trajectory.setOperationContent(DescriptionUtils.generateDescription(obj,operatorName));
+                log.warn("[轨迹-C] 开始保存创建轨迹, trajectoryId={}", trajectoryId);
+                try {
+                    Object enriched = enrichRelatedFields(obj, operatorTrajectory);
+                    // ★ 展平嵌套子表字段(从 costAddList[0].ptlAgreementCostAddList[0] 提取到顶层)
+                    enriched = flattenNestedSubFields(enriched, operatorTrajectory);
+                    if (enriched != null && enriched != obj) {
+                        trajectory.setAfterJson(JSON.toJSONString(enriched, SerializerFeature.DisableCircularReferenceDetect));
+                        log.warn("[轨迹-C] afterJson 已用 enrichRelatedFields + flattenNestedSubFields 补充");
+                    }
+                } catch (Exception e) {
+                    log.error("[轨迹-C] enrichRelatedFields 异常,使用原始 afterJson: {}", e.getMessage(), e);
+                }
+                trajectory.setOperationContent(DescriptionUtils.generateDescription(obj, operatorName));
                 operatorTrajectoryService.save(trajectory);
+                log.warn("[轨迹-C] 创建轨迹保存成功, trajectoryId={}", trajectoryId);
                 break;
             //更新
             case "U":
+                Class<?>[] uEntityClasses = operatorTrajectory.entityClass();
+                Class<?>[] uMapperClasses = operatorTrajectory.mapperClass();
                 for (String s : ids) {
-                    beforeData = GetSelectIdDataUtils.selectById(operatorTrajectory.entityClass(), operatorTrajectory.mapperClass(), s);
+                    beforeData = null;
+                    if (uEntityClasses.length > 0 && uMapperClasses.length > 0) {
+                        beforeData = GetSelectIdDataUtils.selectById(uEntityClasses[0], uMapperClasses[0], s);
+                    }
+                    beforeData = fillSubTableData(beforeData, s, operatorTrajectory);
+                    // ★ 从关联表补充字段(如协议CODE、协议名称)
+                    beforeData = enrichRelatedFields(beforeData, operatorTrajectory);
                     trajectory.setId(IdGenerate.nextId());
                     trajectory.setPrimaryValue(s);
                     if (!Objects.isNull(beforeData)) {
-                        trajectory.setBeforeJson(JSON.toJSONString(beforeData));
+                        trajectory.setBeforeJson(JSON.toJSONString(beforeData, SerializerFeature.DisableCircularReferenceDetect));
                     }
                     trajectory.setOperationContent(DescriptionUtils.generateDescription(beforeData, operatorName));
                     operatorTrajectoryService.save(trajectory);
@@ -207,10 +287,18 @@ public class OperatorTrajectoryAspect {
                 break;
             //删除
             case "D":
+                Class<?>[] dEntityClasses = operatorTrajectory.entityClass();
+                Class<?>[] dMapperClasses = operatorTrajectory.mapperClass();
                 for (String s : ids) {
-                    beforeData = GetSelectIdDataUtils.selectById(operatorTrajectory.entityClass(), operatorTrajectory.mapperClass(), s);
+                    beforeData = null;
+                    if (dEntityClasses.length > 0 && dMapperClasses.length > 0) {
+                        beforeData = GetSelectIdDataUtils.selectById(dEntityClasses[0], dMapperClasses[0], s);
+                    }
+                    beforeData = fillSubTableData(beforeData, s, operatorTrajectory);
+                    // ★ 从关联表补充字段
+                    beforeData = enrichRelatedFields(beforeData, operatorTrajectory);
                     trajectory.setPrimaryValue(s);
-                    trajectory.setBeforeJson(JSON.toJSONString(beforeData));
+                    trajectory.setBeforeJson(JSON.toJSONString(beforeData, SerializerFeature.DisableCircularReferenceDetect));
                     trajectory.setOperationContent(DescriptionUtils.generateDescription(beforeData, operatorName));
                     operatorTrajectoryService.save(trajectory);
                     break;
@@ -223,6 +311,199 @@ public class OperatorTrajectoryAspect {
         methodContext.set(new MethodContext(point.getSignature().getName(), trajectoryId));
     }
 
+    /**
+     * 从嵌套 DTO 结构中提取子表字段到顶层
+     * 处理批量创建 DTO 结构:costAddList[0].ptlAgreementCostAddList[0] 中的子表数据
+     */
+    @SuppressWarnings("unchecked")
+    private Object flattenNestedSubFields(Object data, OperatorTrajectory annotation) {
+        String[] subFieldNames = annotation.subFieldNames();
+        if (data == null || subFieldNames.length == 0) return data;
+
+        try {
+            Map<String, Object> dataMap;
+            if (data instanceof Map) {
+                dataMap = new LinkedHashMap<>((Map<String, Object>) data);
+            } else {
+                dataMap = JSONObject.parseObject(JSON.toJSONString(data), Map.class);
+            }
+
+            // 遍历 costAddList → ptlAgreementCostAddList 找到子表数据
+            Object costAddListObj = dataMap.get("costAddList");
+            if (costAddListObj == null) return dataMap;
+
+            Collection<?> costAddList = (costAddListObj instanceof Collection)
+                    ? (Collection<?>) costAddListObj
+                    : JSON.parseArray(JSON.toJSONString(costAddListObj));
+
+            for (Object costAddItem : costAddList) {
+                JSONObject costAddJson = (costAddItem instanceof JSONObject)
+                        ? (JSONObject) costAddItem
+                        : (JSONObject) JSON.toJSON(costAddItem);
+
+                Object innerListObj = costAddJson.get("ptlAgreementCostAddList");
+                if (innerListObj == null) continue;
+
+                Collection<?> innerList = (innerListObj instanceof Collection)
+                        ? (Collection<?>) innerListObj
+                        : JSON.parseArray(JSON.toJSONString(innerListObj));
+
+                for (Object innerItem : innerList) {
+                    JSONObject innerJson = (innerItem instanceof JSONObject)
+                            ? (JSONObject) innerItem
+                            : (JSONObject) JSON.toJSON(innerItem);
+
+                    for (String subFieldName : subFieldNames) {
+                        if (!dataMap.containsKey(subFieldName) || isEmptyCollection(dataMap.get(subFieldName))) {
+                            Object subData = innerJson.get(subFieldName);
+                            if (subData != null && !isEmptyCollection(subData)) {
+                                dataMap.put(subFieldName, subData);
+                                log.info("[轨迹-C] 展平子表字段: {} (从嵌套 DTO 提取)", subFieldName);
+                            }
+                        }
+                    }
+                }
+            }
+            return dataMap;
+        } catch (Exception e) {
+            log.warn("[轨迹-C] flattenNestedSubFields 异常: {}", e.getMessage());
+            return data;
+        }
+    }
+
+    private boolean isEmptyCollection(Object obj) {
+        if (obj instanceof Collection) return ((Collection<?>) obj).isEmpty();
+        if (obj instanceof JSONArray) return ((JSONArray) obj).isEmpty();
+        return false;
+    }
+
+    /**
+     * 从关联表补充字段到 beforeData
+     * 用于解决主表PO中缺失但在DTO中存在的字段(如协议CODE、协议名称)
+     *
+     * @param beforeData 主表数据(PO对象或Map)
+     * @param annotation 注解配置
+     * @return 补充了关联字段的 beforeData Map
+     */
+    @SuppressWarnings("unchecked")
+    private Object enrichRelatedFields(Object beforeData, OperatorTrajectory annotation) {
+        String enrichConfig = annotation.enrichConfig();
+        if (StringUtils.isEmpty(enrichConfig) || beforeData == null) {
+            return beforeData;
+        }
+
+        try {
+            Map<String, Object> beforeMap;
+            if (beforeData instanceof Map) {
+                beforeMap = new LinkedHashMap<>((Map<String, Object>) beforeData);
+            } else {
+                beforeMap = JSONObject.parseObject(JSON.toJSONString(beforeData), Map.class);
+            }
+
+            // 解析配置格式: mapperClassName:foreignKey:srcField1:dstField1,srcField2:dstField2
+            String[] parts = enrichConfig.split(":", 3);
+            if (parts.length < 3) {
+                log.warn("enrichConfig 格式错误,应为: mapperClassName:foreignKey:srcField1:dstField1,... 实际: {}", enrichConfig);
+                return beforeData;
+            }
+
+            String mapperClassName = parts[0];
+            String foreignKey = parts[1];
+            String fieldMappings = parts[2];
+
+            // 获取外键值
+            Object fkValue = beforeMap.get(foreignKey);
+            if (fkValue == null || StringUtils.isEmpty(String.valueOf(fkValue))) {
+                log.warn("enrichRelatedFields: 外键 {} 为空,跳过", foreignKey);
+                return beforeData;
+            }
+
+            // 加载 Mapper 类并查询关联实体
+            Class<?> mapperClass = Class.forName(mapperClassName);
+            Object relatedEntity = GetSelectIdDataUtils.selectById(null, mapperClass, String.valueOf(fkValue));
+            if (relatedEntity == null) {
+                log.warn("enrichRelatedFields: 未找到关联实体,mapper={}, fkValue={}", mapperClassName, fkValue);
+                return beforeData;
+            }
+
+            // 将关联实体转为 Map
+            Map<String, Object> relatedMap;
+            if (relatedEntity instanceof Map) {
+                relatedMap = (Map<String, Object>) relatedEntity;
+            } else {
+                relatedMap = JSONObject.parseObject(JSON.toJSONString(relatedEntity), Map.class);
+            }
+
+            // 解析字段映射并补充字段
+            String[] mappings = fieldMappings.split(",");
+            for (String mapping : mappings) {
+                String[] fieldPair = mapping.split(":");
+                if (fieldPair.length == 2) {
+                    String srcField = fieldPair[0].trim();
+                    String dstField = fieldPair[1].trim();
+                    Object srcValue = relatedMap.get(srcField);
+                    if (srcValue != null) {
+                        beforeMap.put(dstField, srcValue);
+                        log.info("enrichRelatedFields: 补充字段 {} = {} (来源: {}.{})", dstField, srcValue, mapperClassName, srcField);
+                    }
+                }
+            }
+
+            return beforeMap;
+        } catch (Exception e) {
+            log.error("enrichRelatedFields 异常: {}", e.getMessage(), e);
+            return beforeData;
+        }
+    }
+
+    /**
+     * 回填子表数据到 beforeData
+     */
+    @SuppressWarnings("unchecked")
+    private Object fillSubTableData(Object beforeData, String mainId, OperatorTrajectory annotation) {
+        if (beforeData == null || StringUtils.isEmpty(annotation.subForeignKey())
+                || annotation.subFieldNames().length == 0) {
+            return beforeData;
+        }
+
+        Class<?>[] entityClasses = annotation.entityClass();
+        Class<?>[] mapperClasses = annotation.mapperClass();
+        String[] subFieldNames = annotation.subFieldNames();
+        String foreignKey = annotation.subForeignKey();
+
+        Map<String, Object> beforeMap;
+        if (beforeData instanceof Map) {
+            beforeMap = new LinkedHashMap<>((Map<String, Object>) beforeData);
+        } else {
+            beforeMap = JSONObject.parseObject(JSON.toJSONString(beforeData), Map.class);
+        }
+
+        int subCount = Math.min(Math.min(entityClasses.length - 1, mapperClasses.length - 1), subFieldNames.length);
+        for (int i = 0; i < subCount; i++) {
+            int entityIndex = i + 1;
+            Class<?> subMapperClass = mapperClasses[entityIndex];
+            String fieldName = subFieldNames[i];
+
+            try {
+                List<?> subDataList = GetSelectIdDataUtils.selectListByField(subMapperClass, foreignKey, mainId);
+                log.info("回填子表数据: fieldName={}, mapper={}, foreignKey={}, mainId={}, subDataCount={}",
+                        fieldName, subMapperClass.getSimpleName(), foreignKey, mainId,
+                        subDataList != null ? subDataList.size() : 0);
+                if (subDataList != null && !subDataList.isEmpty()) {
+                    beforeMap.put(fieldName, subDataList);
+                } else {
+                    beforeMap.put(fieldName, new ArrayList<>());
+                }
+            } catch (Exception e) {
+                log.warn("回填子表数据失败: mapper={}, foreignKey={}, mainId={}, error={}",
+                        subMapperClass.getName(), foreignKey, mainId, e.getMessage());
+                beforeMap.put(fieldName, new ArrayList<>());
+            }
+        }
+
+        return beforeMap;
+    }
+
 
     public boolean getEnableDisable(JoinPoint point,  OperatorTrajectory operatorTrajectory){
         Method method = ((MethodSignature) point.getSignature()).getMethod();
@@ -291,4 +572,4 @@ public class OperatorTrajectoryAspect {
             return id;
         }
     }
-}
+}

+ 1 - 1
commons/src/main/java/com/jzg/commons/entity/dto/CostAdd.java

@@ -27,7 +27,7 @@ public class CostAdd {
     @NotBlank(message = "承保规则ID不能为空")
     private String coverRuleId;
 
-
+    @Schema(description = "费用集合")
     private List<PtlAgreementCostAdd> ptlAgreementCostAddList;
 
 }

+ 2 - 1
commons/src/main/java/com/jzg/commons/entity/dto/PtlAgreementCostEnd.java

@@ -129,7 +129,8 @@ public class PtlAgreementCostEnd {
     @Schema(description = "费用类型集合描述")
     public static class PtlAgreementCostTypeDto {
 
-        @Schema(description = "费用类型1-交强险2-商业险3-驾意险")
+        //1-交强险2-商业险3-驾意险
+        @Schema(description = "费用类型")
         private String costType;
 
         @Schema(description = "入口比例")

+ 8 - 0
commons/src/main/java/com/jzg/commons/service/impl/OperatorTrajectoryServiceImpl.java

@@ -56,6 +56,10 @@ public class OperatorTrajectoryServiceImpl implements OperatorTrajectoryService
         List<CompareResult> compareResults = CompatibleUtils.compareFieldsMap(operatorTrajectory);
         operatorTrajectory.setCompareResultList(compareResults);
         operatorTrajectory.setOperatorName(Crud.getNameByCode(operatorTrajectory.getOperatorType()));
+        // ★ 兜底:历史记录未存 createByName 时,用 createBy 填充
+        if (StringUtils.isEmpty(operatorTrajectory.getCreateByName()) && StringUtils.isNotEmpty(operatorTrajectory.getCreateBy())) {
+            operatorTrajectory.setCreateByName(operatorTrajectory.getCreateBy());
+        }
         return operatorTrajectory;
     }
 
@@ -88,6 +92,10 @@ public class OperatorTrajectoryServiceImpl implements OperatorTrajectoryService
             list.forEach(item->{
                 String nameByCode = Crud.getNameByCode(item.getOperatorType());
                 item.setOperatorName(nameByCode);
+                // ★ 兜底:历史记录未存 createByName 时,用 createBy 填充
+                if (StringUtils.isEmpty(item.getCreateByName()) && StringUtils.isNotEmpty(item.getCreateBy())) {
+                    item.setCreateByName(item.getCreateBy());
+                }
                 // 按业务主键查询时(如协议操作轨迹弹窗),填充字段级变更对比结果
                 if (needCompare) {
                     item.setCompareResultList(CompatibleUtils.compareFieldsMap(item));

+ 2 - 2
commons/src/main/java/com/jzg/commons/util/BuildDescUtils.java

@@ -16,15 +16,15 @@ public class BuildDescUtils {
     public static String buildDesc(String companyId, List<PtlAgreementUndwrtRulesAttr> linkList, List<PtlAgreementUndwrtRulesAttr> undwrtRulesAttrList) {
         StringBuilder desc = new StringBuilder();
         Map<String, PtlAgreementUndwrtRulesAttr> attrMap = undwrtRulesAttrList.stream()
-                .filter(attr -> "all".equals(attr.getCompanyIds()) || attr.getCompanyIds().equals(companyId))
+                .filter(attr -> "all".equals(attr.getCompanyIds()) || (attr.getCompanyIds() != null && attr.getCompanyIds().equals(companyId)))
                 .collect(Collectors.toMap(PtlAgreementUndwrtRulesAttr::getAttrCode, Function.identity()));
 
         for (PtlAgreementUndwrtRulesAttr link : linkList) {
             PtlAgreementUndwrtRulesAttr ptlAgreementUndwrtRulesAttr = attrMap.get(link.getAttrCode());
-            ptlAgreementUndwrtRulesAttr.setDictLabal(link.getDictLabal());
             if (ptlAgreementUndwrtRulesAttr == null) {
                 continue;
             }
+            ptlAgreementUndwrtRulesAttr.setDictLabal(link.getDictLabal());
 
             String attrType = ptlAgreementUndwrtRulesAttr.getAttrType();
 

+ 255 - 19
commons/src/main/java/com/jzg/commons/util/GetSelectIdDataUtils.java

@@ -1,24 +1,45 @@
 package com.jzg.commons.util;
 
+
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.jzg.commons.entity.po.OperatorTrajectory;
+import com.jzg.commons.util.StringUtils;
+import com.jzg.commons.util.comparble.CompareResult;
+import com.jzg.commons.util.comparble.SchemaAnnotationUtils;
 import com.jzg.commons.util.spring.SpringUtils;
 import lombok.extern.slf4j.Slf4j;
 
 import java.io.Serializable;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
 
+/**
+ * 比较器工具类
+ */
 @Slf4j
 public class GetSelectIdDataUtils {
 
+    // 定义线程安全的日期格式化器
+    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"
+    ));
+
     public static Object selectById(Class<?> entityClazz, Class<?> mapperClass, String id) {
-        if (entityClazz == null || mapperClass == null || id == null) {
-            throw new IllegalArgumentException("Input parameters cannot be null");
+        if (mapperClass == null || id == null) {
+            throw new IllegalArgumentException("Mapper class and ID cannot be null");
         }
         try {
             Method method = mapperClass.getMethod("selectById", Serializable.class);
@@ -26,44 +47,259 @@ public class GetSelectIdDataUtils {
                 return null;
             }
             if (id.startsWith("[") && id.endsWith("]")) {
-                // 2. 按逗号分割(处理可能存在的空格)
-                String[] elements = id.replaceAll("[\\[\\]\"]", "").split("\\s*,\\s*");
-                // 3. 转换为List
-                List<String> list = new ArrayList<>(Arrays.asList(elements));
+                String[] elements = id.replaceAll("[\\$\\$\\$\\\"]", "").split("\\s*,\\s*");
                 List<Object> resultList = new ArrayList<>();
-                for (String l : list) {
+                for (String l : elements) {
+                    if (cn.hutool.core.util.ObjectUtil.isNull(l)) {
+                        continue;
+                    }
                     Object invoke = method.invoke(SpringUtils.getBean(mapperClass), l);
-                    if (cn.hutool.core.util.ObjectUtil.isNull(invoke)) {
-                        return null;
+                    if (cn.hutool.core.util.ObjectUtil.isNotNull(invoke)) {
+                        resultList.add(invoke);
                     }
-                    resultList.add(convertToObject(entityClazz, invoke));
                 }
-                return resultList;
+                return resultList.isEmpty() ? null : resultList;
             } else {
                 Object invoke = method.invoke(SpringUtils.getBean(mapperClass), id);
                 if (ObjectUtil.isNull(invoke)) {
                     return null;
                 }
-                return convertToObject(entityClazz, invoke);
+                return invoke;
             }
         } catch (NoSuchMethodException e) {
             log.error("Method 'selectById' not found in {}", mapperClass.getName());
         } catch (IllegalAccessException | InvocationTargetException e) {
             log.error("Failed to invoke method 'selectById'", e);
-        } catch (InstantiationException e) {
-            log.error("Failed to instantiate {}", entityClazz.getName());
         } catch (NumberFormatException e) {
             log.error("Failed to parse ID to long: {}", id);
         }
         return null;
     }
 
+    /**
+     * 数组版本:遍历多个 entityClass 和 mapperClass 进行查询
+     */
+    public static Object selectById(Class<?>[] entityClasses, Class<?>[] mapperClasses, String id) {
+        if (entityClasses == null || mapperClasses == null || id == null) {
+            throw new IllegalArgumentException("Input parameters cannot be null");
+        }
+        if (entityClasses.length == 0 || mapperClasses.length == 0) {
+            return null;
+        }
+        if (entityClasses.length == mapperClasses.length) {
+            for (int i = 0; i < entityClasses.length; i++) {
+                Object result = selectById(entityClasses[i], mapperClasses[i], id);
+                if (result != null) {
+                    return result;
+                }
+            }
+            return null;
+        }
+        return selectById(entityClasses[0], mapperClasses[0], id);
+    }
+
+    /**
+     * 按字段条件查询列表(用于子表外键关联查询)
+     * 示例:selectListByField(PtlAgreementCostTypeMapper.class, "ptlAgreementCostId", "xxx")
+     *       等价于 SELECT * FROM ptl_agreement_cost_type WHERE ptl_agreement_cost_id = 'xxx'
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> List<T> selectListByField(Class<?> mapperClass, String fieldName, Object fieldValue) {
+        if (mapperClass == null || fieldName == null) {
+            return new ArrayList<>();
+        }
+        try {
+            // MyBatis-Plus BaseMapper.selectList(Wrapper)
+            Method selectListMethod = mapperClass.getMethod("selectList", com.baomidou.mybatisplus.core.conditions.Wrapper.class);
+            QueryWrapper<Object> wrapper = new QueryWrapper<>();
+            // ★ QueryWrapper 不会自动将驼峰转为下划线,需手动转换
+            // 例如 Java 字段 ptlAgreementCostId → 数据库列 ptl_agreement_cost_id
+            String columnName = StrUtil.toUnderlineCase(fieldName);
+            wrapper.eq(columnName, fieldValue);
+            Object result = selectListMethod.invoke(SpringUtils.getBean(mapperClass), wrapper);
+            if (result instanceof List) {
+                return (List<T>) result;
+            }
+            return new ArrayList<>();
+        } catch (NoSuchMethodException e) {
+            log.error("selectList 方法未找到: {}", mapperClass.getName());
+        } catch (InvocationTargetException e) {
+            // ★ 解包 InvocationTargetException,获取反射目标方法内部抛出的真实异常
+            Throwable cause = e.getCause() != null ? e.getCause() : e;
+            log.error("按字段查询失败: mapper={}, field={}, value={}, error={}",
+                    mapperClass.getName(), fieldName, fieldValue, cause.getMessage(), cause);
+        } catch (Exception e) {
+            log.error("按字段查询失败: mapper={}, field={}, value={}, error={}",
+                    mapperClass.getName(), fieldName, fieldValue, e.getMessage(), e);
+        }
+        return new ArrayList<>();
+    }
+
     private static Object convertToObject(Class<?> entityClazz, Object source) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
         Object targetObj = entityClazz.getDeclaredConstructor().newInstance();
         BeanUtil.copyProperties(source, targetObj);
         return targetObj;
     }
 
+    /**
+     * 比较两个 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 {
+            Map<String, Object> oldMap = parseJsonToMap(afterJson);
+            Map<String, Object> sourceMap = parseJsonToMap(source);
+            List<CompareResult> compareResults = compareMaps(oldMap, sourceMap, operatorTrajectory.getEntityName());
+            List<CompareResult> distinctPeople = compareResults.stream()
+                    .collect(Collectors.toMap(
+                            p -> Arrays.asList(p.getFieldName(), p.getFieldContent()),
+                            p -> p,
+                            (existing, replacement) -> existing
+                    ))
+                    .values()
+                    .stream()
+                    .collect(Collectors.toList());
+
+            return distinctPeople;
+        } catch (Exception e) {
+            log.error("JSON解析失败: {}", e.getMessage(), e);
+            return Collections.emptyList();
+        }
+    }
+
+    /**
+     * 递归比较两个 Map 的字段差异
+     * 支持三种字段类型:
+     *   1. 普通字段 → getValue() 转字符串后对比
+     *   2. 嵌套 JSONObject → 递归对比
+     *   3. List/Set/Collection/JSONArray/Array → JSON 序列化后对比
+     */
+    private static List<CompareResult> compareMaps(Map<String, Object> oldMap, Map<String, Object> sourceMap, String entityName) {
+        List<CompareResult> results = new ArrayList<>();
+
+        for (Map.Entry<String, Object> entry : oldMap.entrySet()) {
+            String key = entry.getKey();
+            if (IGNORED_FIELDS.contains(key)) {
+                continue;
+            }
 
+            Object newData = entry.getValue();
+            Object oldData = sourceMap.get(key);
+
+            // 1. 嵌套 JSONObject → 递归对比
+            if (isJSONObject(newData)) {
+                results.addAll(compareMaps(parseJsonToMap(newData), parseJsonToMap(oldData), entityName));
+            }
+            // 2. 集合/数组/JSONArray → JSON 序列化后对比
+            else if (isCollectionType(newData) || isCollectionType(oldData)) {
+                String newValue = toJsonString(newData);
+                String oldValue = toJsonString(oldData);
+                if (StringUtils.isNotEmpty(newValue) && !Objects.equals(oldValue, newValue)) {
+                    results.add(buildCompareResult(entityName, key, oldValue, newValue));
+                }
+            }
+            // 3. 普通字段 → 字符串对比
+            else {
+                String newValue = getValue(newData);
+                String oldValue = getValue(oldData);
+                if (StringUtils.isNotEmpty(newValue) && !Objects.equals(oldValue, newValue)) {
+                    results.add(buildCompareResult(entityName, key, oldValue, 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();
+    }
+
+    /** 将对象序列化为 JSON 字符串 */
+    private static String toJsonString(Object obj) {
+        if (obj == null) return null;
+        if (obj instanceof JSONArray) return ((JSONArray) obj).toJSONString();
+        return com.alibaba.fastjson2.JSON.toJSONString(obj);
+    }
+
+    /**
+     * 判断对象是否为 JSONObject
+     *
+     * @param obj 对象
+     * @return 是否为 JSONObject
+     */
+    public static boolean isJSONObject(Object obj) {
+        return obj instanceof JSONObject;
+    }
+
+    /**
+     * 将 JSON 字符串解析为 Map
+     *
+     * @param json JSON 字符串
+     * @return 解析后的 Map
+     */
+    private static Map<String, Object> parseJsonToMap(Object json) {
+        if (ObjectUtil.isEmpty(json)) {
+            return Collections.emptyMap();
+        }
+
+        try {
+            if (json instanceof String) {
+                return JSONObject.parseObject((String) json, Map.class);
+            }
+            return JSONObject.parseObject(JSONObject.toJSONString(json), Map.class);
+        } catch (Exception e) {
+            log.warn("JSON解析失败: {}", e.getMessage());
+            return Collections.emptyMap();
+        }
+    }
+
+    /**
+     * 获取对象的字符串表示形式
+     *
+     * @param obj 对象
+     * @return 字符串表示形式
+     */
+    public static String getValue(Object obj) {
+        if (obj == null) {
+            return null;
+        }
+
+        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);
+        } else if (obj instanceof LocalDateTime) {
+            return ((LocalDateTime) obj).format(DATE_TIME_FORMATTER);
+        } else if (obj instanceof LocalDate) {
+            return ((LocalDate) obj).format(DATE_FORMATTER);
+        }
+
+        return null;
+    }
 
-}
+}

+ 915 - 42
commons/src/main/java/com/jzg/commons/util/comparble/CompatibleUtils.java

@@ -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;
     }
-
-}
-
+}

+ 22 - 0
commons/src/main/java/com/jzg/commons/util/comparble/DictLabelResolver.java

@@ -0,0 +1,22 @@
+package com.jzg.commons.util.comparble;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 字典标签解析器接口
+ * 由业务模块(如 organization)实现,提供字典值转换能力
+ */
+public interface DictLabelResolver {
+    /**
+     * 批量获取属性映射(attrCode → attrName)
+     * @return 属性映射表
+     */
+    Map<String, String> getAttrNameMap();
+    
+    /**
+     * 批量获取字典映射(attrCode → {code → name})
+     * @return 字典映射表
+     */
+    Map<String, Map<String, String>> getDictLabelMap();
+}

+ 90 - 14
commons/src/main/java/com/jzg/commons/util/comparble/SchemaAnnotationUtils.java

@@ -4,39 +4,115 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.extern.slf4j.Slf4j;
 
 import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 @Slf4j
 public class SchemaAnnotationUtils {
 
+    /** 缓存 entity:fieldName → @Schema description,避免重复反射 */
+    private static final ConcurrentHashMap<String, String> SCHEMA_CACHE = new ConcurrentHashMap<>();
+
     /**
-     * 获取字段上的 @Schema 注解信息
+     * 获取字段上的 @Schema 注解信息(带缓存)
+     * 支持逗号分隔的多个实体类名(兼容数组结构的 entityClass)
      *
-     * @param entity     实体类的 Class 对象
-     * @param fieldName 字段名
-     * @return 字段上的 @Schema 注解,如果不存在则返回 null
+     * @param entity     实体类的全限定名,支持逗号分隔多个
+     * @param fieldName  字段名
+     * @return 字段上的 @Schema 注解描述
      */
     public static String getSchemaAnnotation(String entity, String fieldName) {
+        String cacheKey = entity + ":" + fieldName;
+        return SCHEMA_CACHE.computeIfAbsent(cacheKey, k -> resolveSchemaAnnotation(entity, fieldName));
+    }
+
+    /** 实际反射查找逻辑(仅在缓存未命中时执行) */
+    private static String resolveSchemaAnnotation(String entity, String fieldName) {
+        try {
+            String[] classNames = entity.split(",");
+            for (String className : classNames) {
+                Class<?> clazz = getClassByName(className.trim());
+                if (clazz == null) {
+                    continue;
+                }
+                // ★ 递归搜索类及其内部类(处理 DTO 中嵌套 static class 定义子表字段的场景)
+                String description = findSchemaInClassAndInner(clazz, fieldName);
+                if (description != null) {
+                    return description;
+                }
+            }
+            log.warn("未在任何实体类中找到字段: {} (entity={})", fieldName, entity);
+            return null;
+        } catch (Exception e) {
+            log.error("getSchemaAnnotation异常: {}", e.getMessage(), e);
+        }
+        return null;
+    }
+
+    /** 递归搜索类及其所有内部类中的字段 @Schema 描述 */
+    private static String findSchemaInClassAndInner(Class<?> clazz, String fieldName) {
+        return findSchemaInClassAndInner(clazz, fieldName, new HashSet<>());
+    }
+
+    /** 递归搜索类、内部类、以及集合字段的泛型类型中的字段 @Schema 描述 */
+    private static String findSchemaInClassAndInner(Class<?> clazz, String fieldName, Set<Class<?>> visited) {
+        if (clazz == null || !visited.add(clazz)) return null;
+
+        // 1. 直接在当前类中查找
         try {
-            Class<?> clazz = getClassByName(entity);
-            // 获取字段
             Field field = clazz.getDeclaredField(fieldName);
-            // 获取字段上的 @Schema 注解
             Schema schemaAnnotation = field.getAnnotation(Schema.class);
             if (schemaAnnotation != null) {
                 return schemaAnnotation.description();
-            } else {
-                log.error("No @Schema annotation found for field: {}", fieldName);
             }
-            return null;
-        } catch (NoSuchFieldException e) {
-            log.error("NoSuchFieldException", e);
+        } catch (NoSuchFieldException ignored) {
+        }
+
+        // 2. 递归搜索声明的内部类
+        for (Class<?> inner : clazz.getDeclaredClasses()) {
+            String result = findSchemaInClassAndInner(inner, fieldName, visited);
+            if (result != null) return result;
         }
+
+        // 3. ★ 搜索集合字段的泛型类型(处理 DTO 中 List<InnerDTO> 字段引用独立类的场景)
+        for (Field field : clazz.getDeclaredFields()) {
+            Class<?> fieldType = getCollectionGenericType(field);
+            if (fieldType != null && !visited.contains(fieldType)) {
+                String result = findSchemaInClassAndInner(fieldType, fieldName, visited);
+                if (result != null) return result;
+            }
+        }
+
         return null;
     }
 
+    /** 获取集合字段的泛型参数类型,如 List<CostAdd> → CostAdd.class */
+    private static Class<?> getCollectionGenericType(Field field) {
+        Type genericType = field.getGenericType();
+        if (genericType instanceof ParameterizedType) {
+            Class<?> rawType = getRawType(field.getType());
+            if (rawType != null && Collection.class.isAssignableFrom(rawType)) {
+                Type[] typeArgs = ((ParameterizedType) genericType).getActualTypeArguments();
+                if (typeArgs.length > 0 && typeArgs[0] instanceof Class) {
+                    return (Class<?>) typeArgs[0];
+                }
+            }
+        }
+        return null;
+    }
+
+    /** 获取字段的原始类型(处理数组等情况) */
+    private static Class<?> getRawType(Class<?> type) {
+        if (type.isArray()) return null;
+        return type;
+    }
 
     /**
-     * 通过类名获取 Class 对象
+     * 通过类名获取 Class 对象(带缓存)
      *
      * @param className 类的全限定名(包括包名)
      * @return 对应的 Class 对象,如果找不到则返回 null
@@ -45,7 +121,7 @@ public class SchemaAnnotationUtils {
         try {
             return Class.forName(className);
         } catch (ClassNotFoundException e) {
-            e.printStackTrace();
+            log.warn("类未找到: {}", className);
         }
         return null;
     }

+ 51 - 0
tenant/insurance/quotation-dadi/src/main/java/com/jzg/quotation/dadi/crawler/build/DaDiCrawlerQuoteResultVoBuild.java

@@ -1,5 +1,7 @@
 package com.jzg.quotation.dadi.crawler.build;
 
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
 import com.jzg.commons.constants.dict.InsOrderStatusEnum;
 import com.jzg.commons.entity.quote.vo.QuoteVo;
 import com.jzg.commons.entity.quote.vo.RiskInfoVo;
@@ -60,6 +62,55 @@ public class DaDiCrawlerQuoteResultVoBuild {
 
         //设置保费,车船税,投保单号,折旧价
         buildParams(daDiQuoteResponse, quoteResultsVo, vehiclePricResponse);
+
+        // 保司评分数据(用于规则引擎匹配)
+        QuoteResultsVo.ScoreVo scoreVo = new QuoteResultsVo.ScoreVo();
+        JSONArray scoreJson = new JSONArray();
+        // 上年保费
+        if (daDiQuoteResponse.getLastPremium() != null) {
+            scoreJson.add(new JSONObject()
+                    .fluentPut("scoreName", "上年保费")
+                    .fluentPut("scoreKey", "lastPremium")
+                    .fluentPut("score", daDiQuoteResponse.getLastPremium()));
+        }
+        // 智能评分
+        if (daDiQuoteResponse.getSmartscore() != null) {
+            scoreJson.add(new JSONObject()
+                    .fluentPut("scoreName", "智能评分")
+                    .fluentPut("scoreKey", "smartscore")
+                    .fluentPut("score", daDiQuoteResponse.getSmartscore()));
+        }
+        // 商业险折扣率
+        if (daDiQuoteResponse.getCdiscountrate() != null) {
+            scoreJson.add(new JSONObject()
+                    .fluentPut("scoreName", "商业险折扣率")
+                    .fluentPut("scoreKey", "cdiscountrate")
+                    .fluentPut("score", daDiQuoteResponse.getCdiscountrate()));
+        }
+        // cbit评分
+        if (daDiQuoteResponse.getCbitScore() != null) {
+            scoreJson.add(new JSONObject()
+                    .fluentPut("scoreName", "cbit评分")
+                    .fluentPut("scoreKey", "cbitScore")
+                    .fluentPut("score", daDiQuoteResponse.getCbitScore()));
+        }
+        // 中银车评分
+        if (daDiQuoteResponse.getCcicVehicleScore() != null) {
+            scoreJson.add(new JSONObject()
+                    .fluentPut("scoreName", "中银车评分")
+                    .fluentPut("scoreKey", "ccicVehicleScore")
+                    .fluentPut("score", daDiQuoteResponse.getCcicVehicleScore()));
+        }
+        // 车信评分
+        if (daDiQuoteResponse.getXcarscore() != null) {
+            scoreJson.add(new JSONObject()
+                    .fluentPut("scoreName", "车信评分")
+                    .fluentPut("scoreKey", "xcarscore")
+                    .fluentPut("score", daDiQuoteResponse.getXcarscore()));
+        }
+        scoreVo.setScoreJson(scoreJson);
+        quoteResultsVo.setScoreVo(scoreVo);
+
         return quoteResultsVo;
     }
 

+ 1 - 1
tenant/insurance/quotation-hengbang/src/main/java/com/jzg/quotation/hengbang/crawler/build/HbCrawlerQuoteResultVoBuild.java

@@ -159,7 +159,7 @@ public final class HbCrawlerQuoteResultVoBuild {
             kindInfoVo.setUnitAmount(carInsureLiabilityDTO.getUnitAmount());
             kindInfoVo.setQuantity(String.valueOf(carInsureLiabilityDTO.getQuantity()));
             kindInfoVo.setDeductibleRate(carInsureLiabilityDTO.getLiabrate());
-            kindInfoVo.setPlatformKindCode(kindInfoVo.getKindCode());
+            kindInfoVo.setPlatformKindCode(kindInfoVo.getPlatformKindCode());
         });
         return kindInfoVoList;
     }

+ 4 - 0
tenant/insurance/quotation-renbaonew/src/main/java/com/jzg/quotation/renbaonew/crawler/entity/response/RenBaoNewCrawlerQuoteResponse.java

@@ -148,6 +148,10 @@ public class RenBaoNewCrawlerQuoteResponse {
          * 上一年保险
          */
         private List<Map<String, Object>> ReInsureItems;
+        /**
+         * 驾意险详情
+         */
+        private List<Map<String, Object>> UnionRMsgVoList;
     }
 
     /**

+ 67 - 15
tenant/insurance/quotation-renbaonew/src/main/java/com/jzg/quotation/renbaonew/crawler/service/impl/RenBaoNewCrawlerRequestBuilder.java

@@ -2,6 +2,7 @@ package com.jzg.quotation.renbaonew.crawler.service.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.google.common.collect.Lists;
+import com.jzg.commons.entity.quote.vo.AccidentalDrivingVo;
 import com.jzg.commons.entity.quote.vo.CarInfoVo;
 import com.jzg.commons.entity.quote.vo.CustomerInfoVo;
 import com.jzg.commons.entity.quote.vo.QuoteVo;
@@ -498,7 +499,12 @@ public class RenBaoNewCrawlerRequestBuilder {
         request.setOcrList(idList);
 
         // 驾意险处理 - 构建 prpCunion(如果没有驾意险则设置为null)
-        List<Map<String, Object>> prpCunion = buildPrpCunion(quoteVo, carInfo, gscomCode, headers, username);
+        List<Map<String, Object>> prpCunion = buildPrpCunion(
+                quoteVo, carInfo, gscomCode, comCodeDes,
+                handler1Code, handler1CodeUni, handler1CodeDes,
+                handlerCode, handlerCodeUni, handlerCodeDes,
+                businessNature, agentCode, agentName,
+                carChecker, carCheckerTranslate, headers, loginInfo.getUserCode());
         if (prpCunion != null && !prpCunion.isEmpty()) {
             request.setPrpCunion(prpCunion);
         }
@@ -922,27 +928,42 @@ public class RenBaoNewCrawlerRequestBuilder {
      * @param quoteVo 报价请求VO,包含险种列表
      * @param carInfo 车辆信息,用于获取座位数(EBS需要)
      * @param comCode 归属部门代码
+     * @param comCodeDes 归属部门名称
+     * @param handler1Code 归属人账号
+     * @param handler1CodeUni 归属人统一工号
+     * @param handler1CodeDes 归属人名称
+     * @param handlerCode 经办人账号
+     * @param handlerCodeUni 经办人统一工号
+     * @param handlerCodeDes 经办人名称
+     * @param businessNature 业务性质
+     * @param agentCode 渠道代码
+     * @param agentName 渠道名称
+     * @param serviceManager 服务经理代码
+     * @param serviceManagerDes 服务经理名称
      * @param headers 请求头
-     * @param username 用户名
+     * @param userCode 当前登录账号
      * @return 非车险列表,为空表示不需要投保非车险
      */
-    private List<Map<String, Object>> buildPrpCunion(QuoteVo quoteVo, CarInfoVo carInfo,
-                                                     String comCode, HttpHeaders headers, String username) {
+    private List<Map<String, Object>> buildPrpCunion(
+            QuoteVo quoteVo, CarInfoVo carInfo, String comCode, String comCodeDes,
+            String handler1Code, String handler1CodeUni, String handler1CodeDes,
+            String handlerCode, String handlerCodeUni, String handlerCodeDes,
+            String businessNature, String agentCode, String agentName,
+            String serviceManager, String serviceManagerDes,
+            HttpHeaders headers, String userCode) {
+        List<AccidentalDrivingVo> accidentalDrivings = quoteVo.getAccidentalDrivings();
         List<Map<String, Object>> prpCunion = new ArrayList<>();
 
-        // 获取驾意险列表(从 quoteVo 中的 kindInfoVo 中筛选驾意险险种)
-        List<Map<String, Object>> kindMapList = convertKindInfoToMapList(quoteVo.getKindInfoVo());
-        if (kindMapList == null || kindMapList.isEmpty()) {
+        // 获取驾意险列表
+        if(accidentalDrivings == null || accidentalDrivings.isEmpty()){
             return prpCunion;
         }
-
-
-        for (Map<String, Object> kindMap : kindMapList) {
-            if (kindMap == null) {
+        for (AccidentalDrivingVo accidentalDriving : quoteVo.getAccidentalDrivings()) {
+            if (accidentalDriving == null) {
                 continue;
             }
 
-            String kindCode = getStringValue(kindMap, "kindCode");
+            String kindCode = accidentalDriving.getProductCode();
             if (kindCode == null || kindCode.isEmpty()) {
                 continue;
             }
@@ -985,11 +1006,31 @@ public class RenBaoNewCrawlerRequestBuilder {
                 continue;
             }
 
-            // 构建驾意险项
+            // 构建驾意险项,字段与Python版本Cunion保持一致
             Map<String, Object> unionItem = new HashMap<>();
+            unionItem.put("uCalculation", 1);
+            unionItem.put("uProposalNo", "");
             unionItem.put("uRiskPlanCode", rideRiskCode);
             unionItem.put("uRiskCode", unionRiskCode);
             unionItem.put("uRiskPlanInfo", uPlanInfos);
+            unionItem.put("uUserCode", userCode);
+            unionItem.put("comCodeDes", comCodeDes);
+            unionItem.put("comCode", comCode);
+            unionItem.put("handler1Code", handler1Code);
+            unionItem.put("handler1code_uni", handler1CodeUni);
+            unionItem.put("handler1CodeDes", handler1CodeDes);
+            unionItem.put("handlerCode", handlerCode);
+            unionItem.put("handlercode_uni", handlerCodeUni);
+            unionItem.put("handlerCodeDes", handlerCodeDes);
+            unionItem.put("businessNature", businessNature);
+            unionItem.put("agentCode", agentCode);
+            unionItem.put("agentName", agentName);
+            unionItem.put("makeCom", comCode);
+            unionItem.put("makeComDes", comCodeDes);
+            unionItem.put("serviceManager", serviceManager);
+            unionItem.put("serviceManagerDes", serviceManagerDes);
+            unionItem.put("isAccredit", "1");
+            unionItem.put("policyType", "1");
 
             // EAD(驾意险)数量固定为1,EBS(乘客意外险)数量为座位数
             if ("EAD".equals(unionRiskCode)) {
@@ -998,7 +1039,6 @@ public class RenBaoNewCrawlerRequestBuilder {
                 int seatCount = carInfo.getSeatCount() != null ? Integer.parseInt(carInfo.getSeatCount()) : 5;
                 unionItem.put("uNumPer", seatCount);
             }
-
             prpCunion.add(unionItem);
             log.info("Added union risk item: {}, count: {}", unionRiskCode, unionItem.get("uNumPer"));
         }
@@ -1073,7 +1113,7 @@ public class RenBaoNewCrawlerRequestBuilder {
         if (response == null) {
             return false;
         }
-        String status = (String) response.get("status");
+        String status = response.get("status").toString();
         String message = (String) response.get("message");
         return "0".equals(status) && "Success".equals(message);
     }
@@ -3680,6 +3720,18 @@ public class RenBaoNewCrawlerRequestBuilder {
                 }
             }
         }
+        // 驾意险处理
+        List<Map<String, Object>> unionRMsgVoList = data.getUnionRMsgVoList();
+        if(!unionRMsgVoList.isEmpty()){
+            List<Map<String, Object>> newPrpCunion =new ArrayList<>();
+            for(Map<String, Object> map :unionRMsgVoList){
+                Map m = new HashMap<>();
+                m.put("uProposalNo",map.get("uProposalNo"));
+                newPrpCunion.add(m);
+            }
+
+            request.setPrpCunion(newPrpCunion);
+        }
 
         prpCmain.put("sumPrem", sumPrem);
         prpCmain.put("sumPremTotal", sumPrem);

+ 20 - 0
tenant/insurance/quotation-renbaonew/src/main/java/com/jzg/quotation/renbaonew/crawler/service/impl/RenBaoNewCrawlerRequestImpl.java

@@ -836,6 +836,7 @@ public class RenBaoNewCrawlerRequestImpl implements RenBaoNewCrawlerRequest {
             Double ciPremium = 0.0;
             Double biPremium = 0.0;
             Double vvTax = 0.0;
+            Double niPremium = 0.0;
             QuoteResultsVo.CostsVo costs = new QuoteResultsVo.CostsVo();
 
             for (RenBaoNewCrawlerSaveResponse.CarQuoteTransProposalRsp rsp :
@@ -897,9 +898,28 @@ public class RenBaoNewCrawlerRequestImpl implements RenBaoNewCrawlerRequest {
             if (biPremium > 0 ) {
                 costs.setBiPremium(biPremium.toString());
             }
+            List<Map<String, Object>> unionRMsgVoList = quoteResponse.getData().getUnionRMsgVoList();
+            if(!unionRMsgVoList.isEmpty()){
+                if (unionRMsgVoList != null) {
+                    for (Map<String, Object> underwriting : unionRMsgVoList) {
+                        Object val = underwriting.get("uPremiums");
+                        if (val != null) {
+                            String strVal = val.toString();
+                            niPremium += Double.parseDouble(strVal);
+                        }
+                    }
+                    totalPremium += niPremium;
+
+                    RiskInfoVo jyRiskInfoVo = new RiskInfoVo();
+                    jyRiskInfoVo.setRiskCode(quoteVo.getAccidentalDrivings().get(0).getProductCode());
+                    resultsVo.setJyRiskInfoVo(jyRiskInfoVo);
+                    resultsVo.setJyPolicyNumber(unionRMsgVoList.get(0).get("uProposalNo").toString());
+                }
+            }
 
             costs.setSumPremium(totalPremium.toString());
             costs.setVvTax(vvTax.toString());
+            costs.setNiPremium(niPremium.toString());
             resultsVo.setCosts(costs);
         }
 

+ 7 - 3
tenant/organization/src/main/java/com/jzg/organization/controller/PtlAgreementCostController.java

@@ -6,10 +6,14 @@ import com.jzg.commons.aop.OperatorTrajectory;
 import com.jzg.commons.constants.OperationTypeEnum;
 import com.jzg.commons.core.page.HttpResult;
 import com.jzg.commons.entity.dto.*;
+import com.jzg.commons.entity.po.PtlAgreementCostRule;
+import com.jzg.commons.entity.po.PtlAgreementCostType;
 import com.jzg.commons.entity.vo.CostsListVo;
 import com.jzg.commons.entity.vo.PtlAgreementCostTypeVo;
 import com.jzg.commons.entity.vo.PtlAgreementCostVo;
 import com.jzg.organization.mapper.PtlAgreementCostMapper;
+import com.jzg.organization.mapper.PtlAgreementCostRuleMapper;
+import com.jzg.organization.mapper.PtlAgreementCostTypeMapper;
 import com.jzg.organization.service.PtlAgreementCostService;
 import com.jzg.organization.service.PtlAgreementCostTypeService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -35,21 +39,21 @@ public class PtlAgreementCostController {
     @Autowired
     private PtlAgreementCostTypeService ptlAgreementCostTypeService;
 
-    @OperatorTrajectory(moduleName = "报价配置/费用管理/新增费用",OperationDict = OperationTypeEnum.IN_AGREEMENT,mapperClass = PtlAgreementCostMapper.class,entityClass = PtlAgreemenCostAdd.class)
+    @OperatorTrajectory(moduleName = "报价配置/费用管理/新增费用",OperationDict = OperationTypeEnum.IN_AGREEMENT, type = "C", mapperClass = {PtlAgreementCostMapper.class, PtlAgreementCostTypeMapper.class, PtlAgreementCostRuleMapper.class},entityClass = {PtlAgreemenCostAdd.class, PtlAgreementCostType.class, PtlAgreementCostRule.class}, subFieldNames = {"ptlAgreementCostTypeList", "ruleConditions"}, subForeignKey = "ptlAgreementCostId", enrichConfig = "com.jzg.organization.mapper.PtlAgreementMapper:ptlAgreementId:agreementCode:ptlAgreementCode,agreementTitle:ptlAgreementName")
     @Operation(summary = "协议费用管理新增")
     @PostMapping("save")
     public HttpResult<String> add(@RequestBody @Valid PtlAgreemenCostAdd ptlAgreemenCostAdd){
         return ptlAgreementCostService.save(ptlAgreemenCostAdd);
     }
 
-    @OperatorTrajectory(moduleName = "报价配置/费用管理/修改费用",OperationDict = OperationTypeEnum.EDITED_AGREEMENT, type = "U", mapperClass = PtlAgreementCostMapper.class,entityClass = PtlAgreementCostEnd.class)
+    @OperatorTrajectory(moduleName = "报价配置/费用管理/修改费用",OperationDict = OperationTypeEnum.EDITED_AGREEMENT, type = "U" , mapperClass = {PtlAgreementCostMapper.class, PtlAgreementCostTypeMapper.class, PtlAgreementCostRuleMapper.class},entityClass = {PtlAgreementCostEnd.class, PtlAgreementCostType.class, PtlAgreementCostRule.class}, subFieldNames = {"ptlAgreementCostTypeList", "ruleConditions"}, subForeignKey = "ptlAgreementCostId", enrichConfig = "com.jzg.organization.mapper.PtlAgreementMapper:ptlAgreementId:agreementCode:ptlAgreementCode,agreementTitle:ptlAgreementName")
     @Operation(summary = "协议费用管理修改")
     @PostMapping("update")
     public HttpResult<String> update(@RequestBody @Valid PtlAgreementCostEnd costEndList){
         return ptlAgreementCostService.update(costEndList);
     }
 
-    @OperatorTrajectory(moduleName = "报价配置/费用管理/删除费用",OperationDict = OperationTypeEnum.DELETE, type = "D", mapperClass = PtlAgreementCostMapper.class,entityClass = PtlAgreementCostVo.class)
+    @OperatorTrajectory(moduleName = "报价配置/费用管理/删除费用",OperationDict = OperationTypeEnum.DELETE, type = "D", mapperClass = {PtlAgreementCostMapper.class, PtlAgreementCostTypeMapper.class, PtlAgreementCostRuleMapper.class},entityClass = {PtlAgreementCostEnd.class, PtlAgreementCostType.class, PtlAgreementCostRule.class}, subFieldNames = {"ptlAgreementCostTypeList", "ruleConditions"}, subForeignKey = "ptlAgreementCostId", enrichConfig = "com.jzg.organization.mapper.PtlAgreementMapper:ptlAgreementId:agreementCode:ptlAgreementCode,agreementTitle:ptlAgreementName")
     @Operation(summary = "协议费用管理批量删除")
     @PostMapping("deleteByIds")
     public HttpResult deleteById(@RequestBody @NotEmpty(message = "id不能为空") List<String> ids){

+ 4 - 1
tenant/organization/src/main/java/com/jzg/organization/service/impl/PtlAgreementCostServiceImpl.java

@@ -46,6 +46,7 @@ import org.springframework.util.CollectionUtils;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -114,6 +115,7 @@ public class PtlAgreementCostServiceImpl extends ServiceImpl<PtlAgreementCostMap
             log.info("费用管理:保存数据不能为空");
             return HttpResult.error("保存数据不能为空");
         }
+        List<String> createdIds = new ArrayList<>();
         for (CostAdd costAdd : costAddList) {
             for (PtlAgreementCostAdd ptlAgreementCostAdd : costAdd.getPtlAgreementCostAddList()){
                 if (ListUtils.hasDuplicates(ptlAgreementCostAdd.getRuleConditions())){
@@ -139,6 +141,7 @@ public class PtlAgreementCostServiceImpl extends ServiceImpl<PtlAgreementCostMap
                     log.error("插入操作失败");
                     throw new RuntimeException(ReturnInformation.FAILURE_MESSAGE);
                 }
+                createdIds.add(ptlAgreementCost.getId());
                 if (!CollectionUtils.isEmpty(ptlAgreementCostAdd.getPtlAgreementCostTypeList())) {
                     costTypeService.saveOrUpdateCostType(BeanUtil.copyToList(ptlAgreementCostAdd.getPtlAgreementCostTypeList(), PtlAgreementCostType.class), ptlAgreementCost.getId());
                 }
@@ -148,7 +151,7 @@ public class PtlAgreementCostServiceImpl extends ServiceImpl<PtlAgreementCostMap
                 log.info("保存成功");
             }
         }
-        return HttpResult.ok(ReturnInformation.SUCCESS_MESSAGE);
+        return HttpResult.ok(ReturnInformation.SUCCESS_MESSAGE, String.join(",", createdIds));
     }
 
     /**