新增ocr接口
This commit is contained in:
parent
5fa97cc878
commit
1ce45419e4
25
api.md
25
api.md
|
|
@ -44,7 +44,7 @@
|
||||||
### 2)OCR识别结婚证,自动带出信息,领取二维码页
|
### 2)OCR识别结婚证,自动带出信息,领取二维码页
|
||||||
为避免用户手动输入,新增 OCR 流程:上传结婚证图片并识别证件信息,前端使用识别结果自动填充表单,再进行短信校验与领取。
|
为避免用户手动输入,新增 OCR 流程:上传结婚证图片并识别证件信息,前端使用识别结果自动填充表单,再进行短信校验与领取。
|
||||||
|
|
||||||
- OCR 上传图片
|
- OCR 上传图片 [新增]
|
||||||
- 方法:`POST`
|
- 方法:`POST`
|
||||||
- 路径:`/marriage/ocr/upload`
|
- 路径:`/marriage/ocr/upload`
|
||||||
- 入参:`multipart/form-data`,字段:`file`(结婚证图片)
|
- 入参:`multipart/form-data`,字段:`file`(结婚证图片)
|
||||||
|
|
@ -59,14 +59,14 @@
|
||||||
- 路径:`/marriage/common/sms`
|
- 路径:`/marriage/common/sms`
|
||||||
- 入参:`CommSmsDTO`
|
- 入参:`CommSmsDTO`
|
||||||
- `mobile`:手机号(必填)
|
- `mobile`:手机号(必填)
|
||||||
- `type`:验证码类型,`0`=登录;`1`=兑换领取;`2`=OCR识别(本页使用 `2`)
|
- `type`:验证码类型,`0`=登录;`1`=兑换领取;`2`=OCR识别(本页使用 `2`) [扩展]
|
||||||
- 出参(示例):
|
- 出参(示例):
|
||||||
```json
|
```json
|
||||||
{ "code": 200, "msg": "", "data": null }
|
{ "code": 200, "msg": "", "data": null }
|
||||||
```
|
```
|
||||||
- 备注:当前实现对 `type=1` 的校验逻辑使用了 `code` 字段比对,可能与期望不一致(代码中以 `MarriageCode::getCode == mobile` 判断重复)。如需严格校验手机号唯一性,可调整实现。
|
- 备注:当前实现对 `type=1` 的校验逻辑使用了 `code` 字段比对,可能与期望不一致(代码中以 `MarriageCode::getCode == mobile` 判断重复)。如需严格校验手机号唯一性,可调整实现。
|
||||||
|
|
||||||
- OCR 识别并返回证件信息(供前端自动填充)
|
- OCR 识别并返回证件信息(供前端自动填充) [新增]
|
||||||
- 方法:`POST`
|
- 方法:`POST`
|
||||||
- 路径:`/marriage/ocr/parse`
|
- 路径:`/marriage/ocr/parse`
|
||||||
- 入参:`OcrParseDTO`
|
- 入参:`OcrParseDTO`
|
||||||
|
|
@ -161,15 +161,28 @@
|
||||||
## OCR识别流程(免手动输入)
|
## OCR识别流程(免手动输入)
|
||||||
|
|
||||||
### 端到端流程
|
### 端到端流程
|
||||||
1. 前端上传结婚证图片:`POST /marriage/ocr/upload` → 返回 `uploadId`
|
1. 前端上传结婚证图片:`POST /marriage/ocr/upload` [新增] → 返回 `uploadId`
|
||||||
2. 发送短信验证码:`POST /marriage/common/sms`(`type=2`)
|
2. 发送短信验证码:`POST /marriage/common/sms`(`type=2`) [扩展]
|
||||||
3. 识别证件信息:`POST /marriage/ocr/parse`(`mobile`、`smsCode`、`uploadId`)→ 返回 `parsed.marriageNo` 等信息
|
3. 识别证件信息:`POST /marriage/ocr/parse`(`mobile`、`smsCode`、`uploadId`) [新增] → 返回 `parsed.marriageNo` 等信息
|
||||||
4. 自动填充领取表单,走校验与领取:`POST /marriage/receiveCheck` → `POST /marriage/receiveCode`
|
4. 自动填充领取表单,走校验与领取:`POST /marriage/receiveCheck` → `POST /marriage/receiveCode`
|
||||||
|
|
||||||
### 安全与权限
|
### 安全与权限
|
||||||
- `POST /marriage/ocr/**` 与 `POST /marriage/common/sms` 已放行匿名访问,用于领取端无登录场景。
|
- `POST /marriage/ocr/**` 与 `POST /marriage/common/sms` 已放行匿名访问,用于领取端无登录场景。
|
||||||
- 后端不存储用户上传的原始图片文件,仅在 Redis 缓存 Base64(10 分钟),减少存储与泄露风险。
|
- 后端不存储用户上传的原始图片文件,仅在 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` 包含示例字号片段
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 兑奖页面(改为自动带出结婚证号、手机号、姓名)
|
## 兑奖页面(改为自动带出结婚证号、手机号、姓名)
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,43 @@
|
||||||
<artifactId>jjwt</artifactId>
|
<artifactId>jjwt</artifactId>
|
||||||
<version>0.9.1</version>
|
<version>0.9.1</version>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
@ -115,4 +152,3 @@
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ public class OcrController {
|
||||||
@Value("${baidu.ocr.storagePath:data/ocr}")
|
@Value("${baidu.ocr.storagePath:data/ocr}")
|
||||||
private String storagePath;
|
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")
|
@PostMapping("/upload")
|
||||||
public ResultObject upload(@RequestParam("file") MultipartFile file) {
|
public ResultObject upload(@RequestParam("file") MultipartFile file) {
|
||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
|
|
@ -91,7 +97,7 @@ public class OcrController {
|
||||||
if (StringUtils.isBlank(accessToken)) {
|
if (StringUtils.isBlank(accessToken)) {
|
||||||
return ResultUtil.failedMessage("获取OCR令牌失败,请稍后再试!");
|
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<>();
|
Map<String, String> params = new HashMap<>();
|
||||||
params.put("image", imageBase64);
|
params.put("image", imageBase64);
|
||||||
params.put("language_type", "CHN_ENG");
|
params.put("language_type", "CHN_ENG");
|
||||||
|
|
@ -113,7 +119,7 @@ public class OcrController {
|
||||||
|
|
||||||
private String getAccessToken() {
|
private String getAccessToken() {
|
||||||
try {
|
try {
|
||||||
String url = "https://aip.baidubce.com/oauth/2.0/token";
|
String url = authUrl;
|
||||||
Map<String, String> params = new HashMap<>();
|
Map<String, String> params = new HashMap<>();
|
||||||
params.put("grant_type", "client_credentials");
|
params.put("grant_type", "client_credentials");
|
||||||
params.put("client_id", apiKey);
|
params.put("client_id", apiKey);
|
||||||
|
|
@ -131,22 +137,17 @@ public class OcrController {
|
||||||
|
|
||||||
private List<String> extractWords(String ocrResp) {
|
private List<String> extractWords(String ocrResp) {
|
||||||
try {
|
try {
|
||||||
// 粗略解析 words_result 列表
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
String wordsResultJson = JacksonUtil.getValue(ocrResp, "words_result");
|
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(ocrResp);
|
||||||
if (StringUtils.isBlank(wordsResultJson)) {
|
com.fasterxml.jackson.databind.JsonNode arr = root.get("words_result");
|
||||||
|
if (arr == null || !arr.isArray()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
// 由于 JacksonUtil.getValue 返回的是子节点字符串,这里简单处理,按关键字段提取
|
|
||||||
List<String> words = new ArrayList<>();
|
List<String> words = new ArrayList<>();
|
||||||
String[] items = wordsResultJson.split("\\{\\\"words\\\":");
|
for (com.fasterxml.jackson.databind.JsonNode n : arr) {
|
||||||
for (String item : items) {
|
String w = n.path("words").asText("");
|
||||||
int end = item.indexOf("}\"");
|
if (StringUtils.isNotBlank(w)) {
|
||||||
if (end > 0) {
|
words.add(w);
|
||||||
String w = item.substring(0, end);
|
|
||||||
w = w.replace("\"", "").replace("}", "");
|
|
||||||
if (StringUtils.isNotBlank(w)) {
|
|
||||||
words.add(w);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return words;
|
return words;
|
||||||
|
|
@ -173,6 +174,17 @@ public class OcrController {
|
||||||
if (StringUtils.contains(w, "登记日期") || StringUtils.contains(w, "日期")) {
|
if (StringUtils.contains(w, "登记日期") || StringUtils.contains(w, "日期")) {
|
||||||
map.put("registerDate", w.replaceAll("[^0-9-年/月日]", ""));
|
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;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue