前言

最近在项目里落了一个身份证 OCR 识别接口,目标很明确:

  • 前端只负责上传身份证图片
  • 后端统一调用华为云 OCR RecognizeIdCard
  • 对外返回本项目自己的统一结构
  • 尽量把华为云 SDK 的复杂度封装在服务内部

这篇文章就是我这次接入的笔记总结。

一、官方资料

本次实现参考的是华为云官方身份证识别 API 文档:

二、实现目标

我最终落地的接口如下:

  • 请求地址:POST */ocr/id-card
  • 请求类型:multipart/form-data
  • 请求参数:
    • file:身份证图片文件
    • side:可选,front、back、double_side
  • 返回结构:R<IdCardOcrResultVO>

设计思路是:

  1. 前端传图片文件,不直接对接华为云
  2. 后端把图片转成 Base64,按官方 SDK 要求组装 IdCardRequestBody
  3. 后端统一处理参数校验、异常转换、结果映射
  4. 前端永远只面对项目自己的返回结构

三、为什么选 Java SDK,而不是自己拼 HTTP

华为云身份证识别本质上可以通过 HTTP API 调用,但这里我直接用了官方 Java SDK,原因有三个:

  • SDK 已经封装好了鉴权、区域和请求构造,少踩坑
  • SDK 的请求模型和响应模型是强类型的,开发和维护成本更低
  • 后续如果继续扩展营业执照、银行卡、通用文字识别,也能沿用同一套接入方式

这次实际用到的核心类有:

  • OcrClient
  • BasicCredentials
  • OcrRegion
  • RecognizeIdCardRequest
  • IdCardRequestBody
  • RecognizeIdCardResponse

四、Maven 依赖

我在项目里接入的是华为云 OCR SDK 3.1.125 版本。

根 pom.xml 中统一版本:

<properties>
    <huaweicloud.sdk.version>3.1.125</huaweicloud.sdk.version>
</properties>

业务模块 home-society-exam-main/pom.xml 中引入依赖:

<dependency>
    <groupId>com.huaweicloud.sdk</groupId>
    <artifactId>huaweicloud-sdk-core</artifactId>
    <version>${huaweicloud.sdk.version}</version>
</dependency>

<dependency>
    <groupId>com.huaweicloud.sdk</groupId>
    <artifactId>huaweicloud-sdk-ocr</artifactId>
    <version>${huaweicloud.sdk.version}</version>
</dependency>

五、配置方式

为了避免把 AK/SK/region 写死在业务代码里,我把华为云配置抽到了配置文件。

配置示例:

huawei:
  ocr:
    ak: your-ak
    sk: your-sk
    region: cn-north-4

说明:

  • ak:华为云访问密钥 AK
  • sk:华为云访问密钥 SK
  • region:OCR 区域,例如 cn-north-4

这里一定不要把真实密钥提交到仓库。

六、整体调用流程

整个身份证 OCR 接口的调用链很简单:

  1. Controller 接收 MultipartFile
  2. Service 校验文件是否为空、大小是否超过 7MB、图片格式是否合法、side 是否合法
  3. 把图片字节转成 Base64
  4. 用 HuaweiOcrClientUtil 获取华为云 OcrClient
  5. 组装 RecognizeIdCardRequest
  6. 调用 client.recognizeIdCard(request)
  7. 把华为云返回的正反面结果转换成项目自己的 VO
  8. 统一返回 R.ok(...) 或 R.failed(...)

七、几个关键实现点

1. 为什么要把图片转成 Base64

因为华为云官方身份证识别支持两种常见输入方式:

  • image:图片 Base64
  • url:图片公网地址

我这里选择的是 image 方式,因为项目的对外接口是文件上传接口,前端直接传文件最简单。后端收到 MultipartFile 之后转为 Base64,再交给华为云 SDK。

对应代码如下:

String base64Image = Base64.getEncoder().encodeToString(file.getBytes());

然后放进请求体:

IdCardRequestBody body = new IdCardRequestBody()
        .withImage(base64Image)
        .withReturnVerification(Boolean.TRUE)
        .withDetectCopy(Boolean.TRUE)
        .withDetectReproduce(Boolean.TRUE);

2. 为什么本地限制 7MB

华为云官方文档对图片大小有要求,而 Base64 会比原始图片更大一些,所以我在本地先拦截原图大小,避免无意义请求华为云。

private static final long MAX_FILE_SIZE = 7 * 1024 * 1024L;

3. 为什么 side 为空时不传

