From b33e804d5f758b753a2d51ea0c43d4ef24503117 Mon Sep 17 00:00:00 2001 From: cwchen <1048842385@qq.com> Date: Mon, 15 Dec 2025 14:12:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=9B=E6=A0=87=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/resources/mapper/AnalysisMapper.xml | 28 +- .../domain/ocr/vo/TwoAnalysisResponse.java | 5 + .../consumer/RabbitMQConsumerService.java | 313 +++++++++++++++++- 3 files changed, 338 insertions(+), 8 deletions(-) diff --git a/bonus-analysis/src/main/resources/mapper/AnalysisMapper.xml b/bonus-analysis/src/main/resources/mapper/AnalysisMapper.xml index 2a467aa..6ffe3fd 100644 --- a/bonus-analysis/src/main/resources/mapper/AnalysisMapper.xml +++ b/bonus-analysis/src/main/resources/mapper/AnalysisMapper.xml @@ -89,9 +89,31 @@ - UPDATE tb_pro SET pro_name = #{proName},pro_introduction = #{proIntroduction}, - pro_code = #{proCode},tenderer = #{tenderer},agency = #{agency},bid_opening_time = #{bidOpeningTime}, - bid_opening_method = #{bidOpeningMethod} WHERE pro_id = #{proId} + UPDATE tb_pro + + + pro_name = #{proName}, + + + pro_introduction = #{proIntroduction}, + + + pro_code = #{proCode}, + + + tenderer = #{tenderer}, + + + agency = #{agency}, + + + bid_opening_time = #{bidOpeningTime}, + + + bid_opening_method = #{bidOpeningMethod}, + + + WHERE pro_id = #{proId} diff --git a/bonus-common/src/main/java/com/bonus/common/domain/ocr/vo/TwoAnalysisResponse.java b/bonus-common/src/main/java/com/bonus/common/domain/ocr/vo/TwoAnalysisResponse.java index cff8cd5..2e34574 100644 --- a/bonus-common/src/main/java/com/bonus/common/domain/ocr/vo/TwoAnalysisResponse.java +++ b/bonus-common/src/main/java/com/bonus/common/domain/ocr/vo/TwoAnalysisResponse.java @@ -19,4 +19,9 @@ public class TwoAnalysisResponse { private Long id; private String jsonStr; + + public TwoAnalysisResponse(Long id, String jsonStr) { + this.id = id; + this.jsonStr = jsonStr; + } } diff --git a/bonus-rabbitmq/src/main/java/com/bonus/rabbitmq/consumer/RabbitMQConsumerService.java b/bonus-rabbitmq/src/main/java/com/bonus/rabbitmq/consumer/RabbitMQConsumerService.java index ed8bd4b..d762b28 100644 --- a/bonus-rabbitmq/src/main/java/com/bonus/rabbitmq/consumer/RabbitMQConsumerService.java +++ b/bonus-rabbitmq/src/main/java/com/bonus/rabbitmq/consumer/RabbitMQConsumerService.java @@ -1,8 +1,10 @@ package com.bonus.rabbitmq.consumer; import com.bonus.analysis.service.IASAnalysisService; +import com.bonus.common.domain.analysis.dto.AnalysisProDto; import com.bonus.common.domain.analysis.po.ProComposition; import com.bonus.common.domain.analysis.vo.AnalysisLabelItemOcrVo; +import com.bonus.common.domain.analysis.vo.AnalysisVo; import com.bonus.common.domain.ocr.dto.AnalysisOcrRequest; import com.bonus.common.domain.ocr.vo.AnalysisResponse; import com.bonus.common.domain.ocr.vo.TwoAnalysisResponse; @@ -11,9 +13,13 @@ import com.bonus.common.utils.FileUtil; import com.bonus.file.config.MinioConfig; import com.bonus.file.util.MinioUtil; import com.bonus.ocr.service.AnalysisOcrService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; @@ -26,7 +32,11 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.sql.Array; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -40,8 +50,6 @@ import java.util.stream.Collectors; @Slf4j public class RabbitMQConsumerService { - private final ObjectMapper objectMapper = new ObjectMapper(); - @Resource private MinioConfig minioConfig; @@ -54,6 +62,19 @@ public class RabbitMQConsumerService { @Resource(name = "IASAnalysisService") private IASAnalysisService analysisService; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /**项目相关字段*/ + private static final String[] PRO_LABEL_ARR = {"PRO_NAME","PRO_INTRODUCTION","PRO_CODE","TENDERER","AGENCY","BID_OPENING_TIME","BID_OPENING_METHOD"}; + private static final String[] BID_LABEL_ARR = {"BID_SECTION_LIST"}; + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒"); + + // 正则表达式:匹配 "开标时间:" 后面的日期时间部分 + // 它会捕获直到第一个分号 (;) 或字符串结束前的所有内容 + // 匹配中文日期格式: YYYY年MM月DD日HH时MM分SS秒 + private static final Pattern DATE_PATTERN = + Pattern.compile("开标时间[:|:](\\d{4}年\\d{2}月\\d{2}日\\d{2}时\\d{2}分\\d{2}秒)"); @RabbitListener( queues = "myQueue", @@ -207,10 +228,24 @@ public class RabbitMQConsumerService { throw new RuntimeException("招标解析算法服务返回结果为空"); } log.info("OCR识别成功 - 数据: {}", ocrResponse2.getData()); - Map data = ocrResponse2.getData(); - List analysisDataList = new ArrayList<>(); + List analysisDataList = processFileContent(objectMapper.writeValueAsString(ocrResponse2)); + + // 查询项目相关数据 + if(message.getCompositionType() == 1){ + List> proMapList = convertData(labelItemVoList,1); + List> newProMapList = mergeData(proMapList, analysisDataList); + Map proMap = extractAllNonIdKeys(newProMapList); + AnalysisProDto analysisProDto = convertMapToAnalysisVo(proMap); + analysisProDto.setProId(message.getProId()); + // 更新项目数据 + analysisService.editProData(analysisProDto); + List> bidMapList = convertData(labelItemVoList,2); + List> newBidMapList = mergeData(bidMapList, analysisDataList); + } // 更新解析标签数据 - analysisService.updateProBidAnalysisData(message, analysisDataList); + if(CollectionUtils.isNotEmpty(analysisDataList)) { + analysisService.updateProBidAnalysisData(message, analysisDataList); + } return true; } catch (IOException e) { log.error("OCR识别失败", e); @@ -218,6 +253,62 @@ public class RabbitMQConsumerService { } } + public static List processFileContent(String fileContent){ + + try { + // Jackson ObjectMapper 是 JSON 序列化和反序列化的核心 + List resultList = new ArrayList<>(); + + // 1. 解析整个文件内容 + JsonNode rootNode = objectMapper.readTree(fileContent); + + // 2. 导航到 "data" 节点 + JsonNode dataNode = rootNode.path("data"); + + if (dataNode.isMissingNode() || !dataNode.isObject()) { + return resultList; + } + // 3. 遍历 'data' 节点下的所有属性(即每个分析结果对象) + // dataNode.fields() 返回一个迭代器,包含属性名和对应的值节点 + Iterator> fields = dataNode.fields(); + + while (fields.hasNext()) { + Map.Entry field = fields.next(); + JsonNode analysisObjectNode = field.getValue(); // 获取内部对象,如 BID_VALIDITY_WAYDYC 的值 + + if (!analysisObjectNode.isObject()) { + continue; // 跳过非对象属性 + } + + // 将 JsonNode 强制转换为 ObjectNode,以便进行字段操作(如移除) + ObjectNode originalObjectNode = (ObjectNode) analysisObjectNode; + + // 4. 提取 'id' 字段 + JsonNode idNode = originalObjectNode.get("id"); + if (idNode == null || !idNode.isNumber()) { + System.err.println("警告:对象缺少或 'id' 不是数字,跳过该项。"); + continue; + } + Long id = idNode.asLong(); + + // 5. 从对象中移除 'id' 字段 + // **注意:** 此操作会修改 originalObjectNode,但这是我们所需要的。 + originalObjectNode.remove("id"); + + // 6. 将剩余的对象内容(即移除了id后的所有内容)转换成 JSON 字符串 + String jsonStr = objectMapper.writeValueAsString(originalObjectNode); + + // 7. 创建 TwoAnalysisResponse 对象并添加到列表 + resultList.add(new TwoAnalysisResponse(id, jsonStr)); + } + + return resultList; + } catch (JsonProcessingException e) { + log.error(e.getMessage(), e); + return null; + } + } + /** * 构建招标解析算法服务请求 - 一次处理 * @@ -283,6 +374,218 @@ public class RabbitMQConsumerService { return 1; // 规则: 剩下的情况也就是都是 1 了 } + /** + * 根据要求筛选和转换数据列表。 + * + * @param labelItemVoList 输入的列表 + * @return 转换后的 List> + */ + public List> convertData(List labelItemVoList,int type) { + // 1. 定义目标键数组 (转换为小写) + // 这是一个优化步骤,避免在循环中重复转换大小写 + Map lowerCaseKeyMap = new HashMap<>(); + for (String key : type == 1 ? PRO_LABEL_ARR : BID_LABEL_ARR) { + lowerCaseKeyMap.put(key, key.toLowerCase()); + } + List> resultList = new ArrayList<>(); + + // 2. 遍历输入列表 + for (AnalysisLabelItemOcrVo vo : labelItemVoList) { + // 3. 筛选数据: analysisLevel == 3 + if (vo.getAnalysisLevel() != null && vo.getAnalysisLevel() == 3) { + + String targetField = vo.getTargetField(); + + // 确保 targetField 对应的值在我们的目标数组中 + if (lowerCaseKeyMap.containsKey(targetField)) { + + // 4. 提取数据和转换 + Map resultMap = new HashMap<>(); + + // 获取小写键,例如 "PRO_NAME" -> "pro_name" + String lowerCaseKey = lowerCaseKeyMap.get(targetField); + + // 5. 构建 Map: { "id": vo.getId(), "pro_name": targetFieldValue } + // 注意:根据您的描述 "取出对应的 数据 的 id 和 proLabelArr 中 的参数 改为(小写) 作为 key 形成 list map" + // 结果Map的结构可能应该是 { "pro_name": vo.getId() },表示 pro_name 对应的值是这个对象的 id。 + // 我提供两种可能的解释: + + // 【解释 A:Map 中只包含一个键值对,键是小写参数,值是 ID】 +// resultMap.put(lowerCaseKey, vo.getId()); + + // 【解释 B (更常见的数据结构):Map 中包含 ID 字段和目标字段】 + resultMap.put("id", vo.getId()); // 增加 id 键 + resultMap.put(lowerCaseKey, targetField); // 假设 targetField 字符串就是需要的值 + // 5. 收集结果 + resultList.add(resultMap); + } + } + } + + return resultList; + } + + /** + * 根据 ID 匹配,将 analysisDataList 中的 jsonStr 数据填充到 proMapList 对应的 Map 中。 + * * @param proMapList 包含待填充 Map 的列表 (应包含 "id" 键)。 + * @param analysisDataList 包含 JSON 字符串数据的列表。 + * @return 填充完成的 proMapList。 + */ + public List> mergeData( + List> proMapList, + List analysisDataList) { + + // 1. 构建查找表 (基于 ID 的 Map) + // 假设 proMapList 中的每个 Map 都包含一个 "id" 键 + Map> proMapLookup = proMapList.stream() + .filter(m -> m.containsKey("id")) + .collect(Collectors.toMap( + // 键:从 Map 中取出 Long 类型的 id,并将其转换为 String + m -> String.valueOf(m.get("id")), + // 值:Map 本身 + m -> m, + // 处理 ID 冲突:如果两个 Map 具有相同的 "id",保留现有的那个 + (existing, replacement) -> existing + )); + + // 2. 遍历分析列表 + for (TwoAnalysisResponse analysisData : analysisDataList) { + String id = analysisData.getId().toString(); + String jsonStr = analysisData.getJsonStr(); + + // 检查 ID 是否存在于查找表中 + Map targetMap = proMapLookup.get(id); + + if (targetMap != null && jsonStr != null && !jsonStr.trim().isEmpty()) { + try { + // 3. 解析 JSON 字符串为 Map + // 使用 raw type 以适应不同的值类型 + Map sourceJsonMap = + objectMapper.readValue(jsonStr, new com.fasterxml.jackson.core.type.TypeReference>() {}); + + // 4. 匹配与填充 + for (Map.Entry entry : sourceJsonMap.entrySet()) { + String originalKey = entry.getKey(); + Object value = entry.getValue(); + + // 将 JSON 键转换为小写,与 proMapList 的键保持一致 + String lowerCaseKey = originalKey.toLowerCase(); + + // 仅填充除 "id" 以外的键值对(根据您的要求) + if (!"id".equals(lowerCaseKey)) { + // 填充或覆盖目标 Map 中的值 + targetMap.put(lowerCaseKey, value); + } + } + + } catch (JsonProcessingException e) { + // 处理 JSON 解析错误,例如打印日志或跳过 + System.err.println("Error parsing JSON for ID " + id + ": " + e.getMessage()); + } + } + } + + // proMapList 已经在循环中被修改(因为 Map 是引用类型),可以直接返回 + return proMapList; + } + + /** + * 1. 转换 newProMapList 中 "bid_opening_time" 字段的值: + * - 从复杂字符串中提取日期时间。 + * - 将提取到的日期时间转换为 Date 类型。 + * 2. 提取所有 Map 里除 "id" 以外的键,组成一个新的 Map 对象。 + * + * @param newProMapList 包含数据的列表 + * @return 包含所有不重复键的新 Map (键为字段名,值为 null) + */ + public Map extractAllNonIdKeys(List> newProMapList) { + + // --- 步骤 1: 复杂日期类型转换 --- + processComplexDateConversion(newProMapList); + + // --- 步骤 2: 提取不重复的键 (原有的逻辑) --- + // (省略与上一个回答中相同的键提取逻辑,因为它没有变化) + Map allKeysMap = new HashMap<>(); + + if (newProMapList == null || newProMapList.isEmpty()) { + return allKeysMap; + } + + for (Map map : newProMapList) { + if (map != null) { + for (String key : map.keySet()) { + if (!"id".equals(key)) { + allKeysMap.put(key, null); + } + } + } + } + + return allKeysMap; + } + + /** + * 辅助方法:处理 "bid_opening_time" 字段的复杂日期字符串提取和转换 + */ + private void processComplexDateConversion(List> newProMapList) { + if (newProMapList == null) { + return; + } + + for (Map map : newProMapList) { + if (map != null && map.containsKey("bid_opening_time")) { + Object timeValue = map.get("bid_opening_time"); + + if (timeValue instanceof String) { + String fullTimeStr = (String) timeValue; + + // 1. 使用正则表达式查找匹配的日期时间字符串 + Matcher matcher = DATE_PATTERN.matcher(fullTimeStr); + + if (matcher.find()) { + // 提取捕获组 1 中的日期时间 (例如:"2025年06月23日15时00分00秒") + String dateOnlyStr = matcher.group(1); + + try { + // 2. 尝试将提取出的字符串解析为 Date 对象 + Date parsedDate = DATE_FORMAT.parse(dateOnlyStr); + + // 3. 替换 Map 中的值,完成转换 + map.put("bid_opening_time", parsedDate); + + } catch (ParseException e) { + // 如果解析失败,可能是日期格式有微小差异 + System.err.println("Warning: Could not parse extracted date string '" + dateOnlyStr + "'. Error: " + e.getMessage()); + } + } else { + // 如果正则表达式没有找到匹配的日期 + System.err.println("Warning: Date pattern not found in string: " + fullTimeStr); + } + } + } + } + } + + public AnalysisProDto convertMapToAnalysisVo(Map proMap) { + if (proMap == null) { + return null; + } + + // 使用 ObjectMapper 的 convertValue 方法进行转换 + // Jackson 会自动匹配 Map 的键到 AnalysisVo 的字段,并处理类型转换。 + // 注意:如果 Map 中包含 Date 对象 (如 bidOpeningTime),Jackson 会正确处理。 + // 如果 Map 中包含的日期是字符串,则需要确保 AnalysisVo 字段上添加了适当的 @JsonFormat 注解。 + try { + AnalysisProDto analysisProDto = objectMapper.convertValue(proMap, AnalysisProDto.class); + return analysisProDto; + } catch (IllegalArgumentException e) { + // 处理转换失败的情况,例如类型不匹配或字段缺失 + System.err.println("Error converting Map to AnalysisVo: " + e.getMessage()); + // 抛出异常或返回 null,取决于业务需求 + return null; + } + } + /** * 从minio中获取文件 *