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; }