这是为了让华为云自动判断身份证正反面,提升接口容错性。

if (StrUtil.isNotBlank(side)) {
    body.withSide(side);
}

如果调用方明确知道自己上传的是正面或反面,建议传 side,能减少识别歧义。

4. 为什么要做结果兼容

华为云在不同场景下返回结构可能存在差异,有时会放在 front/back,有时会直接放在顶层字段。所以我在代码里做了两层兼容:

  • 优先解析 result.getFront() 和 result.getBack()
  • 如果没有,再尝试解析顶层字段

这样能减少因为返回结构轻微差异导致的空结果问题。

5. 为什么统一封装异常

华为云 SDK 会抛出多种异常:

  • ConnectionException
  • RequestTimeoutException
  • ServiceResponseException

如果直接把异常抛给前端,前端处理会很乱。所以我在服务层统一转换成项目自己的失败消息,这样接口行为更稳定。

八、核心代码

下面附上这次接入的核心代码,都是我当前项目里实际使用的版本。

1. 华为云配置类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * 华为云OCR配置
 *
 * @author 久睡成瘾
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "huawei.ocr")
public class HuaweiOcrProperties {

    /**
     * 华为云访问密钥AK
     */
    private String ak;

    /**
     * 华为云访问密钥SK
     */
    private String sk;

    /**
     * OCR服务区域,例如 cn-north-4
     */
    private String region;
}

2. 华为云客户端工具类

import cn.hutool.core.util.StrUtil;

import com.huaweicloud.sdk.core.auth.BasicCredentials;
import com.huaweicloud.sdk.core.auth.ICredential;
import com.huaweicloud.sdk.ocr.v1.OcrClient;
import com.huaweicloud.sdk.ocr.v1.region.OcrRegion;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * 华为云OCR客户端工具类
 *
 * @author 久睡成瘾
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class HuaweiOcrClientUtil {

    private final HuaweiOcrProperties huaweiOcrProperties;

    /**
     * 懒加载缓存OCR客户端,避免重复创建连接配置
     */
    private volatile OcrClient ocrClient;

    /**
     * 获取OCR客户端
     *
     * @return OCR客户端
     */
    public OcrClient getClient() {
        if (ocrClient == null) {
            synchronized (this) {
                if (ocrClient == null) {
                    ocrClient = this.buildClient();
                }
            }
        }
        return ocrClient;
    }

    /**
     * 创建OCR客户端
     *
     * @return OCR客户端
     */
    private OcrClient buildClient() {
        this.checkProperties();
        try {
            ICredential auth = new BasicCredentials()
                    .withAk(huaweiOcrProperties.getAk())
                    .withSk(huaweiOcrProperties.getSk());
            return OcrClient.newBuilder()
                    .withCredential(auth)
                    .withRegion(OcrRegion.valueOf(huaweiOcrProperties.getRegion()))
                    .build();
        } catch (IllegalArgumentException e) {
            log.error("初始化华为云OCR客户端失败,region配置不合法:{}", huaweiOcrProperties.getRegion(), e);
            throw new IllegalStateException("华为云OCR区域配置不正确,请检查huawei.ocr.region", e);
        } catch (Exception e) {
            log.error("初始化华为云OCR客户端失败", e);
            throw new IllegalStateException("初始化华为云OCR客户端失败", e);
        }
    }

    /**
     * 校验配置是否完整
     */
    private void checkProperties() {
        if (StrUtil.hasBlank(huaweiOcrProperties.getAk(), huaweiOcrProperties.getSk(), huaweiOcrProperties.getRegion())) {
            throw new IllegalStateException("华为云OCR配置不完整,请检查huawei.ocr.ak、huawei.ocr.sk、huawei.ocr.region");
        }
    }
}

3. Service 接口

import org.springframework.web.multipart.MultipartFile;

/**
 * 身份证OCR服务
 *
 * @author 久睡成瘾
 */
public interface IdCardOcrService {

    /**
     * 识别身份证图片
     *
     * @param file 身份证图片文件
     * @param side 证件面,支持front、back、double_side
     * @return 识别结果
     */
    R<IdCardOcrResultVO> recognizeIdCard(MultipartFile file, String side);
}

4. Service 实现

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;

