招标解析

This commit is contained in:
cwchen 2025-12-15 14:12:55 +08:00
parent 8b8ba24e1c
commit b33e804d5f
3 changed files with 338 additions and 8 deletions

View File

@ -89,9 +89,31 @@
<!--更新项目数据-->
<update id="editProData">
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
<set>
<if test="proName != null and proName != ''">
pro_name = #{proName},
</if>
<if test="proIntroduction != null and proIntroduction != ''">
pro_introduction = #{proIntroduction},
</if>
<if test="proCode != null and proCode != ''">
pro_code = #{proCode},
</if>
<if test="tenderer != null and tenderer != ''">
tenderer = #{tenderer},
</if>
<if test="agency != null and agency != ''">
agency = #{agency},
</if>
<if test="bidOpeningTime != null">
bid_opening_time = #{bidOpeningTime},
</if>
<if test="bidOpeningMethod != null and bidOpeningMethod != ''">
bid_opening_method = #{bidOpeningMethod},
</if>
</set>
WHERE pro_id = #{proId}
</update>
<!--更新标段数据-->

View File

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

View File

@ -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<String, Object> data = ocrResponse2.getData();
List<TwoAnalysisResponse> analysisDataList = new ArrayList<>();
List<TwoAnalysisResponse> analysisDataList = processFileContent(objectMapper.writeValueAsString(ocrResponse2));
// 查询项目相关数据
if(message.getCompositionType() == 1){
List<Map<String, Object>> proMapList = convertData(labelItemVoList,1);
List<Map<String, Object>> newProMapList = mergeData(proMapList, analysisDataList);
Map<String, Object> proMap = extractAllNonIdKeys(newProMapList);
AnalysisProDto analysisProDto = convertMapToAnalysisVo(proMap);
analysisProDto.setProId(message.getProId());
// 更新项目数据
analysisService.editProData(analysisProDto);
List<Map<String, Object>> bidMapList = convertData(labelItemVoList,2);
List<Map<String, Object>> 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<TwoAnalysisResponse> processFileContent(String fileContent){
try {
// Jackson ObjectMapper JSON 序列化和反序列化的核心
List<TwoAnalysisResponse> 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<Map.Entry<String, JsonNode>> fields = dataNode.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> 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<Map<String, Object>>
*/
public List<Map<String, Object>> convertData(List<AnalysisLabelItemOcrVo> labelItemVoList,int type) {
// 1. 定义目标键数组 (转换为小写)
// 这是一个优化步骤避免在循环中重复转换大小写
Map<String, String> lowerCaseKeyMap = new HashMap<>();
for (String key : type == 1 ? PRO_LABEL_ARR : BID_LABEL_ARR) {
lowerCaseKeyMap.put(key, key.toLowerCase());
}
List<Map<String, Object>> 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<String, Object> 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
// 我提供两种可能的解释
// 解释 AMap 中只包含一个键值对键是小写参数值是 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<Map<String, Object>> mergeData(
List<Map<String, Object>> proMapList,
List<TwoAnalysisResponse> analysisDataList) {
// 1. 构建查找表 (基于 ID Map)
// 假设 proMapList 中的每个 Map 都包含一个 "id"
Map<String, Map<String, Object>> 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<String, Object> targetMap = proMapLookup.get(id);
if (targetMap != null && jsonStr != null && !jsonStr.trim().isEmpty()) {
try {
// 3. 解析 JSON 字符串为 Map
// 使用 raw type 以适应不同的值类型
Map<String, Object> sourceJsonMap =
objectMapper.readValue(jsonStr, new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {});
// 4. 匹配与填充
for (Map.Entry<String, Object> 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<String, Object> extractAllNonIdKeys(List<Map<String, Object>> newProMapList) {
// --- 步骤 1: 复杂日期类型转换 ---
processComplexDateConversion(newProMapList);
// --- 步骤 2: 提取不重复的键 (原有的逻辑) ---
// (省略与上一个回答中相同的键提取逻辑因为它没有变化)
Map<String, Object> allKeysMap = new HashMap<>();
if (newProMapList == null || newProMapList.isEmpty()) {
return allKeysMap;
}
for (Map<String, Object> 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<Map<String, Object>> newProMapList) {
if (newProMapList == null) {
return;
}
for (Map<String, Object> 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<String, Object> 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中获取文件
*