新增ocr接口

This commit is contained in:
lianjie111 2025-11-12 18:29:32 +08:00
parent 5fa97cc878
commit 1ce45419e4
3 changed files with 83 additions and 22 deletions

25
api.md
View File

@ -44,7 +44,7 @@
### 2OCR识别结婚证自动带出信息领取二维码页
为避免用户手动输入,新增 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 缓存 Base6410 分钟),减少存储与泄露风险。
### 配置项标注
- 百度 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` 包含示例字号片段
---
## 兑奖页面(改为自动带出结婚证号、手机号、姓名)

View File

@ -95,6 +95,43 @@
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@ -115,4 +152,3 @@
</build>
</project>

View File

@ -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<String, String> 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<String, String> params = new HashMap<>();
params.put("grant_type", "client_credentials");
params.put("client_id", apiKey);
@ -131,22 +137,17 @@ public class OcrController {
private List<String> 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<String> 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;
}