import com.huaweicloud.sdk.core.exception.ConnectionException;
import com.huaweicloud.sdk.core.exception.RequestTimeoutException;
import com.huaweicloud.sdk.core.exception.ServiceResponseException;
import com.huaweicloud.sdk.ocr.v1.model.IdCardRequestBody;
import com.huaweicloud.sdk.ocr.v1.model.IdCardResult;
import com.huaweicloud.sdk.ocr.v1.model.IdcardBackResult;
import com.huaweicloud.sdk.ocr.v1.model.IdcardBackVerificationResult;
import com.huaweicloud.sdk.ocr.v1.model.IdcardFrontResult;
import com.huaweicloud.sdk.ocr.v1.model.IdcardFrontVerificationResult;
import com.huaweicloud.sdk.ocr.v1.model.IdcardVerificationResult;
import com.huaweicloud.sdk.ocr.v1.model.RecognizeIdCardRequest;
import com.huaweicloud.sdk.ocr.v1.model.RecognizeIdCardResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.Set;

/**
 * 身份证OCR服务实现
 *
 * @author 久睡成瘾
 */
@Slf4j
@Service
@AllArgsConstructor
public class IdCardOcrServiceImpl implements IdCardOcrService {

    /**
     * 官方建议原图不超过7MB,这里在本地先做一层拦截
     */
    private static final long MAX_FILE_SIZE = 7 * 1024 * 1024L;

    /**
     * 允许上传的图片后缀
     */
    private static final Set<String> ALLOWED_EXTENSIONS = new HashSet<>(Arrays.asList("jpg", "jpeg", "png", "bmp", "tif", "tiff"));

    /**
     * 允许上传的图片Content-Type
     */
    private static final Set<String> ALLOWED_CONTENT_TYPES = new HashSet<>(Arrays.asList(
            "image/jpg", "image/jpeg", "image/png", "image/bmp", "image/tif", "image/tiff", "image/x-tiff"
    ));

    /**
     * 允许的身份证面参数
     */
    private static final Set<String> ALLOWED_SIDES = new HashSet<>(Arrays.asList("front", "back", "double_side"));

    private final HuaweiOcrClientUtil huaweiOcrClientUtil;

    /**
     * 识别身份证图片
     *
     * @param file 身份证图片
     * @param side 证件面
     * @return 识别结果
     */
    @Override
    public R<IdCardOcrResultVO> recognizeIdCard(MultipartFile file, String side) {
        if (file == null || file.isEmpty()) {
            return R.failed("请上传身份证图片");
        }
        if (file.getSize() > MAX_FILE_SIZE) {
            return R.failed("身份证图片大小不能超过7MB");
        }

        String normalizedSide = this.normalizeSide(side);
        if (StrUtil.isNotBlank(normalizedSide) && !ALLOWED_SIDES.contains(normalizedSide)) {
            return R.failed("side参数仅支持front、back、double_side");
        }
        if (!this.isSupportedImage(file)) {
            return R.failed("仅支持jpg、jpeg、png、bmp、tiff格式的身份证图片");
        }

        try {
            String base64Image = Base64.getEncoder().encodeToString(file.getBytes());
            RecognizeIdCardRequest request = this.buildRequest(base64Image, normalizedSide);
            RecognizeIdCardResponse response = huaweiOcrClientUtil.getClient().recognizeIdCard(request);
            IdCardOcrResultVO resultVO = this.buildResult(response == null ? null : response.getResult(), normalizedSide);
            if (resultVO == null || (resultVO.getFront() == null && resultVO.getBack() == null)) {
                return R.failed("身份证识别失败,未获取到有效识别结果");
            }
            return R.ok(resultVO, "身份证识别成功");
        } catch (IllegalStateException e) {
            log.error("身份证OCR配置异常:{}", e.getMessage());
            return R.failed(e.getMessage());
        } catch (ConnectionException e) {
            log.error("调用华为云身份证OCR连接异常", e);
            return R.failed("调用华为云身份证OCR失败,网络连接异常");
        } catch (RequestTimeoutException e) {
            log.error("调用华为云身份证OCR超时", e);
            return R.failed("调用华为云身份证OCR失败,请求超时");
        } catch (ServiceResponseException e) {
            log.error("调用华为云身份证OCR服务异常,status:{}, requestId:{}, errorCode:{}, errorMsg:{}",
                    e.getHttpStatusCode(), e.getRequestId(), e.getErrorCode(), e.getErrorMsg(), e);
            return R.failed("调用华为云身份证OCR失败:" + StrUtil.blankToDefault(e.getErrorMsg(), "服务异常"));
        } catch (IOException e) {
            log.error("读取身份证图片失败", e);
            return R.failed("读取身份证图片失败");
        } catch (Exception e) {
            log.error("身份证OCR识别异常", e);
            return R.failed("身份证识别失败,请稍后重试");
        }
    }

