前言

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

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

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

一、官方资料

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

二、实现目标

我最终落地的接口如下:

  • 请求地址:POST */ocr/id-card
  • 请求类型:multipart/form-data
  • 请求参数:
    • file:身份证图片文件
    • side:可选,frontbackdouble_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 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