From aade0779777fbe34b12a13cad861db6b7639cc9b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=BD=97=E6=88=90?= <2847920761@qq.com>
Date: Fri, 18 Jul 2025 18:37:27 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=99=BE=E5=BA=A6=E4=BA=91ap?=
=?UTF-8?q?i=E7=9A=84=E4=BA=BA=E8=84=B8=E5=92=8COCR?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/main/resources/application.yml | 7 +
.../ruoyi-modules/ruoyi-system/pom.xml | 23 +-
.../common/constant/businessConstant.java | 9 +
.../common/utils/HttpClientConfig.java | 24 ++
.../common/utils/baiduUtil/BaiDuCommon.java | 101 +++++++++
.../common/utils/baiduUtil/BaiDuFace.java | 205 ++++++++++++++++++
.../common/utils/baiduUtil/BaiDuOCR.java | 204 +++++++++++++++++
.../baiduUtil/entity/AccessTokenResponse.java | 19 ++
.../baiduUtil/entity/face/ComparisonRes.java | 49 +++++
.../baiduUtil/entity/face/HumanFaceReq.java | 23 ++
.../baiduUtil/entity/face/HumanFaceRes.java | 142 ++++++++++++
.../utils/baiduUtil/entity/ocr/BankData.java | 39 ++++
.../utils/baiduUtil/entity/ocr/Field.java | 16 ++
.../baiduUtil/entity/ocr/IDCardInfo.java | 30 +++
.../utils/baiduUtil/entity/ocr/Location.java | 18 ++
.../utils/baiduUtil/entity/ocr/OcrReq.java | 24 ++
.../utils/baiduUtil/entity/ocr/Result.java | 22 ++
.../baiduUtil/entity/ocr/WordsResult.java | 33 +++
18 files changed, 987 insertions(+), 1 deletion(-)
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/constant/businessConstant.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/HttpClientConfig.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuCommon.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuFace.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuOCR.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/AccessTokenResponse.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/ComparisonRes.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/HumanFaceReq.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/HumanFaceRes.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/BankData.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Field.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/IDCardInfo.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Location.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/OcrReq.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Result.java
create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/WordsResult.java
diff --git a/xinnengyuan/ruoyi-admin/src/main/resources/application.yml b/xinnengyuan/ruoyi-admin/src/main/resources/application.yml
index a873a5c6..558f54b1 100644
--- a/xinnengyuan/ruoyi-admin/src/main/resources/application.yml
+++ b/xinnengyuan/ruoyi-admin/src/main/resources/application.yml
@@ -319,3 +319,10 @@ warm-flow:
- 255,205,23
## 已办理
- 157,255,0
+
+
+--- # 百度云配置
+baidu:
+ client:
+ id: zSB7KdLgY7a1tIEx3eTy65TE
+ secret: 5nabjclW5BWGV8UwEueDgBDmOveRVkmD
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml b/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml
index f94e10e4..d1bb30ee 100644
--- a/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml
@@ -17,7 +17,12 @@
-
+
+
+ com.alibaba
+ fastjson
+ 2.0.32
+
@@ -189,6 +194,22 @@
org.dromara
ruoyi-workflow
+
+ com.google.code.gson
+ gson
+
+
+ org.scala-lang
+ scala-library
+ 2.13.9
+ compile
+
+
+ net.java.dev.jna
+ jna-platform
+ 5.15.0
+ compile
+
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/constant/businessConstant.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/constant/businessConstant.java
new file mode 100644
index 00000000..3086e68c
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/constant/businessConstant.java
@@ -0,0 +1,9 @@
+package org.dromara.common.constant;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 10:38
+ * @Version 1.0
+ */public interface businessConstant {
+ String REDIS_BAIDU_KEY = "baidu:access_token"; //百度token存储在redis中的key
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/HttpClientConfig.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/HttpClientConfig.java
new file mode 100644
index 00000000..613b696f
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/HttpClientConfig.java
@@ -0,0 +1,24 @@
+package org.dromara.common.utils;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.net.http.HttpClient;
+import java.time.Duration;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 10:16
+ * @Version 1.0
+ *
+ * HttpClient 设计为可重用、线程安全的组件,其内部维护了连接池等资源,适合在多个接口调用中共享使用,所以交给Spring Bean 管理
+ */
+@Configuration
+public class HttpClientConfig {
+ @Bean
+ public HttpClient httpClient() {
+ return HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(10))
+ .build();
+ }
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuCommon.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuCommon.java
new file mode 100644
index 00000000..d2e41c54
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuCommon.java
@@ -0,0 +1,101 @@
+package org.dromara.common.utils.baiduUtil;
+
+import com.google.gson.Gson;
+import org.dromara.common.utils.baiduUtil.entity.AccessTokenResponse;
+import org.glassfish.jaxb.runtime.v2.runtime.reflect.opt.Const;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Value;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.concurrent.TimeUnit;
+
+import static org.dromara.common.constant.businessConstant.REDIS_BAIDU_KEY;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 9:46
+ * @Version 1.0
+ *
+ * 获取百度AccessToken
+ */
+@Service
+public class BaiDuCommon {
+ @Autowired
+ private HttpClient httpClient;
+
+ @Autowired
+ private StringRedisTemplate redisTemplate;
+
+ @Value("${baidu.client.id}")
+ private String clientId;
+
+ @Value("${baidu.client.secret}")
+ private String clientSecret;
+
+ private static final String TOKEN_URL = "https://aip.baidubce.com/oauth/2.0/token?client_id=%s&client_secret=%s&grant_type=client_credentials";
+
+ private static final Gson gson = new Gson();
+
+ /**
+ * 获取百度AccessToken,优先从Redis缓存中获取
+ */
+ public String getAccessToken() {
+ // 1. 尝试从Redis中获取
+ String cachedToken = redisTemplate.opsForValue().get(REDIS_BAIDU_KEY);
+ if (cachedToken != null) {
+ return cachedToken;
+ }
+
+ // 2. Redis中没有,加锁防止并发请求刷新Token
+ synchronized (this) {
+ // 再次检查,避免其他线程已经刷新
+ cachedToken = redisTemplate.opsForValue().get(REDIS_BAIDU_KEY);
+ if (cachedToken != null) {
+ return cachedToken;
+ }
+
+ // 3. 真正调用API获取新Token
+ String newToken = fetchAccessTokenFromBaidu();
+ if (newToken != null) {
+ // 4. 将新Token存入Redis,并设置过期时间(比实际有效期短一些)
+ redisTemplate.opsForValue().set(REDIS_BAIDU_KEY, newToken, 29, TimeUnit.DAYS);
+ return newToken;
+ }
+ }
+
+ return null; // 获取失败
+ }
+
+ /**
+ * 从百度API获取AccessToken
+ */
+ private String fetchAccessTokenFromBaidu() {
+ String url = String.format(TOKEN_URL, clientId, clientSecret);
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .header("Content-Type", "application/json")
+ .header("Accept", "application/json")
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build();
+
+ try {
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+
+ if (response.statusCode() == 200) {
+ AccessTokenResponse tokenResponse = gson.fromJson(response.body(), AccessTokenResponse.class);
+ return tokenResponse.getAccessToken();
+ } else {
+ throw new IOException("获取AccessToken失败,状态码: " + response.statusCode());
+ }
+ } catch (IOException | InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException("调用百度API获取AccessToken失败", e);
+ }
+ }
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuFace.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuFace.java
new file mode 100644
index 00000000..733f0c17
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuFace.java
@@ -0,0 +1,205 @@
+package org.dromara.common.utils.baiduUtil;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.dromara.common.utils.baiduUtil.entity.face.ComparisonRes;
+import org.dromara.common.utils.baiduUtil.entity.face.HumanFaceReq;
+import org.dromara.common.utils.baiduUtil.entity.face.HumanFaceRes;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 17:09
+ * @Version 1.0
+ * 处理百度人脸相关逻辑:人脸检测、人脸对比
+ */
+@Service
+public class BaiDuFace {
+
+ @Autowired
+ private BaiDuCommon baiDuCommon;
+ @Autowired
+ private HttpClient httpClient;
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ // 人脸识别API地址
+ private static final String FACE_DETECT_URL = "https://aip.baidubce.com/rest/2.0/face/v3/detect?access_token=%s";
+ // 人脸对比API地址
+ private static final String FACE_COMPARE_URL = "https://aip.baidubce.com/rest/2.0/face/v3/match?access_token=%s";
+
+
+
+ /**
+ * 人脸识别+人脸检测
+ * @param request 人脸请求参数
+ * @throws RuntimeException 检测失败时抛出异常(由Spring统一处理)
+ */
+ public void humanFace(HumanFaceReq request) {
+ // 1. 获取AccessToken
+ String accessToken = baiDuCommon.getAccessToken();
+ if (accessToken == null || accessToken.trim().isEmpty()) {
+ throw new RuntimeException("获取访问令牌失败:token为空");
+ }
+
+ // 2. 构建请求URL
+ String requestUrl = String.format(FACE_DETECT_URL, URLEncoder.encode(accessToken, StandardCharsets.UTF_8));
+
+ try {
+ // 3. 序列化请求参数
+ String requestBody = objectMapper.writeValueAsString(request);
+
+ // 4. 构建HTTP请求
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .uri(URI.create(requestUrl))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .build();
+
+ // 5. 发送请求并获取响应
+ HttpResponse response = httpClient.send(
+ httpRequest,
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ // 6. 解析响应结果
+ HumanFaceRes faceRep = objectMapper.readValue(response.body(), HumanFaceRes.class);
+
+ // 7. 处理API返回错误
+ if (faceRep.getErrorCode() != 0) {
+ throw new RuntimeException("错误码说明:" + faceRep.getErrorMsg());
+ }
+
+ // 8. 验证人脸信息(无人脸时抛出异常)
+ if (faceRep.getResult() == null
+ || faceRep.getResult().getFaceList() == null
+ || faceRep.getResult().getFaceList().isEmpty()) {
+ throw new RuntimeException("未检测到有效人脸信息");
+ }
+
+ // 9. 人脸质量校验
+ HumanFaceRes.FaceList faceInfo = faceRep.getResult().getFaceList().get(0);
+
+ // 9.1 人脸置信度校验(必须为1.0)
+ if (faceInfo.getFaceProbability() != 1.0) {
+ throw new RuntimeException("人脸置信度过低,请重新拍摄!");
+ }
+
+ // 9.2 真实人脸校验(排除卡通人脸)
+ HumanFaceRes.FaceType faceType = faceInfo.getFaceType();
+ if (faceType == null) {
+ throw new RuntimeException("无法判断人脸类型");
+ }
+ if ("cartoon".equals(faceType.getType())) {
+ throw new RuntimeException("请传入真实人脸,勿使用卡通图像");
+ }
+ // 校验真实人脸置信度(需≥0.8)
+ if (faceType.getProbability() < 0.8) {
+ throw new RuntimeException("人脸识别不清晰,请换个角度拍摄");
+ }
+
+ // 9.3 人脸模糊度校验(模糊度≥0.1视为模糊)
+ HumanFaceRes.Quality quality = faceInfo.getQuality();
+ if (quality == null) {
+ throw new RuntimeException("无法获取人脸质量信息");
+ }
+ if (quality.getBlur() != 0) {
+ throw new RuntimeException("人脸过于模糊,请保持镜头稳定");
+ }
+
+ // 9.4 光线校验(光线值<100视为过暗)
+ if (quality.getIllumination() < 100.0) {
+ throw new RuntimeException("光线太暗,请在光线充足的环境下拍摄");
+ }
+
+ // 9.5 人脸完整性校验(1为完整,0为不完整)
+ if (quality.getCompleteness() != 1) {
+ throw new RuntimeException("人脸未完全显示,请确保面部在框内");
+ }
+
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("请求参数序列化失败:" + e.getMessage(), e);
+ } catch (IOException e) {
+ throw new RuntimeException("IO处理异常:" + e.getMessage(), e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // 恢复中断状态
+ throw new RuntimeException("请求被中断:" + e.getMessage(), e);
+ }
+ }
+
+
+
+ /**
+ * 人脸对比(计算人脸相似度)
+ * @param requestList 人脸请求参数列表(至少包含2个人脸)
+ * @return 人脸相似度得分
+ * @throws RuntimeException 对比失败时抛出异常
+ */
+ public double comparison(List requestList) {
+ // 1. 校验请求参数
+ if (requestList == null || requestList.size() < 2) {
+ throw new RuntimeException("人脸对比至少需要2张人脸图片");
+ }
+
+ // 2. 获取AccessToken
+ String accessToken = baiDuCommon.getAccessToken();
+ if (accessToken == null || accessToken.trim().isEmpty()) {
+ throw new RuntimeException("获取访问令牌失败:token为空");
+ }
+
+ // 3. 构建请求URL
+ String requestUrl = String.format(FACE_COMPARE_URL, URLEncoder.encode(accessToken, StandardCharsets.UTF_8));
+
+ try {
+ // 4. 序列化请求参数
+ String requestBody = objectMapper.writeValueAsString(requestList);
+
+ // 5. 构建HTTP请求
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .uri(URI.create(requestUrl))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .build();
+
+ // 6. 发送请求并获取响应
+ HttpResponse response = httpClient.send(
+ httpRequest,
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ // 7. 解析响应结果
+ ComparisonRes comparisonRep = objectMapper.readValue(response.body(), ComparisonRes.class);
+
+ // 8. 处理API返回错误
+ if (comparisonRep.getErrorCode() != 0) {
+ throw new RuntimeException("人脸对比失败:" + comparisonRep.getErrorMsg()
+ + "(错误码:" + comparisonRep.getErrorCode() + ")");
+ }
+
+ // 9. 校验对比结果
+ if (comparisonRep.getResult() == null) {
+ throw new RuntimeException("人脸对比结果为空");
+ }
+
+ return comparisonRep.getResult().getScore();
+
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("请求参数序列化失败:" + e.getMessage(), e);
+ } catch (IOException e) {
+ throw new RuntimeException("IO处理异常:" + e.getMessage(), e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException("请求被中断:" + e.getMessage(), e);
+ }
+ }
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuOCR.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuOCR.java
new file mode 100644
index 00000000..65a45da6
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/BaiDuOCR.java
@@ -0,0 +1,204 @@
+package org.dromara.common.utils.baiduUtil;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.dromara.common.utils.baiduUtil.entity.ocr.IDCardInfo;
+import org.dromara.common.utils.baiduUtil.entity.ocr.OcrReq;
+import org.dromara.common.utils.baiduUtil.entity.ocr.Result;
+import org.dromara.common.utils.baiduUtil.entity.ocr.WordsResult;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 10:45
+ * @Version 1.0
+ *
+ * 处理百度OCR相关逻辑:身份证识别、银行卡识别
+ */
+@Service
+public class BaiDuOCR {
+ @Autowired
+ private BaiDuCommon baiDuCommon;
+ @Autowired
+ private HttpClient httpClient;
+ @Autowired
+ private ObjectMapper objectMapper; //ObjectMapper 是 Java 处理 JSON 的核心工具,项目中使用它进行 JSON 与 Java 对象的相互转换。
+
+
+ String baseUrlTemplate = "https://aip.baidubce.com/rest/2.0/ocr/v1/bankcard?access_token=%s";
+ String BASE_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard?access_token=%s";
+
+
+
+ /**
+ * @description 银行卡OCR识别
+ * @author 铁憨憨
+ * @date 2025/7/18 11:50
+ * @param vr 请求参数
+ * @return 识别结果Result
+ **/
+ public Result bankCardOCRRecognition(OcrReq vr) {
+ // 先从缓存里面捞取token,若为空直接抛出异常
+ String atStr = baiDuCommon.getAccessToken();
+ if (atStr == null || atStr.trim().isEmpty()) {
+ throw new RuntimeException("获取访问令牌失败:token为空");
+ }
+
+ try {
+ // 构建请求URL(包含token参数)
+ String requestUrl = String.format(baseUrlTemplate, URLEncoder.encode(atStr, StandardCharsets.UTF_8));
+
+ // 准备请求体(将请求参数转为JSON)
+ String requestBody = objectMapper.writeValueAsString(vr);
+ if (requestBody == null) {
+ throw new RuntimeException("请求参数序列化失败");
+ }
+
+ // 构建HTTP请求
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(requestUrl))
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .build();
+
+ // 发送请求并获取响应
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+
+ // 处理响应状态码(非200直接抛出异常)
+ if (response.statusCode() != 200) {
+ throw new RuntimeException("接口请求失败,状态码:" + response.statusCode() + ",响应信息:" + response.body());
+ }
+
+ // 解析响应结果(若解析失败抛出异常)
+ Result result = objectMapper.readValue(response.body(), Result.class);
+ if (result == null) {
+ throw new RuntimeException("响应结果解析失败");
+ }
+
+// // 若接口返回错误码(如百度API的error_code),抛出异常
+// if (result.getErrorCode() != null && result.getErrorCode() != 0) {
+// throw new RuntimeException("接口返回错误:" + result.getErrorMessage() + "(错误码:" + result.getErrorCode() + ")");
+// }
+
+ return result;
+
+ } catch (IOException e) {
+ // IO异常(如序列化失败、网络IO错误)
+ throw new RuntimeException("IO处理异常:" + e.getMessage(), e);
+ } catch (InterruptedException e) {
+ // 线程中断异常
+ Thread.currentThread().interrupt(); // 恢复中断状态
+ throw new RuntimeException("请求被中断:" + e.getMessage(), e);
+ }
+ }
+
+
+
+
+ /**
+ * @description 身份证OCR识别
+ * @author 铁憨憨
+ * @date 2025/7/18 16:53
+ * @param request 请求参数
+ * @return 识别结果WordsResult
+ **/
+ public WordsResult idCardOCR(OcrReq request) {
+ // 获取AccessToken,为空直接抛异常
+ String accessToken = baiDuCommon.getAccessToken();
+ if (accessToken == null || accessToken.trim().isEmpty()) {
+ throw new RuntimeException("获取访问令牌失败:token为空");
+ }
+
+ // 构建请求URL
+ String requestUrl = String.format(BASE_URL, accessToken);
+
+ try {
+ // 准备请求体(序列化失败抛异常)
+ String requestBody = objectMapper.writeValueAsString(request);
+ if (requestBody == null) {
+ throw new RuntimeException("请求参数序列化失败");
+ }
+
+ // 构建HTTP请求
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .uri(URI.create(requestUrl))
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .build();
+
+ // 发送请求并获取响应
+ HttpResponse response = httpClient.send(
+ httpRequest,
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ // 处理响应状态码(非200直接抛异常)
+ if (response.statusCode() != 200) {
+ throw new RuntimeException("接口请求失败,状态码:" + response.statusCode() + ",响应信息:" + response.body());
+ }
+
+ // 处理响应内容(去除空格并解析)
+ String responseBody = response.body().replaceAll("\\s+", "");
+ IDCardInfo idCardInfo = objectMapper.readValue(responseBody, IDCardInfo.class);
+ if (idCardInfo == null) {
+ throw new RuntimeException("响应结果解析失败");
+ }
+
+ // 检查身份证状态(状态异常由validateImageStatus抛异常)
+ validateImageStatus(idCardInfo.getImageStatus());
+
+ // 提取识别结果(结果为空抛异常)
+ WordsResult wordsResult = idCardInfo.getWordsResult();
+ if (wordsResult == null) {
+ throw new RuntimeException("未识别到身份证信息");
+ }
+
+ return wordsResult;
+
+ } catch (IOException e) {
+ // IO异常(序列化、网络IO等)
+ throw new RuntimeException("IO处理异常:" + e.getMessage(), e);
+ } catch (InterruptedException e) {
+ // 线程中断异常
+ Thread.currentThread().interrupt(); // 恢复中断状态
+ throw new RuntimeException("请求被中断:" + e.getMessage(), e);
+ }
+ }
+
+
+ /**
+ * @description 验证身份证图片状态
+ * @author 铁憨憨
+ * @date 2025/7/18 17:08
+ * @param imageStatus 图片状态
+ * @throws RuntimeException 当状态不正常时抛出运行时异常
+ **/
+ private void validateImageStatus(String imageStatus) {
+ if (imageStatus == null || "normal".equals(imageStatus)) {
+ return; // 正常状态,直接返回
+ }
+
+ // 根据不同状态码抛出不同的异常信息
+ String errorMessage = switch (imageStatus) {
+ case "reversed_side" -> "身份证正反面颠倒,请调整后重新上传";
+ case "non_idcard" -> "上传的图片中不包含身份证,请重新上传";
+ case "blurred" -> "身份证图片模糊,请重新上传清晰的图片";
+ case "other_type_card" -> "上传的不是身份证,请上传有效的身份证图片";
+ case "over_exposure" -> "身份证关键字段反光或过曝,请调整光线后重新上传";
+ case "over_dark" -> "身份证图片欠曝(亮度过低),请调整光线后重新上传";
+ default -> "未知错误:" + imageStatus;
+ };
+
+ throw new RuntimeException(errorMessage);
+ }
+
+
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/AccessTokenResponse.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/AccessTokenResponse.java
new file mode 100644
index 00000000..d0fec39d
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/AccessTokenResponse.java
@@ -0,0 +1,19 @@
+package org.dromara.common.utils.baiduUtil.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 9:56
+ * @Version 1.0
+ */
+@Data
+@Accessors(chain = true)
+public class AccessTokenResponse {
+ private String accessToken;
+ private int expiresIn;
+ private String scope;
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/ComparisonRes.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/ComparisonRes.java
new file mode 100644
index 00000000..08735652
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/ComparisonRes.java
@@ -0,0 +1,49 @@
+package org.dromara.common.utils.baiduUtil.entity.face;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 17:15
+ * @Version 1.0
+ *
+ * 人脸对比响应结果
+ */
+@Data
+public class ComparisonRes {
+ @JsonProperty("error_code")
+ private int errorCode;
+
+ @JsonProperty("error_msg")
+ private String errorMsg;
+
+ @JsonProperty("log_id")
+ private long logId;
+
+ @JsonProperty("timestamp")
+ private long timestamp;
+
+ @JsonProperty("cached")
+ private int cached;
+
+ @JsonProperty("result")
+ private CompareResult result;
+
+ @Data
+ public static class CompareResult {
+ @JsonProperty("score")
+ private double score; // 人脸相似度得分
+
+ @JsonProperty("face_list")
+ private List faceList;
+ }
+
+ @Data
+ public static class FaceToken {
+ @JsonProperty("face_token")
+ private String faceToken;
+ }
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/HumanFaceReq.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/HumanFaceReq.java
new file mode 100644
index 00000000..e3383f17
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/HumanFaceReq.java
@@ -0,0 +1,23 @@
+package org.dromara.common.utils.baiduUtil.entity.face;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 17:12
+ * @Version 1.0
+ *
+ * 人脸识别请求参数
+ */
+@Data
+public class HumanFaceReq {
+ @JsonProperty("image")
+ private String image; // 图片base64字符串
+
+ @JsonProperty("image_type")
+ private String imageType = "BASE64"; // 图片类型(固定为BASE64)
+
+ @JsonProperty("face_field")
+ private String faceField = "face_type,quality"; // 需要返回的人脸字段
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/HumanFaceRes.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/HumanFaceRes.java
new file mode 100644
index 00000000..2e4c5742
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/face/HumanFaceRes.java
@@ -0,0 +1,142 @@
+package org.dromara.common.utils.baiduUtil.entity.face;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 17:13
+ * @Version 1.0
+ *
+ * 人脸识别响应结果
+ */
+@Data
+public class HumanFaceRes {
+ @JsonProperty("error_code")
+ private int errorCode;
+
+ @JsonProperty("error_msg")
+ private String errorMsg;
+
+ @JsonProperty("log_id")
+ private long logId;
+
+ @JsonProperty("timestamp")
+ private long timestamp;
+
+ @JsonProperty("cached")
+ private int cached;
+
+ @JsonProperty("result")
+ private Result result;
+
+ @Data
+ public static class Result {
+ @JsonProperty("face_num")
+ private int faceNum;
+
+ @JsonProperty("face_list")
+ private List faceList;
+ }
+
+ @Data
+ public static class FaceList {
+ @JsonProperty("face_token")
+ private String faceToken;
+
+ @JsonProperty("location")
+ private Location location;
+
+ @JsonProperty("face_probability")
+ private double faceProbability;
+
+ @JsonProperty("angle")
+ private Angle angle;
+
+ @JsonProperty("face_type")
+ private FaceType faceType;
+
+ @JsonProperty("quality")
+ private Quality quality;
+ }
+
+ @Data
+ public static class Location {
+ @JsonProperty("left")
+ private double left;
+
+ @JsonProperty("top")
+ private double top;
+
+ @JsonProperty("width")
+ private int width;
+
+ @JsonProperty("height")
+ private int height;
+
+ @JsonProperty("rotation")
+ private int rotation;
+ }
+
+ @Data
+ public static class Angle {
+ @JsonProperty("yaw")
+ private double yaw;
+
+ @JsonProperty("pitch")
+ private double pitch;
+
+ @JsonProperty("roll")
+ private double roll;
+ }
+
+ @Data
+ public static class FaceType {
+ @JsonProperty("type")
+ private String type;
+
+ @JsonProperty("probability")
+ private double probability;
+ }
+
+ @Data
+ public static class Quality {
+ @JsonProperty("occlusion")
+ private Occlusion occlusion;
+
+ @JsonProperty("blur")
+ private double blur;
+
+ @JsonProperty("illumination")
+ private double illumination;
+
+ @JsonProperty("completeness")
+ private long completeness;
+ }
+
+ @Data
+ public static class Occlusion {
+ @JsonProperty("left_eye")
+ private double leftEye;
+
+ @JsonProperty("right_eye")
+ private double rightEye;
+
+ @JsonProperty("nose")
+ private double nose;
+
+ @JsonProperty("mouth")
+ private double mouth;
+
+ @JsonProperty("left_cheek")
+ private double leftCheek;
+
+ @JsonProperty("right_cheek")
+ private double rightCheek;
+
+ @JsonProperty("chin_contour")
+ private double chinContour;
+ }
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/BankData.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/BankData.java
new file mode 100644
index 00000000..e74f8461
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/BankData.java
@@ -0,0 +1,39 @@
+package org.dromara.common.utils.baiduUtil.entity.ocr;
+
+import com.alibaba.fastjson2.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 10:58
+ * @Version 1.0
+ *
+ * 银行卡具体信息
+ */
+@Data
+//public class BankData {
+// private String valid_date; // 有效期
+// private String bank_card_number; // 卡号
+// private String bank_name; // 银行名称
+// private int bank_card_type; // 卡类型(0未知,1借记卡,2贷记卡等)
+// private String holder_name; // 持卡人姓名(可能为空)
+//}
+
+
+public class BankData {
+
+ @JSONField(name = "bank_card_number",label = "银行卡卡号")
+ private String bankCardNumber;
+
+ @JSONField(name = "valid_date",label = "有效期")
+ private String validDate;
+
+ @JSONField(name = "bank_card_type",label = "银行卡类型,0:不能识别; 1:借记卡; 2:贷记卡(原信用卡大部分为贷记卡); 3:准贷记卡; 4:预付费卡")
+ private int bankCardType;
+
+ @JSONField(name = "bank_name",label = "银行名,不能识别时为空")
+ private String bankName;
+
+ @JSONField(name = "holder_name",label = "持卡人姓名,不能识别时为空")
+ private String holderName;
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Field.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Field.java
new file mode 100644
index 00000000..ebc77a35
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Field.java
@@ -0,0 +1,16 @@
+package org.dromara.common.utils.baiduUtil.entity.ocr;
+
+import lombok.Data;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 10:55
+ * @Version 1.0
+ *
+ * 单个识别字段(含文字和位置)
+ */
+@Data
+public class Field {
+ private Location location; // 位置信息
+ private String words; // 识别文字
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/IDCardInfo.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/IDCardInfo.java
new file mode 100644
index 00000000..fbd30a2d
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/IDCardInfo.java
@@ -0,0 +1,30 @@
+package org.dromara.common.utils.baiduUtil.entity.ocr;
+
+import com.alibaba.fastjson2.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 10:50
+ * @Version 1.0
+ *
+ * 身份证识别结果
+ */
+@Data
+public class IDCardInfo {
+ @JSONField(name = "words_result")
+ private WordsResult wordsResult; // 识别字段集合(姓名、身份证号等)
+
+ @JSONField(name = "words_result_num")
+ private int wordsResultNum; // 识别字段数量
+
+ @JSONField(name = "idcard_number_type")
+ private int idCardNumberType; // 身份证类型(如18位/15位)
+
+ private String imageStatus; // 图像状态(如是否颠倒)
+
+ @JSONField(name = "log_id")
+ private long logId; // 请求唯一标识(日志ID)
+
+ private String photo; // 身份证照片base64(若检测)
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Location.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Location.java
new file mode 100644
index 00000000..fc611511
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Location.java
@@ -0,0 +1,18 @@
+package org.dromara.common.utils.baiduUtil.entity.ocr;
+
+import lombok.Data;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 10:56
+ * @Version 1.0
+ *
+ * 位置信息(坐标)
+ */
+@Data
+public class Location {
+ private int top; // 顶部坐标
+ private int left; // 左侧坐标
+ private int width; // 宽度
+ private int height; // 高度
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/OcrReq.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/OcrReq.java
new file mode 100644
index 00000000..a785503c
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/OcrReq.java
@@ -0,0 +1,24 @@
+package org.dromara.common.utils.baiduUtil.entity.ocr;
+
+import com.alibaba.fastjson2.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 10:47
+ * @Version 1.0
+ *
+ * OCR请求参数(身份证/银行卡通用)
+ */
+@Data
+public class OcrReq {
+ private String image; // 图像base64(与url二选一)
+
+ private String url; // 图像URL(与image二选一)
+
+ @JSONField(name = "id_card_side")
+ private String idCardSide; // 身份证正反面(front/back,银行卡无需)
+
+ @JSONField(name = "detect_photo")
+ private boolean detectPhoto;// 是是否开启银行卡质量类型(清晰模糊、边框/四角不完整)检测功能,默认不开启,即:false。
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Result.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Result.java
new file mode 100644
index 00000000..b92420c1
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/Result.java
@@ -0,0 +1,22 @@
+package org.dromara.common.utils.baiduUtil.entity.ocr;
+
+import com.alibaba.fastjson2.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 11:20
+ * @Version 1.0
+ *
+ * 银行卡识别的完整返回结果
+ */
+@Data
+public class Result {
+ @JSONField(name = "result")
+ private BankData res;
+
+ private int direction;
+
+ @JSONField(name = "log_id")
+ private long logId;
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/WordsResult.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/WordsResult.java
new file mode 100644
index 00000000..43de74b4
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/common/utils/baiduUtil/entity/ocr/WordsResult.java
@@ -0,0 +1,33 @@
+package org.dromara.common.utils.baiduUtil.entity.ocr;
+
+import com.alibaba.fastjson2.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * @Author 铁憨憨
+ * @Date 2025/7/18 10:50
+ * @Version 1.0
+ *
+ * 身份证识别字段集合
+ */
+@Data
+public class WordsResult {
+ @JSONField(name = "姓名")
+ private Field name; // 姓名
+ @JSONField(name = "民族")
+ private Field nation; // 民族
+ @JSONField(name = "住址")
+ private Field address; // 住址
+ @JSONField(name = "公民身份号码")
+ private Field citizenIdentification; // 身份证号
+ @JSONField(name = "出生")
+ private Field birth; // 出生日期
+ @JSONField(name = "性别")
+ private Field gender; // 性别
+ @JSONField(name = "失效日期")
+ private Field expirationDate; // 失效日期
+ @JSONField(name = "签发机关")
+ private Field issuingAuthority; // 签发机关
+ @JSONField(name = "签发日期")
+ private Field issueDate; // 签发日期
+}