    /**
     * 组装华为云身份证识别请求体
     *
     * @param base64Image 图片Base64
     * @param side        证件面
     * @return 请求对象
     */
    private RecognizeIdCardRequest buildRequest(String base64Image, String side) {
        IdCardRequestBody body = new IdCardRequestBody()
                .withImage(base64Image)
                .withReturnVerification(Boolean.TRUE)
                .withDetectCopy(Boolean.TRUE)
                .withDetectReproduce(Boolean.TRUE);
        // side为空时不传,让华为云自动判断识别面
        if (StrUtil.isNotBlank(side)) {
            body.withSide(side);
        }
        return new RecognizeIdCardRequest().withBody(body);
    }

    /**
     * 构建统一返回结果
     *
     * @param result 华为云识别结果
     * @param side   请求入参中的证件面
     * @return 统一结果
     */
    private IdCardOcrResultVO buildResult(IdCardResult result, String side) {
        if (result == null) {
            return null;
        }

        IdCardOcrResultVO resultVO = new IdCardOcrResultVO();
        if (result.getFront() != null) {
            resultVO.setFront(this.convertFront(result.getFront()));
        }
        if (result.getBack() != null) {
            resultVO.setBack(this.convertBack(result.getBack()));
        }
        if (resultVO.getFront() != null || resultVO.getBack() != null) {
            return resultVO;
        }

        // 兼容华为云单面识别直接返回顶层字段的情况
        if (this.shouldBuildFront(result, side)) {
            resultVO.setFront(this.convertFront(result));
        }
        if (this.shouldBuildBack(result, side)) {
            resultVO.setBack(this.convertBack(result));
        }
        return resultVO;
    }

    /**
     * 判断是否应按身份证正面解析
     *
     * @param result 华为云结果
     * @param side   证件面参数
     * @return 是否按正面处理
     */
    private boolean shouldBuildFront(IdCardResult result, String side) {
        if ("front".equals(side)) {
            return true;
        }
        if ("back".equals(side)) {
            return false;
        }
        return this.hasFrontData(result);
    }

    /**
     * 判断是否应按身份证反面解析
     *
     * @param result 华为云结果
     * @param side   证件面参数
     * @return 是否按反面处理
     */
    private boolean shouldBuildBack(IdCardResult result, String side) {
        if ("back".equals(side)) {
            return true;
        }
        if ("front".equals(side)) {
            return false;
        }
        return this.hasBackData(result);
    }

    /**
     * 判断顶层结果中是否包含正面核心字段
     *
     * @param result 华为云结果
     * @return 是否包含正面字段
     */
    private boolean hasFrontData(IdCardResult result) {
        return StrUtil.isNotBlank(result.getName())
                || StrUtil.isNotBlank(result.getNumber())
                || StrUtil.isNotBlank(result.getAddress())
                || StrUtil.isNotBlank(result.getBirth())
                || StrUtil.isNotBlank(result.getEthnicity())
                || StrUtil.isNotBlank(result.getSex());
    }

    /**
     * 判断顶层结果中是否包含反面核心字段
     *
     * @param result 华为云结果
     * @return 是否包含反面字段
     */
    private boolean hasBackData(IdCardResult result) {
        return StrUtil.isNotBlank(result.getIssue())
                || StrUtil.isNotBlank(result.getValidFrom())
                || StrUtil.isNotBlank(result.getValidTo());
    }

    /**
     * 转换身份证正面结果
     *
     * @param result 华为云正面结果
     * @return 平台正面结果
     */
    private IdCardFrontVO convertFront(IdcardFrontResult result) {
        if (result == null) {
            return null;
        }
        IdCardFrontVO frontVO = new IdCardFrontVO();
        frontVO.setName(result.getName());
        frontVO.setSex(result.getSex());
        frontVO.setEthnicity(result.getEthnicity());
        frontVO.setBirth(result.getBirth());
        frontVO.setAddress(result.getAddress());
        frontVO.setNumber(result.getNumber());
        frontVO.setDetectCopyResult(result.getDetectCopyResult());
        frontVO.setDetectReproduceResult(result.getDetectReproduceResult());
        frontVO.setVerificationResult(this.convertVerification(result.getVerificationResult()));
        return frontVO;
    }

