From 1ce45419e46119c7f62a35bf502a96c7523ca044 Mon Sep 17 00:00:00 2001
From: lianjie111 <1046407070@qq.com>
Date: Wed, 12 Nov 2025 18:29:32 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Eocr=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
api.md | 25 ++++++++---
com-marriage-client/pom.xml | 38 ++++++++++++++++-
.../client/controller/OcrController.java | 42 ++++++++++++-------
3 files changed, 83 insertions(+), 22 deletions(-)
diff --git a/api.md b/api.md
index 8450926..4bdfc40 100644
--- a/api.md
+++ b/api.md
@@ -44,7 +44,7 @@
### 2)OCR识别结婚证,自动带出信息,领取二维码页
为避免用户手动输入,新增 OCR 流程:上传结婚证图片并识别证件信息,前端使用识别结果自动填充表单,再进行短信校验与领取。
-- OCR 上传图片
+- OCR 上传图片 [新增]
- 方法:`POST`
- 路径:`/marriage/ocr/upload`
- 入参:`multipart/form-data`,字段:`file`(结婚证图片)
@@ -59,14 +59,14 @@
- 路径:`/marriage/common/sms`
- 入参:`CommSmsDTO`
- `mobile`:手机号(必填)
- - `type`:验证码类型,`0`=登录;`1`=兑换领取;`2`=OCR识别(本页使用 `2`)
+ - `type`:验证码类型,`0`=登录;`1`=兑换领取;`2`=OCR识别(本页使用 `2`) [扩展]
- 出参(示例):
```json
{ "code": 200, "msg": "", "data": null }
```
- 备注:当前实现对 `type=1` 的校验逻辑使用了 `code` 字段比对,可能与期望不一致(代码中以 `MarriageCode::getCode == mobile` 判断重复)。如需严格校验手机号唯一性,可调整实现。
-- OCR 识别并返回证件信息(供前端自动填充)
+- OCR 识别并返回证件信息(供前端自动填充) [新增]
- 方法:`POST`
- 路径:`/marriage/ocr/parse`
- 入参:`OcrParseDTO`
@@ -161,15 +161,28 @@
## OCR识别流程(免手动输入)
### 端到端流程
-1. 前端上传结婚证图片:`POST /marriage/ocr/upload` → 返回 `uploadId`
-2. 发送短信验证码:`POST /marriage/common/sms`(`type=2`)
-3. 识别证件信息:`POST /marriage/ocr/parse`(`mobile`、`smsCode`、`uploadId`)→ 返回 `parsed.marriageNo` 等信息
+1. 前端上传结婚证图片:`POST /marriage/ocr/upload` [新增] → 返回 `uploadId`
+2. 发送短信验证码:`POST /marriage/common/sms`(`type=2`) [扩展]
+3. 识别证件信息:`POST /marriage/ocr/parse`(`mobile`、`smsCode`、`uploadId`) [新增] → 返回 `parsed.marriageNo` 等信息
4. 自动填充领取表单,走校验与领取:`POST /marriage/receiveCheck` → `POST /marriage/receiveCode`
### 安全与权限
- `POST /marriage/ocr/**` 与 `POST /marriage/common/sms` 已放行匿名访问,用于领取端无登录场景。
- 后端不存储用户上传的原始图片文件,仅在 Redis 缓存 Base64(10 分钟),减少存储与泄露风险。
+### 配置项标注
+- 百度 OCR 相关配置(开发环境) [新增]
+ - `baidu.ocr.appId`、`baidu.ocr.apiKey`、`baidu.ocr.secretKey`(建议通过环境变量注入)
+ - `baidu.ocr.authUrl`(默认:`https://aip.baidubce.com/oauth/2.0/token`)
+ - `baidu.ocr.generalUrl`(默认:`https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic`)
+ - `baidu.ocr.storagePath`(当前不落盘,仅占位)
+
+### 测试说明
+- 集成测试覆盖 OCR 上传与识别接口,使用本地伪服务模拟百度返回 [新增]
+ - 测试类:`com-marriage-client/src/test/java/com/jinrui/marriage/client/controller/OcrControllerTest.java`
+ - 通过属性重定向百度 URL 到本地:`baidu.ocr.authUrl`、`baidu.ocr.generalUrl`
+ - 断言返回的 `parsed.marriageNo` 包含示例字号片段
+
---
## 兑奖页面(改为自动带出结婚证号、手机号、姓名)
diff --git a/com-marriage-client/pom.xml b/com-marriage-client/pom.xml
index 8be2bee..2568e8d 100644
--- a/com-marriage-client/pom.xml
+++ b/com-marriage-client/pom.xml
@@ -95,6 +95,43 @@
jjwt
0.9.1
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+
+
+
+
+ org.mockito
+ mockito-inline
+ 4.11.0
+ test
+
+
+ org.mockito
+ mockito-core
+ 4.11.0
+ test
+
+
+ net.bytebuddy
+ byte-buddy
+ 1.14.11
+ test
+
+
+ net.bytebuddy
+ byte-buddy-agent
+ 1.14.11
+ test
+
@@ -115,4 +152,3 @@
-
diff --git a/com-marriage-client/src/main/java/com/jinrui/marriage/client/controller/OcrController.java b/com-marriage-client/src/main/java/com/jinrui/marriage/client/controller/OcrController.java
index 3a80f78..35aafc9 100644
--- a/com-marriage-client/src/main/java/com/jinrui/marriage/client/controller/OcrController.java
+++ b/com-marriage-client/src/main/java/com/jinrui/marriage/client/controller/OcrController.java
@@ -39,6 +39,12 @@ public class OcrController {
@Value("${baidu.ocr.storagePath:data/ocr}")
private String storagePath;
+ @Value("${baidu.ocr.authUrl:https://aip.baidubce.com/oauth/2.0/token}")
+ private String authUrl;
+
+ @Value("${baidu.ocr.generalUrl:https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic}")
+ private String generalUrl;
+
@PostMapping("/upload")
public ResultObject upload(@RequestParam("file") MultipartFile file) {
if (file == null || file.isEmpty()) {
@@ -91,7 +97,7 @@ public class OcrController {
if (StringUtils.isBlank(accessToken)) {
return ResultUtil.failedMessage("获取OCR令牌失败,请稍后再试!");
}
- String ocrUrl = "https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token=" + accessToken;
+ String ocrUrl = generalUrl + "?access_token=" + accessToken;
Map params = new HashMap<>();
params.put("image", imageBase64);
params.put("language_type", "CHN_ENG");
@@ -113,7 +119,7 @@ public class OcrController {
private String getAccessToken() {
try {
- String url = "https://aip.baidubce.com/oauth/2.0/token";
+ String url = authUrl;
Map params = new HashMap<>();
params.put("grant_type", "client_credentials");
params.put("client_id", apiKey);
@@ -131,22 +137,17 @@ public class OcrController {
private List extractWords(String ocrResp) {
try {
- // 粗略解析 words_result 列表
- String wordsResultJson = JacksonUtil.getValue(ocrResp, "words_result");
- if (StringUtils.isBlank(wordsResultJson)) {
+ com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
+ com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(ocrResp);
+ com.fasterxml.jackson.databind.JsonNode arr = root.get("words_result");
+ if (arr == null || !arr.isArray()) {
return Collections.emptyList();
}
- // 由于 JacksonUtil.getValue 返回的是子节点字符串,这里简单处理,按关键字段提取
List words = new ArrayList<>();
- String[] items = wordsResultJson.split("\\{\\\"words\\\":");
- for (String item : items) {
- int end = item.indexOf("}\"");
- if (end > 0) {
- String w = item.substring(0, end);
- w = w.replace("\"", "").replace("}", "");
- if (StringUtils.isNotBlank(w)) {
- words.add(w);
- }
+ for (com.fasterxml.jackson.databind.JsonNode n : arr) {
+ String w = n.path("words").asText("");
+ if (StringUtils.isNotBlank(w)) {
+ words.add(w);
}
}
return words;
@@ -173,6 +174,17 @@ public class OcrController {
if (StringUtils.contains(w, "登记日期") || StringUtils.contains(w, "日期")) {
map.put("registerDate", w.replaceAll("[^0-9-年/月日]", ""));
}
+ java.util.regex.Matcher mNo = java.util.regex.Pattern.compile("[A-Z][0-9]{6}-[0-9]{4}-[0-9]{6}").matcher(w);
+ if (mNo.find()) {
+ map.put("marriageNo", mNo.group());
+ }
+ java.util.regex.Matcher mDate = java.util.regex.Pattern.compile("(\\d{4})([年/-])(\\d{2})([月/-]?)(\\d{2})").matcher(w);
+ if (mDate.find()) {
+ String yyyy = mDate.group(1);
+ String mm = mDate.group(3);
+ String dd = mDate.group(5);
+ map.put("registerDate", yyyy + "-" + mm + "-" + dd);
+ }
}
return map;
}