From 377a592195a597df127a5c27cb1d97d32c46f23f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Thu, 30 Jul 2026 22:27:02 +0800 Subject: [PATCH] =?UTF-8?q?refactor(certificate):=20=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E8=BA=AB=E4=BB=BD=E8=AF=81=E6=AD=A3=E5=8F=8D=E9=9D=A2OCR?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除旧的正面、反面单独识别接口,改用统一的身份证OCR合并识别接口 - 优化身份证OCR调用逻辑,添加图片base64转换失败处理 - 新增身份证识别服务实现,调用阿里云蜜堂有信接口 - 统一身份证照片识别失败错误码和提示,增强日志记录 - 验证身份证信息一致性及有效期,支持多格式日期解析 - 调整controller逻辑,使用新身份证OCR响应对象进行验证和回填 --- .../controller/CertificateController.java | 24 ++- .../love/service/CertificateService.java | 17 ++- .../service/impl/CertificateServiceImpl.java | 143 ++++++++---------- .../oa/service/impl/IdcardServiceImpl.java | 91 +++++++---- .../controller/OpenEquipmentController.java | 21 ++- 5 files changed, 161 insertions(+), 135 deletions(-) diff --git a/src/main/java/com/gxwebsoft/love/controller/CertificateController.java b/src/main/java/com/gxwebsoft/love/controller/CertificateController.java index 4d8cb1d..e055d6b 100644 --- a/src/main/java/com/gxwebsoft/love/controller/CertificateController.java +++ b/src/main/java/com/gxwebsoft/love/controller/CertificateController.java @@ -13,9 +13,7 @@ import com.gxwebsoft.common.system.service.UserService; import com.gxwebsoft.love.entity.Certificate; import com.gxwebsoft.love.param.CertificateParam; import com.gxwebsoft.love.service.CertificateService; -import com.gxwebsoft.love.vo.idcheck.BackRecognitionResult; -import com.gxwebsoft.love.vo.idcheck.FrontRecognitionResult; -import com.gxwebsoft.love.vo.idcheck.Response; +import com.gxwebsoft.oa.vo.IdcardRespVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.security.access.prepost.PreAuthorize; @@ -97,26 +95,22 @@ public class CertificateController extends BaseController { return fail("请上传身份证照片", certificate); } - // 验证正面 - Response front = certificateService.idcardfrontrecongnition(collect.get(0)); - if (front.getError_code() != 0) { - return fail(front.getReason()); + // 身份证OCR识别(正反面合并) + IdcardRespVO resp = certificateService.idcardOcr(collect.get(0), collect.get(1)); + if (resp == null) { + return fail("身份证识别调用失败", certificate); } - - - // 验证反面 - Response back = certificateService.idcardbackrecongnition(collect.get(1)); - if (back.getError_code() != 0) { - return fail(back.getReason()); + if (!"FP00000".equals(resp.getCode())) { + return fail(resp.getMessage() == null ? "身份证识别失败" : resp.getMessage(), certificate); } certificate.setStatus(30); // 信息是否一致 - if (!certificate.getCertificateCode().equals(front.getResult().getIdcardno())) { + if (!certificate.getCertificateCode().equals(resp.getIdCardNo())) { return fail("认证失败", certificate); } - if (!certificate.getRealName().equals(front.getResult().getName())) { + if (!certificate.getRealName().equals(resp.getName())) { return fail("认证失败", certificate); } User user = new User(); diff --git a/src/main/java/com/gxwebsoft/love/service/CertificateService.java b/src/main/java/com/gxwebsoft/love/service/CertificateService.java index a1ef22e..a41469b 100644 --- a/src/main/java/com/gxwebsoft/love/service/CertificateService.java +++ b/src/main/java/com/gxwebsoft/love/service/CertificateService.java @@ -4,9 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.love.entity.Certificate; import com.gxwebsoft.love.param.CertificateParam; -import com.gxwebsoft.love.vo.idcheck.BackRecognitionResult; -import com.gxwebsoft.love.vo.idcheck.FrontRecognitionResult; -import com.gxwebsoft.love.vo.idcheck.Response; +import com.gxwebsoft.oa.vo.IdcardRespVO; import java.util.List; @@ -42,10 +40,15 @@ public interface CertificateService extends IService { */ Certificate getByIdRel(Integer certificateId); + /** + * 身份证OCR识别(正反面合并识别) + * + * @param frontImageUrl 正面图片 URL + * @param backImageUrl 反面图片 URL + * @return 识别结果 + */ + IdcardRespVO idcardOcr(String frontImageUrl, String backImageUrl); + boolean verifyIdcard(Certificate param, List files); - Response idcardfrontrecongnition(String imageUrl); - - Response idcardbackrecongnition(String imageUrl); - } diff --git a/src/main/java/com/gxwebsoft/love/service/impl/CertificateServiceImpl.java b/src/main/java/com/gxwebsoft/love/service/impl/CertificateServiceImpl.java index 2f16e8e..4cf0cf1 100644 --- a/src/main/java/com/gxwebsoft/love/service/impl/CertificateServiceImpl.java +++ b/src/main/java/com/gxwebsoft/love/service/impl/CertificateServiceImpl.java @@ -2,7 +2,6 @@ package com.gxwebsoft.love.service.impl; import cn.hutool.core.exceptions.ExceptionUtil; import com.alibaba.fastjson2.JSONObject; -import com.alibaba.fastjson2.TypeReference; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.gxwebsoft.common.core.utils.HttpUtils; import com.gxwebsoft.common.core.utils.ImageUtil; @@ -12,9 +11,7 @@ import com.gxwebsoft.love.entity.Certificate; import com.gxwebsoft.love.mapper.CertificateMapper; import com.gxwebsoft.love.param.CertificateParam; import com.gxwebsoft.love.service.CertificateService; -import com.gxwebsoft.love.vo.idcheck.BackRecognitionResult; -import com.gxwebsoft.love.vo.idcheck.FrontRecognitionResult; -import com.gxwebsoft.love.vo.idcheck.Response; +import com.gxwebsoft.oa.vo.IdcardRespVO; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.util.EntityUtils; @@ -22,10 +19,12 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; -import java.time.LocalDateTime; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; /** * 证件管理记录表Service实现 @@ -39,12 +38,13 @@ public class CertificateServiceImpl extends ServiceImpl pageRel(CertificateParam param) { PageParam page = new PageParam<>(param); - //page.setDefaultOrder("create_time desc"); List list = baseMapper.selectPageRel(page, param); return new PageResult<>(list, page.getTotal()); } @@ -52,9 +52,7 @@ public class CertificateServiceImpl extends ServiceImpl listRel(CertificateParam param) { List list = baseMapper.selectListRel(param); - // 排序 PageParam page = new PageParam<>(); - //page.setDefaultOrder("create_time desc"); return page.sortRecords(list); } @@ -65,94 +63,81 @@ public class CertificateServiceImpl extends ServiceImpl headers = new HashMap<>(); + headers.put("Authorization", "APPCODE " + APP_CODE); + headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + headers.put("X-Ca-Nonce", UUID.randomUUID().toString()); + + Map querys = new HashMap<>(); + Map bodys = new HashMap<>(); + bodys.put("reqNo", UUID.randomUUID().toString().replace("-", "")); + bodys.put("frontImg", frontImg); + bodys.put("backImg", backImg); + + try { + HttpResponse response = HttpUtils.doPost(HOST, "/v1/tools/ocr/idCard", "POST", headers, querys, bodys); + HttpEntity entity = response.getEntity(); + String string = EntityUtils.toString(entity); + return JSONObject.parseObject(string, IdcardRespVO.class); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + @Override public boolean verifyIdcard(Certificate param, List files) { - // 验证正面 - Response front = idcardfrontrecongnition(files.get(0)); - if(front.getError_code() != 0){ - ExceptionUtil.wrapAndThrow(new Exception(front.getReason())); + if (files == null || files.size() < 2) { + ExceptionUtil.wrapAndThrow(new RuntimeException("缺少身份证正反面照片")); return false; } - - // 验证反面 - Response back = idcardbackrecongnition(files.get(1)); - if(back.getError_code() != 0){ - ExceptionUtil.wrapAndThrow(new Exception(back.getReason())); + IdcardRespVO resp = idcardOcr(files.get(0), files.get(1)); + if (resp == null) { + ExceptionUtil.wrapAndThrow(new RuntimeException("身份证识别调用失败")); + return false; + } + if (!SUCCESS_CODE.equals(resp.getCode())) { + ExceptionUtil.wrapAndThrow(new RuntimeException(resp.getMessage() == null ? "身份证识别失败" : resp.getMessage())); return false; } // 信息是否一致 - if(!param.getCertificateCode().equals(front.getResult().getIdcardno())){ + if (!param.getCertificateCode().equals(resp.getIdCardNo())) { return false; } - if(!param.getRealName().equals(front.getResult().getName())){ + if (!param.getRealName().equals(resp.getName())) { return false; } - boolean b = LocalDateTime.parse(back.getResult().getEndDate()).compareTo(LocalDateTime.now()) > 0; - return b; - } - @Override - public Response idcardfrontrecongnition(String imageUrl) { - - String host = "https://zidv2.market.alicloudapi.com"; - String path = "/thirdnode/ImageAI/idcardfrontrecongnition"; - String method = "POST"; - Map headers = new HashMap(); - //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105 - headers.put("Authorization", "APPCODE " + appcode); - //根据API的要求,定义相对应的Content-Type - headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); - Map querys = new HashMap(); - Map bodys = new HashMap(); - bodys.put("base64Str", ImageUtil.ImageBase64(imageUrl)); - - - try { - HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys); - HttpEntity entity = response.getEntity(); - String string = EntityUtils.toString(entity); - Response o = JSONObject.parseObject(string, new TypeReference>() { - }); - - return o; - } catch (Exception e) { - e.printStackTrace(); + // 有效期是否过期 + String expireDate = resp.getExpireDate(); + if (expireDate == null || expireDate.isEmpty()) { + return false; } - - return null; - } - - @Override - public Response idcardbackrecongnition(String imageUrl) { - - String host = "https://zidv2.market.alicloudapi.com"; - String path = "/thirdnode/ImageAI/idcardbackrecongnition"; - String method = "POST"; - Map headers = new HashMap(); - //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105 - headers.put("Authorization", "APPCODE " + appcode); - //根据API的要求,定义相对应的Content-Type - headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); - Map querys = new HashMap(); - Map bodys = new HashMap(); - bodys.put("base64Str", ImageUtil.ImageBase64(imageUrl)); - - + LocalDate expire; try { - HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys); - HttpEntity entity = response.getEntity(); - String string = EntityUtils.toString(entity); - Response o = JSONObject.parseObject(string, new TypeReference>() { - }); - - return o; + expire = LocalDate.parse(expireDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")); } catch (Exception e) { - e.printStackTrace(); + // 尝试按 yyyyMMdd 解析 + try { + expire = LocalDate.parse(expireDate, DateTimeFormatter.ofPattern("yyyyMMdd")); + } catch (Exception ex) { + return false; + } } - - return null; + return expire.isAfter(LocalDate.now()); } } diff --git a/src/main/java/com/gxwebsoft/oa/service/impl/IdcardServiceImpl.java b/src/main/java/com/gxwebsoft/oa/service/impl/IdcardServiceImpl.java index 725981a..c48ce3c 100644 --- a/src/main/java/com/gxwebsoft/oa/service/impl/IdcardServiceImpl.java +++ b/src/main/java/com/gxwebsoft/oa/service/impl/IdcardServiceImpl.java @@ -1,12 +1,10 @@ package com.gxwebsoft.oa.service.impl; import com.alibaba.fastjson2.JSONObject; -import com.alibaba.fastjson2.TypeReference; import com.gxwebsoft.common.core.utils.HttpUtils; -import com.gxwebsoft.love.vo.idcheck.FrontRecognitionResult; -import com.gxwebsoft.love.vo.idcheck.Response; import com.gxwebsoft.oa.service.IdcardService; import com.gxwebsoft.oa.vo.IdcardRespVO; +import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.util.EntityUtils; @@ -16,42 +14,75 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; +@Slf4j @Service public class IdcardServiceImpl implements IdcardService { + + /** 阿里云市场 - 身份证 OCR 供应商(蜜堂有信) */ + private static final String HOST = "https://miitangs14.market.alicloudapi.com"; + private static final String APP_CODE = "1390018309a443c695c98c8da2bed691"; + private static final String SUCCESS_CODE = "FP00000"; + + /** + * @param frontImg 身份证正面图片的 base64 字符串(已带 ?x-oss-process 压缩,调用方负责生成) + * @param backImg 身份证反面图片的 base64 字符串 + */ @Override public IdcardRespVO verify(String frontImg, String backImg) { + // 1. 校验输入 + if (frontImg == null || frontImg.isEmpty() || backImg == null || backImg.isEmpty()) { + log.warn("身份证 verify 调用缺少正/反面图片, frontIsBlank={}, backIsBlank={}", + frontImg == null || frontImg.isEmpty(), + backImg == null || backImg.isEmpty()); + return null; + } - String host = "https://miitangs14.market.alicloudapi.com"; - String path = "/v1/tools/ocr/idCard"; - String method = "POST"; - String appcode = "1390018309a443c695c98c8da2bed691"; - Map headers = new HashMap(); - //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105 - headers.put("Authorization", "APPCODE " + appcode); - //根据API的要求,定义相对应的Content-Type + // 2. 调用新接口(正反面合并为一次请求) + IdcardRespVO resp; + try { + resp = callOcr(frontImg, backImg); + } catch (Exception e) { + log.error("身份证OCR识别调用异常", e); + return null; + } + if (resp == null) { + log.warn("身份证OCR识别返回为空(接口无响应或解析失败)"); + return null; + } + + // 3. 失败返回具体原因 + if (!SUCCESS_CODE.equals(resp.getCode())) { + log.warn("身份证OCR识别失败 code={}, message={}", resp.getCode(), resp.getMessage()); + IdcardRespVO fail = new IdcardRespVO(); + fail.setCode(resp.getCode()); + fail.setMessage(resp.getMessage()); + return fail; + } + + // 4. 成功 + log.info("身份证识别成功 name={}, idCardNo={}", resp.getName(), resp.getIdCardNo()); + return resp; + } + + /** + * 调用蜜堂有信身份证 OCR 接口:POST /v1/tools/ocr/idCard + */ + private IdcardRespVO callOcr(String frontImg, String backImg) throws Exception { + Map headers = new HashMap<>(); + headers.put("Authorization", "APPCODE " + APP_CODE); headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); - //需要给X-Ca-Nonce的值生成随机字符串,每次请求不能相同 headers.put("X-Ca-Nonce", UUID.randomUUID().toString()); - Map querys = new HashMap(); - Map bodys = new HashMap(); + + Map querys = new HashMap<>(); + Map bodys = new HashMap<>(); + bodys.put("reqNo", UUID.randomUUID().toString().replace("-", "")); bodys.put("frontImg", frontImg); bodys.put("backImg", backImg); - - try { - - HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys); - HttpEntity entity = response.getEntity(); - String string = EntityUtils.toString(entity); - IdcardRespVO respVO = JSONObject.parseObject(string, new TypeReference() { - }); - return respVO; - //获取response的body - //System.out.println(EntityUtils.toString(response.getEntity())); - } catch (Exception e) { - e.printStackTrace(); - } - - return null; + HttpResponse response = HttpUtils.doPost(HOST, "/v1/tools/ocr/idCard", "POST", headers, querys, bodys); + HttpEntity entity = response.getEntity(); + String body = EntityUtils.toString(entity); + log.debug("OCR 调用 frontImgLen={}, backImgLen={}, respLen={}", frontImg.length(), backImg.length(), body.length()); + return JSONObject.parseObject(body, IdcardRespVO.class); } } diff --git a/src/main/java/com/gxwebsoft/open/controller/OpenEquipmentController.java b/src/main/java/com/gxwebsoft/open/controller/OpenEquipmentController.java index d0d2c2b..fd43c2f 100644 --- a/src/main/java/com/gxwebsoft/open/controller/OpenEquipmentController.java +++ b/src/main/java/com/gxwebsoft/open/controller/OpenEquipmentController.java @@ -474,14 +474,21 @@ public class OpenEquipmentController extends BaseController { return fail("身份证识别服务暂时不可用,请稍后重试"); } if (verify == null) { - return fail("身份证识别失败,请稍后重试"); + return fail("身份证识别服务调用失败,请稍后重试"); } // 4. 正面校验:非 FP00000 说明传的不是有效身份证人像面 if (!"FP00000".equals(verify.getCode())) { - // 内部错误码仅记录日志,不回吐给用户 - log.warn("身份证正面识别失败 code={}, message={}", verify.getCode(), verify.getMessage()); - return fail("身份证正面照片识别失败,请重新上传清晰的身份证人像面照片"); + // 优先用阿里云返回的具体原因,没拿到再回退到通用提示 + String reason = verify.getMessage(); + if ("FRONT_FAIL".equals(verify.getCode())) { + return fail(StrUtil.isNotBlank(reason) ? reason : "身份证正面照片识别失败,请重新上传清晰的身份证人像面照片"); + } + if ("BACK_FAIL".equals(verify.getCode())) { + return fail(StrUtil.isNotBlank(reason) ? reason : "身份证反面照片识别失败,请重新上传清晰的身份证国徽面照片"); + } + log.warn("身份证识别未通过 code={}, message={}", verify.getCode(), verify.getMessage()); + return fail(StrUtil.isNotBlank(reason) ? reason : "身份证识别失败,请重新上传清晰的身份证照片"); } // 5. 反面校验:签发机关/日期缺失说明传的不是有效身份证国徽面 @@ -491,6 +498,12 @@ public class OpenEquipmentController extends BaseController { return fail("身份证反面照片识别失败,请重新上传清晰的身份证国徽面照片"); } + // 6. 正面字段也兜底校验一次(避免 OCR 返回空字符串导致回填出空实名) + if (StringUtils.isEmpty(verify.getName()) + || StringUtils.isEmpty(verify.getIdCardNo())) { + return fail("身份证正面照片识别失败,请重新上传清晰的身份证人像面照片"); + } + // 6. 校验通过,回填实名信息 order.setRealName(verify.getName()); order.setIdCode(verify.getIdCardNo());