    /**
     * 转换身份证正面结果
     *
     * @param result 华为云顶层结果
     * @return 平台正面结果
     */
    private IdCardFrontVO convertFront(IdCardResult result) {
        if (result == null) {
            return null;
        }
        IdCardFrontVO frontVO = new IdCardFrontVO();
        frontVO.setName(result.getName());
        frontVO.setSex(result.getSex());
        frontVO.setEthnicity(result.getEthnicity());
        frontVO.setBirth(result.getBirth());
        frontVO.setAddress(result.getAddress());
        frontVO.setNumber(result.getNumber());
        frontVO.setDetectCopyResult(result.getDetectCopyResult());
        frontVO.setDetectReproduceResult(result.getDetectReproduceResult());
        frontVO.setVerificationResult(this.convertVerification(result.getVerificationResult()));
        return frontVO;
    }

    /**
     * 转换身份证反面结果
     *
     * @param result 华为云反面结果
     * @return 平台反面结果
     */
    private IdCardBackVO convertBack(IdcardBackResult result) {
        if (result == null) {
            return null;
        }
        IdCardBackVO backVO = new IdCardBackVO();
        backVO.setIssue(result.getIssue());
        backVO.setValidFrom(result.getValidFrom());
        backVO.setValidTo(result.getValidTo());
        backVO.setDetectCopyResult(result.getDetectCopyResult());
        backVO.setDetectReproduceResult(result.getDetectReproduceResult());
        backVO.setVerificationResult(this.convertVerification(result.getVerificationResult()));
        return backVO;
    }

    /**
     * 转换身份证反面结果
     *
     * @param result 华为云顶层结果
     * @return 平台反面结果
     */
    private IdCardBackVO convertBack(IdCardResult result) {
        if (result == null) {
            return null;
        }
        IdCardBackVO backVO = new IdCardBackVO();
        backVO.setIssue(result.getIssue());
        backVO.setValidFrom(result.getValidFrom());
        backVO.setValidTo(result.getValidTo());
        backVO.setDetectCopyResult(result.getDetectCopyResult());
        backVO.setDetectReproduceResult(result.getDetectReproduceResult());
        backVO.setVerificationResult(this.convertVerification(result.getVerificationResult()));
        return backVO;
    }

    /**
     * 转换通用校验结果
     *
     * @param result 华为云校验结果
     * @return 平台校验结果
     */
    private IdCardVerificationVO convertVerification(IdcardVerificationResult result) {
        if (result == null) {
            return null;
        }
        IdCardVerificationVO verificationVO = new IdCardVerificationVO();
        verificationVO.setValidNumber(result.getValidNumber());
        verificationVO.setValidBirth(result.getValidBirth());
        verificationVO.setValidSex(result.getValidSex());
        verificationVO.setValidDate(result.getValidDate());
        verificationVO.setValidValidityPeriod(result.getValidValidityPeriod());
        return verificationVO;
    }

    /**
     * 转换正面校验结果
     *
     * @param result 华为云校验结果
     * @return 平台校验结果
     */
    private IdCardVerificationVO convertVerification(IdcardFrontVerificationResult result) {
        if (result == null) {
            return null;
        }
        IdCardVerificationVO verificationVO = new IdCardVerificationVO();
        verificationVO.setValidNumber(result.getValidNumber());
        verificationVO.setValidBirth(result.getValidBirth());
        verificationVO.setValidSex(result.getValidSex());
        return verificationVO;
    }

    /**
     * 转换反面校验结果
     *
     * @param result 华为云校验结果
     * @return 平台校验结果
     */
    private IdCardVerificationVO convertVerification(IdcardBackVerificationResult result) {
        if (result == null) {
            return null;
        }
        IdCardVerificationVO verificationVO = new IdCardVerificationVO();
        verificationVO.setValidDate(result.getValidDate());
        verificationVO.setValidValidityPeriod(result.getValidValidityPeriod());
        return verificationVO;
    }

    /**
     * 归一化side参数
     *
     * @param side 原始side参数
     * @return 去空格后的side
     */
    private String normalizeSide(String side) {
        if (StrUtil.isBlank(side)) {
            return null;
        }
        return StrUtil.trim(side).toLowerCase();
    }

    /**
     * 校验文件是否为支持的图片格式
     *
     * @param file 上传文件
     * @return 是否支持
     */
    private boolean isSupportedImage(MultipartFile file) {
        String originalFilename = file.getOriginalFilename();
        String extension = originalFilename == null ? null : FileUtil.extName(originalFilename);
        if (StrUtil.isNotBlank(extension) && ALLOWED_EXTENSIONS.contains(extension.toLowerCase())) {
            return true;
        }
        String contentType = StrUtil.blankToDefault(file.getContentType(), "").toLowerCase();
        return ALLOWED_CONTENT_TYPES.contains(contentType);
    }
}

