检验/证书

master
chenfeng 6 months ago
parent ae0a1cadeb
commit 95a7e1f4b1
  1. 11
      yudao-module-special/yudao-module-special-biz/src/main/java/cn/iocoder/yudao/module/special/controller/admin/equipmentregistry/vo/EquipmentRegistryRespVO.java
  2. 29
      yudao-server/src/main/java/cn/iocoder/yudao/server/controller/OcrProcessController.java
  3. 39
      yudao-server/src/main/java/cn/iocoder/yudao/server/controller/vo/OcrResponseVo.java
  4. 75
      yudao-server/src/main/java/cn/iocoder/yudao/server/service/CertificateOfApprovalService.java
  5. 105
      yudao-server/src/main/java/cn/iocoder/yudao/server/service/FieldMappingService.java
  6. 70
      yudao-server/src/main/java/cn/iocoder/yudao/server/service/SpecialGasCylinderService.java
  7. 269
      yudao-server/src/main/java/cn/iocoder/yudao/server/service/SupervisionService.java

@ -6,8 +6,11 @@ import lombok.*;
import java.time.LocalDate;
import java.util.*;
import java.math.BigDecimal;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import com.alibaba.excel.annotation.*;
@Schema(description = "管理后台 - 特种设备登记 Response VO")
@ -41,11 +44,11 @@ public class EquipmentRegistryRespVO {
@Schema(description = "气瓶公称工作压力(MPa)", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("气瓶公称工作压力(MPa)")
private BigDecimal nominalWorkingPressure;
private String nominalWorkingPressure;
@Schema(description = "气瓶容积(L)", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("气瓶容积(L)")
private BigDecimal cylinderVolume;
private String cylinderVolume;
@Schema(description = "制造单位名称", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("制造单位名称")
@ -53,7 +56,7 @@ public class EquipmentRegistryRespVO {
@Schema(description = "制造日期", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("制造日期")
private LocalDate productionDate;
private String productionDate;
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "19404")
@ExcelProperty("产品编号")
@ -117,4 +120,6 @@ public class EquipmentRegistryRespVO {
@ExcelProperty("创建时间")
private LocalDateTime createTime;
private String type;
}

@ -3,7 +3,10 @@ package cn.iocoder.yudao.server.controller;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.server.controller.vo.OcrReqVO;
import cn.iocoder.yudao.server.controller.vo.OcrResVO;
import cn.iocoder.yudao.server.controller.vo.OcrResponseVo;
import cn.iocoder.yudao.server.service.OcrProcessingService;
import cn.iocoder.yudao.server.service.SupervisionService;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
@ -11,7 +14,16 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.security.PermitAll;
import javax.imageio.ImageIO;
import javax.validation.Valid;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
/**
* 默认 Controller解决部分 module 未开启时的 404 提示
@ -24,8 +36,25 @@ import javax.validation.Valid;
public class OcrProcessController {
@Autowired
private OcrProcessingService ocrProcessingService;
@Autowired
private SupervisionService supervisionService;
@PostMapping("/admin-api/analyze")
public CommonResult<OcrResVO> processOcrResult(@Valid @RequestBody OcrReqVO request) {
return CommonResult.success(ocrProcessingService.process(request.getFilePath()));
}
/**
* 获取识别结果
*
* @param param 请求参数
* @return 识别结果
*/
@PostMapping("/admin-api/getIdentifyAndObtain")
@PermitAll
public CommonResult<Object> getIdentifyAndObtain(@RequestBody Map<String, Object> param) {
return supervisionService.getIdentifyAndObtain(param);
}
}

@ -0,0 +1,39 @@
package cn.iocoder.yudao.server.controller.vo;
import lombok.Data;
import java.util.List;
@Data
public class OcrResponseVo {
private String logId;
private Result result;
private int errorCode;
private String errorMsg;
@Data
public static class Result {
private List<TableRecResult> tableRecResults;
}
@Data
public static class TableRecResult {
private PrunedResult prunedResult;
}
@Data
public static class PrunedResult {
private List<TableRes> table_res_list;
private TableOcrPred overall_ocr_res;
}
@Data
public static class TableRes {
private TableOcrPred table_ocr_pred;
}
@Data
public static class TableOcrPred {
private List<String> rec_texts;
}
}

@ -0,0 +1,75 @@
package cn.iocoder.yudao.server.service;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.module.special.controller.admin.equipmentregistry.vo.EquipmentRegistryRespVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
@Service
public class CertificateOfApprovalService {
@Autowired
private FieldMappingService fieldMappingService;
/**
* 合格证书
*
* @param texts
* @return
*/
public EquipmentRegistryRespVO getCertificateOfApproval(List<String> texts) {
String keyName = extractManufacturer(texts, "燃气系统安装出厂检验质量证明书", 0);
Map<String, String> fieldMap = fieldMappingService.getCertificateMapping(keyName, texts);
EquipmentRegistryRespVO equipmentRegistryRespVO = new EquipmentRegistryRespVO();
equipmentRegistryRespVO.setEquipmentType("特种气瓶");
equipmentRegistryRespVO.setProductName("车用气瓶");
equipmentRegistryRespVO.setCylinderQuantity(1);
String fillingMedium = extractManufacturer(texts, fieldMap.get("fillingMedium"), 2);
if (ObjectUtil.isNotEmpty(fillingMedium)) {
//充装介质
equipmentRegistryRespVO.setFillingMedium(fillingMedium);
}
String nominalWorkingPressure = extractManufacturer(texts, fieldMap.get("nominalWorkingPressure"), 2);
if (ObjectUtil.isNotEmpty(nominalWorkingPressure)) {
//气瓶公称工作压力
equipmentRegistryRespVO.setNominalWorkingPressure(nominalWorkingPressure);
}
String cylinderVolume = extractManufacturer(texts, fieldMap.get("cylinderVolume"), 2);
if (ObjectUtil.isNotEmpty(cylinderVolume)) {
//气瓶容积
equipmentRegistryRespVO.setCylinderVolume(cylinderVolume);
}
String productId = extractManufacturer(texts, fieldMap.get("productId"), 1);
if (ObjectUtil.isNotEmpty(productId)) {
//产品编号
equipmentRegistryRespVO.setProductId(productId);
}
return equipmentRegistryRespVO;
}
public static String extractManufacturer(List<String> recTexts, String keyword, int offset) {
// 防御性检查:列表为空或为null时直接返回null
if (recTexts == null || recTexts.isEmpty()) {
return null;
}
// 使用 Stream 查找关键词位置
int index = IntStream.range(0, recTexts.size())
.filter(i -> recTexts.get(i).contains(keyword))
.findFirst()
.orElse(-1); // 未找到返回-1
// 检查是否找到关键词,且偏移后不越界
if (index != -1 && index + offset < recTexts.size()) {
return recTexts.get(index + offset);
}
return null; // 未找到或越界时返回null
}
}

@ -0,0 +1,105 @@
package cn.iocoder.yudao.server.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class FieldMappingService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 检验
private static final List<String> MANUFACTURER_CANDIDATES = Arrays.asList(
"制造单位名称", "制造单位", "单位名称"
);
private static final List<String> PRODUCTION_DATE_CANDIDATES = Arrays.asList(
"制造日期", "生产日期"
);
private static final List<String> SUPERVISION_AGENCY_CANDIDATES = Arrays.asList(
"监督检验机构", "监检机构", "监检单位", "监督检验单位"
);
//合格证书
//充装介质
private static final List<String> MANUFACTURER_CANDIDATES_ZS = Arrays.asList(
"充装介质", "介质"
);
//气瓶公称工作压力
private static final List<String> PRODUCTION_DATE_CANDIDATES_ZS = Arrays.asList(
"工作压力", "公称压力"
);
//气瓶容积
private static final List<String> SUPERVISION_AGENCY_CANDIDATES_ZS = Arrays.asList(
"公称容积", "气瓶容积", "容积"
);
//产品编号
private static final List<String> SUPERVISION_AGENCY_CANDIDATES_BH_ZS = Arrays.asList(
"产品编号", "产品编码", "编号", "编码"
);
/**
* 获取公司字段名映射优先从缓存读取适用于监督检验证书
*/
public Map<String, String> getFieldMapping(String companyId, List<String> texts) {
String redisKey = "special_equipment:field_mapping:" + companyId;
// 1. 尝试从缓存读取
Map<String, String> cachedMap = (Map<String, String>) redisTemplate.opsForValue().get(redisKey);
if (cachedMap != null) {
return cachedMap;
}
// 2. 缓存未命中,自动识别字段名
Map<String, String> fieldMap = new HashMap<>();
fieldMap.put("manufacturerField", detectField(texts, MANUFACTURER_CANDIDATES));
fieldMap.put("productionDateField", detectField(texts, PRODUCTION_DATE_CANDIDATES));
fieldMap.put("supervisionAgencyField", detectField(texts, SUPERVISION_AGENCY_CANDIDATES));
// 3. 存入缓存(有效期 30 天)
redisTemplate.opsForValue().set(redisKey, fieldMap, 365, TimeUnit.DAYS);
return fieldMap;
}
/**
* 获取公司字段名映射优先从缓存读取适用于合格证书
*/
public Map<String, String> getCertificateMapping(String companyId, List<String> texts) {
String redisKey = "special_equipment:field_mapping:" + companyId;
// 1. 尝试从缓存读取
Map<String, String> cachedMap = (Map<String, String>) redisTemplate.opsForValue().get(redisKey);
if (cachedMap != null) {
return cachedMap;
}
// 2. 缓存未命中,自动识别字段名
Map<String, String> fieldMap = new HashMap<>();
fieldMap.put("fillingMedium", detectField(texts, MANUFACTURER_CANDIDATES_ZS));
fieldMap.put("nominalWorkingPressure", detectField(texts, PRODUCTION_DATE_CANDIDATES_ZS));
fieldMap.put("cylinderVolume", detectField(texts, SUPERVISION_AGENCY_CANDIDATES_ZS));
fieldMap.put("productId", detectField(texts, SUPERVISION_AGENCY_CANDIDATES_BH_ZS));
// 3. 存入缓存(有效期 30 天)
redisTemplate.opsForValue().set(redisKey, fieldMap, 365, TimeUnit.DAYS);
return fieldMap;
}
/**
* 自动识别字段名
* 如果没有匹配到则返回候选列表的第一个值作为默认值
*/
private String detectField(List<String> texts, List<String> candidates) {
return candidates.stream()
.filter(candidate -> texts.stream().anyMatch(text -> text.contains(candidate)))
.findFirst()
.orElse(candidates.get(0)); // 返回候选列表的第一个值作为默认值
}
}

@ -0,0 +1,70 @@
package cn.iocoder.yudao.server.service;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.special.controller.admin.equipmentregistry.vo.EquipmentRegistryRespVO;
import com.baomidou.mybatisplus.generator.IFill;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.IntStream;
@Service
public class SpecialGasCylinderService {
@Autowired
private FieldMappingService fieldMappingService;
/**
* 监督检验证书
*
* @param texts
* @return
*/
public EquipmentRegistryRespVO getSpecialGasCylinder(List<String> texts) {
String keyName = extractManufacturer(texts, "监督检验机构", 0);
Map<String, String> fieldMap = fieldMappingService.getFieldMapping(keyName, texts);
EquipmentRegistryRespVO equipmentRegistryRespVO = new EquipmentRegistryRespVO();
equipmentRegistryRespVO.setEquipmentType("特种气瓶");
equipmentRegistryRespVO.setProductName("车用气瓶");
String manufacturerField = extractManufacturer(texts, fieldMap.get("manufacturerField"), 1);
if (ObjectUtil.isNotEmpty(manufacturerField)) {
equipmentRegistryRespVO.setManufacturer(manufacturerField);
}
String productionDateField = extractManufacturer(texts, fieldMap.get("productionDateField"), 1);
if (ObjectUtil.isNotEmpty(productionDateField)) {
equipmentRegistryRespVO.setProductionDate(productionDateField);
}
String supervisionAgencyField = extractManufacturer(texts, fieldMap.get("supervisionAgencyField"), 0);
if (ObjectUtil.isNotEmpty(supervisionAgencyField)) {
equipmentRegistryRespVO.setSupervisionAgency(supervisionAgencyField.replace(fieldMap.get("supervisionAgencyField"), ""));
}
return equipmentRegistryRespVO;
}
public static String extractManufacturer(List<String> recTexts, String keyword, int offset) {
// 防御性检查:列表为空或为null时直接返回null
if (recTexts == null || recTexts.isEmpty()) {
return null;
}
// 使用 Stream 查找关键词位置
int index = IntStream.range(0, recTexts.size())
.filter(i -> recTexts.get(i).contains(keyword))
.findFirst()
.orElse(-1); // 未找到返回-1
// 检查是否找到关键词,且偏移后不越界
if (index != -1 && index + offset < recTexts.size()) {
return recTexts.get(index + offset);
}
return null; // 未找到或越界时返回null
}
}

@ -0,0 +1,269 @@
package cn.iocoder.yudao.server.service;
import cn.hutool.core.collection.CollectionUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.special.controller.admin.equipmentregistry.vo.EquipmentRegistryRespVO;
import cn.iocoder.yudao.server.controller.vo.OcrResVO;
import cn.iocoder.yudao.server.controller.vo.OcrResponseVo;
import com.baomidou.mybatisplus.generator.IFill;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpRequest;
import sun.misc.BASE64Encoder;
/**
* 监督检验证书
*/
@Service
public class SupervisionService {
private static final Logger LOGGER = LoggerFactory.getLogger(SupervisionService.class);
private static final String UNIFIED_REQUEST_URL = "http://192.168.130.192:8080/table-recognition";
private static final Gson GSON = new Gson();
private final ReentrantLock lock = new ReentrantLock();
@Autowired
private SpecialGasCylinderService specialGasCylinderService;
@Resource
private RedisTemplate<String, OcrResponseVo> redisTemplate;
@Autowired
private CertificateOfApprovalService certificateOfApprovalService;
/**
* 获取识别结果
*
* @param param 请求参数
* @return 识别结果
*/
public CommonResult<Object> getIdentifyAndObtain(Map<String, Object> param) {
if (!validateParams(param)) {
return CommonResult.error(500, "参数异常");
}
try {
String fileName = param.get("imageUrl").toString().substring(param.get("imageUrl").toString().lastIndexOf('/') + 1);
OcrResponseVo redisValue = redisTemplate.opsForValue().get(fileName);
if (redisValue != null) {
return CommonResult.success(determineDocumentType(redisValue));
// return CommonResult.success(redisValue);
}
String postfix = getFileExtensionFromUrl(param.get("imageUrl").toString());
String imageBase64 = null;
if (Objects.equals(postfix, "pdf")) {
//pdf
imageBase64 = convertPdfToBase64(param.get("imageUrl").toString());
} else {
//图片
imageBase64 = convertImageToBase64(param.get("imageUrl").toString());
}
if (!StringUtils.hasText(imageBase64)) {
return CommonResult.error(500, "图片转换失败");
}
Map<String, Object> requestPayload = buildRequestPayload(imageBase64, postfix);
OcrResponseVo response = sendOcrRequest(requestPayload);
redisTemplate.opsForValue().set(fileName, response);
// return CommonResult.success(response);
return CommonResult.success(determineDocumentType(response));
} catch (Exception e) {
LOGGER.error("OCR识别系统异常: {}", e.getMessage(), e);
return CommonResult.error(500, "系统异常");
}
}
private Object determineDocumentType(OcrResponseVo ocrResponseVo) {
List<OcrResponseVo.TableRecResult> tableRecResults = ocrResponseVo.getResult().getTableRecResults();
Map<String, Object> param = new HashMap<>();
List<EquipmentRegistryRespVO> equipmentRegistryRespVOS = new ArrayList<>();
for (OcrResponseVo.TableRecResult result : tableRecResults) {
List<String> texts = new ArrayList<>();
//表格返回参数table_res_list
if (CollectionUtil.isNotEmpty(result.getPrunedResult().getTable_res_list())) {
texts = result.getPrunedResult().getTable_res_list().get(0).getTable_ocr_pred().getRec_texts();
}
if (null != result.getPrunedResult().getOverall_ocr_res()) {
texts.addAll(result.getPrunedResult().getOverall_ocr_res().getRec_texts());
}
EquipmentRegistryRespVO equipmentRegistryRespVO = new EquipmentRegistryRespVO();
//监督检验
boolean supervisionAndInspection = texts.stream().anyMatch(t -> t.contains("监督检验"));
if (supervisionAndInspection) {
equipmentRegistryRespVO = specialGasCylinderService.getSpecialGasCylinder(texts);
equipmentRegistryRespVO.setType("supervisionAndInspection");
}
//车用气瓶安装合格证
boolean CertificateOfApproval = texts.stream().anyMatch(t -> t.contains("燃气系统安装出厂检验质量证明书") || t.contains("充装介质"));
if (CertificateOfApproval) {
equipmentRegistryRespVO = certificateOfApprovalService.getCertificateOfApproval(texts);
equipmentRegistryRespVO.setType("certificateOfApproval");
}
equipmentRegistryRespVOS.add(equipmentRegistryRespVO);
}
param.put("extractedData", equipmentRegistryRespVOS);
return param;
}
/**
* 验证参数有效性
*/
private boolean validateParams(Map<String, Object> param) {
return !ObjectUtils.isEmpty(param) &&
!ObjectUtils.isEmpty(param.get("imageUrl")) &&
param.get("imageUrl") instanceof String;
}
/**
* 构建OCR请求负载
*/
private Map<String, Object> buildRequestPayload(String imageBase64, String postfix) {
Integer fileType = 1;
if (postfix.equals("pdf")) {
fileType = 0;
}
Map<String, Object> payload = new HashMap<>();
payload.put("file", imageBase64);
payload.put("fileType", fileType);
return payload;
}
/**
* 发送OCR请求
*/
public OcrResponseVo sendOcrRequest(Map<String, Object> payload) {
lock.lock();
try {
String requestBody = GSON.toJson(payload);
HttpResponse response = HttpRequest.post(UNIFIED_REQUEST_URL)
.header("Content-Type", "application/json")
.body(requestBody)
.timeout(500000)// TODO 先把超时时间设置长一点
.execute();
if (!response.isOk() || !StringUtils.hasText(response.body())) {
throw new ServiceException(500, "OCR服务调用失败");
}
return GSON.fromJson(response.body(), OcrResponseVo.class);
} finally {
lock.unlock();
}
}
/**
* 将图片转换为Base64编码
*/
private String convertImageToBase64(String imageUrl) throws IOException {
Objects.requireNonNull(imageUrl, "Image URL cannot be null");
try (InputStream inputStream = new URL(imageUrl).openStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
}
}
public static String convertPdfToBase64(String filePathOrUrl) throws IOException {
Objects.requireNonNull(filePathOrUrl, "PDF path/URL cannot be null");
try (InputStream inputStream = getInputStream(filePathOrUrl);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
}
}
/**
* 根据输入路径获取输入流自动区分本地文件或URL
*/
private static InputStream getInputStream(String filePathOrUrl) throws IOException {
if (filePathOrUrl.startsWith("http://") || filePathOrUrl.startsWith("https://")) {
return new URL(filePathOrUrl).openStream(); // 处理网络PDF
} else {
return new FileInputStream(filePathOrUrl); // 处理本地PDF
}
}
public static String ImageToBase64ByLocal(String imageUrl) {// 将图片文件转化为字节数组字符串,并对其进行Base64编码处理
byte[] data = null;
// 读取图片字节数组
try {
InputStream inputStream = new FileInputStream("D:\\images\\3_特种 001.jpg");
// InputStream inputStream = new URL(imageUrl).openStream();
data = new byte[inputStream.available()];
inputStream.read(data);
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
// 对字节数组Base64编码
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(data);// 返回Base64编码过的字节数组字符串
}
/**
* 从URL获取图片格式
*/
private String getImageFormat(String imageUrl) {
String lowerUrl = imageUrl.toLowerCase();
if (lowerUrl.endsWith(".png")) return "png";
if (lowerUrl.endsWith(".gif")) return "gif";
if (lowerUrl.endsWith(".bmp")) return "bmp";
return "jpg"; // 默认格式
}
public static String getFileExtensionFromUrl(String urlStr) {
try {
URL url = new URL(urlStr);
String path = url.getPath();
int lastIndex = path.lastIndexOf('.');
if (lastIndex != -1 && lastIndex < path.length() - 1) {
return path.substring(lastIndex + 1).toLowerCase();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
Loading…
Cancel
Save