新增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识别结婚证自动带出信息领取二维码页 ### 2OCR识别结婚证自动带出信息领取二维码页
为避免用户手动输入,新增 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 缓存 Base6410 分钟),减少存储与泄露风险。 - 后端不存储用户上传的原始图片文件,仅在 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> <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>

View File

@ -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,24 +137,19 @@ 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 (end > 0) {
String w = item.substring(0, end);
w = w.replace("\"", "").replace("}", "");
if (StringUtils.isNotBlank(w)) { if (StringUtils.isNotBlank(w)) {
words.add(w); words.add(w);
} }
} }
}
return words; return words;
} catch (Exception e) { } catch (Exception e) {
return Collections.emptyList(); return Collections.emptyList();
@ -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;
} }