5. Controller

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
 * 身份证OCR Controller
 *
 * @author 久睡成瘾
 */
@Api(value = "idCardOcr", tags = "身份证OCR识别")
@RestController
@Slf4j
@AllArgsConstructor
@RequestMapping("${controller.prefix}/${controller.exam.prefix}/ocr")
public class IdCardOcrController {

    private final IdCardOcrService idCardOcrService;

    /**
     * 身份证图片识别接口
     *
     * @param file 身份证图片
     * @param side 证件面
     * @return 识别结果
     */
    @ApiOperation("身份证图片识别")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "file", value = "身份证图片文件", required = true, dataType = "__file", paramType = "form"),
            @ApiImplicitParam(name = "side", value = "证件面,可选front、back、double_side", dataType = "string", paramType = "form")
    })
    @PostMapping("/id-card")
    public R<IdCardOcrResultVO> recognizeIdCard(@RequestParam(value = "file", required = false) MultipartFile file,
                                                @RequestParam(value = "side", required = false) String side) {
        return idCardOcrService.recognizeIdCard(file, side);
    }
}

6. 返回对象

IdCardOcrResultVO

@Data
public class IdCardOcrResultVO {
    private IdCardFrontVO front;
    private IdCardBackVO back;
}

IdCardFrontVO

@Data
public class IdCardFrontVO {
    private String name;
    private String sex;
    private String ethnicity;
    private String birth;
    private String address;
    private String number;
    private Boolean detectReproduceResult;
    private Boolean detectCopyResult;
    private IdCardVerificationVO verificationResult;
}

IdCardBackVO

@Data
public class IdCardBackVO {
    private String issue;
    private String validFrom;
    private String validTo;
    private Boolean detectReproduceResult;
    private Boolean detectCopyResult;
    private IdCardVerificationVO verificationResult;
}

IdCardVerificationVO

@Data
public class IdCardVerificationVO {
    private Boolean validNumber;
    private Boolean validBirth;
    private Boolean validSex;
    private Boolean validDate;
    private Boolean validValidityPeriod;
}

九、返回结果示例

正面成功返回示例:

{
    "code": 0,
    "msg": "身份证识别成功",
    "data": {
        "front": {
            "name": "test",
            "sex": "男",
            "ethnicity": "汉",
            "birth": "20**-0*-0*",
            "address": "**********",
            "number": "****************",
            "detectReproduceResult": false,
            "detectCopyResult": false,
            "verificationResult": {
                "validNumber": true,
                "validBirth": true,
                "validSex": true,
                "validDate": false,
                "validValidityPeriod": false
            }
        },
        "back": null
    }
}

反面成功返回示例:

{
    "code": 0,
    "msg": "身份证识别成功",
    "data": {
        "front": null,
        "back": {
            "issue": "*******",
            "validFrom": "20**-0*-0*",
            "validTo": "20**-0*-0*",
            "detectReproduceResult": false,
            "detectCopyResult": false,
            "verificationResult": {
                "validNumber": false,
                "validBirth": false,
                "validSex": false,
                "validDate": true,
                "validValidityPeriod": true
            }
        }
    }
}

十、这次接入里几个值得记住的点

这次做完之后,我觉得有几个点特别值得记下来:

  • 不要把云厂商接口直接暴露给前端,最好后端统一收口
  • 文件上传场景下,MultipartFile -> Base64 -> withImage(...) 是很自然的做法
  • AK/SK/region 一定要从配置中读取,不要写死在代码里
  • 异常一定要统一转换,不要把 SDK 原始异常直接暴露给前端
  • 对返回结构做一层项目自己的 VO 映射,后续换供应商时会轻松很多

十一、总结

从实现难度上看,华为云身份证 OCR 的 Java SDK 接入并不复杂,真正需要花心思的其实不是“怎么调 SDK”,而是:

  • 怎么把调用方式封装得足够简单
  • 怎么处理异常和边界情况
  • 怎么把云厂商返回转换成项目自己的稳定接口

如果只是做一个能跑通的 Demo,很快就能完成。
但如果是要放进正式业务系统,配置隔离、参数校验、异常处理和结果映射这些工作,反而更关键。


如果这篇文章对你有帮助,请 点赞、收藏、关注 一波!你的支持是我持续输出高质量技术干货的最大动力!!!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