Springboot调用阿里云行驶证 OCR 识别

1. 准备工作

在开始写代码前,请确保你已经完成了以下两步:

  1. 注册阿里云账号并实名认证。
  2. 开通 OCR 服务
    • 登录 阿里云控制台
    • 搜索"文字识别 OCR"。
    • 找到"行驶证识别" (RecognizeVehicleLicense) 实例并开通(通常有免费额度)。
    • 创建 AccessKey (ID 和 Secret),这是你调用接口的"钥匙",请妥善保管。

2. 项目依赖配置

打开你的 Spring Boot 项目的 pom.xml 文件,添加以下依赖。我们需要 Lombok 简化代码,Hutool 处理 JSON,以及 阿里云 SDK

java 复制代码
<dependencies>
    <!-- 1. Lombok: 自动生成 Getter/Setter,让 DTO 更简洁 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.32</version>
    </dependency>

    <!-- 2. Hutool: 国产神器,处理 JSON 解析非常方便 -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.38</version>
    </dependency>

    <!-- 3. 阿里云 OCR SDK (Java 版) -->
    <!-- 注意:版本号请以阿里云官网最新为准,此处为示例 -->
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>ocr_api20210707</artifactId>
        <version>3.1.3</version> 
    </dependency>
    
    <!-- 4. Spring Boot Configuration Processor (可选,用于生成配置提示) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    
    <!-- Spring Boot Web (如果你还没有的话) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

3. 配置文件 (application.yml)

src/main/resources/application.yml 中配置你的阿里云密钥。

java 复制代码
aliyun:
  ocr:
    enabled: true
    # 地域,如 cn-shanghai, cn-beijing 等
    region: "cn-shanghai"
    # 服务端点,通常格式为 ocr.${region}.aliyuncs.com
    endpoint: "ocr.cn-shanghai.aliyuncs.com"
    # 替换为你自己的 AccessKey
    access-key-id: "LTAI5t..."
    access-key-secret: "9fX..."

4. 核心基础设施:配置与 Client 初始化

我们需要将配置文件映射到 Java 对象,并初始化阿里云的 Client Bean,以便在 Service 中直接注入使用。

4.1 配置属性类 (OcrConfigProperties.java)

使用 @ConfigurationProperties 将 yml 配置绑定到 Java 类,类型安全且方便管理。

java 复制代码
package com.chainLinker.common.config;

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

@Data
@ConfigurationProperties(prefix = "aliyun.ocr")
public class OcrConfigProperties {

    /**
     * 是否启用 OCR 服务
     */
    private boolean enabled = true;

    /**
     * 地域标识 (如 cn-shanghai)
     */
    private String region;

    /**
     * API 端点 (如 ocr.cn-shanghai.aliyuncs.com)
     */
    private String endpoint;

    /**
     * AccessKey ID
     */
    private String accessKeyId;

    /**
     * AccessKey Secret
     */
    private String accessKeySecret;
}

4.2 自动配置类 (OcrConfig.java)

负责创建阿里云 SDK 的 Client 单例 Bean,并增加日志脱敏功能,防止密钥泄露。

java 复制代码
package com.chainLinker.common.config;

import com.aliyun.ocr_api20210707.Client;
import com.aliyun.teaopenapi.models.Config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(OcrConfigProperties.class)
@RequiredArgsConstructor
@Slf4j
public class OcrConfig {

    private final OcrConfigProperties ocrConfigProperties;

    @Bean
    public Client createClient() throws Exception {
        if (!ocrConfigProperties.isEnabled()) {
            log.warn("阿里云 OCR 服务未启用 (enabled=false),跳过 Client 初始化");
            return null;
        }

        log.info("正在初始化阿里云 OCR Client...");
        // 日志脱敏打印,避免明文泄露 Key
        log.info("AccessKeyId: {}", maskAccessKey(ocrConfigProperties.getAccessKeyId()));

        Config config = new Config();
        config.accessKeyId = ocrConfigProperties.getAccessKeyId();
        config.accessKeySecret = ocrConfigProperties.getAccessKeySecret();
        config.endpoint = ocrConfigProperties.getEndpoint();
        
        // 可选:设置超时时间等
        // config.connectTimeout = 5000;
        // config.readTimeout = 5000;

        Client client = new Client(config);
        log.info("OCR Client 初始化完成,Endpoint: {}", config.endpoint);
        return client;
    }

    /**
     * 脱敏显示 AccessKey (保留前8位和后4位)
     */
    private String maskAccessKey(String accessKeyId) {
        if (accessKeyId == null || accessKeyId.length() < 12) {
            return "***";
        }
        return accessKeyId.substring(0, 8) + "***" + accessKeyId.substring(accessKeyId.length() - 4);
    }
}

5. 定义数据模型 (DTO)

5.1 行驶证正面 DTO (VehicleLicenseFaceDto)

java 复制代码
package com.chainLinker.common.dto;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class VehicleLicenseFaceDto {

    private String owner;                 // 所有人

    private String licensePlateNumber;    // 号牌号码

    private String vehicleType;           // 车辆类型

    private String model;                 // 品牌型号

    private String vinCode;               // 车辆识别代号 (车架号)

    private String engineNumber;          // 发动机号码

    private String registrationDate;      // 注册日期

    private String issueDate;             // 发证日期

    private String useNature;             // 使用性质

    private String address;               // 住址

    private String issueAuthority;        // 签发机关
}

5.2 行驶证反面 DTO (VehicleLicenseBackDto)

java 复制代码
package com.chainLinker.common.dto;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class VehicleLicenseBackDto {

    private String licensePlateNumber;    // 号牌号码

    private String passengerCapacity;     // 核定载人数

    private String totalWeight;           // 总质量

    private String curbWeight;            // 整备质量

    private String permittedWeight;       // 核定载质量

    private String overallDimension;      // 外廓尺寸

    private String tractionWeight;        // 准牵引总质量

    private String inspectionRecord;      // 检验记录

    private String energySign;            // 能源标志

    private String recordNumber;          // 档案编号

    private String remarks;               // 备注

    private String barcodeNumber;         // 条形码编号
}

5.3 统一返回结果 DTO (OcrResultDto)

java 复制代码
package com.chainLinker.common.dto;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class VehicleLicenseResultDto {
    private VehicleLicenseFaceDto face;
    private VehicleLicenseBackDto back;

    // 为了方便调用者直接获取车牌号,可以冗余这个字段
    private String plateNumber;
}

6. 核心工具类:JSON 解析器

阿里云返回的 JSON 结构嵌套较深,此工具类负责将其转换为我们的 DTO。

java 复制代码
package com.chainLinker.common.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.json.JSONObject;
import com.chainLinker.common.dto.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class VehicleLicenseParser {

    private static final Logger logger = LoggerFactory.getLogger(VehicleLicenseParser.class);

    public static VehicleLicenseResultDto parse(String jsonResponse) {
        if (StrUtil.isBlank(jsonResponse)) {
            return null;
        }

        try {
            // 1. 解析最外层
            JSONObject root = JSONUtil.parseObj(jsonResponse);
            JSONObject body = root.getJSONObject("body");
            if (body == null) {
                logger.warn("缺少 body 字段");
                return null;
            }

            // 2. 获取 body 中的 data 字符串 (这是阿里云返回的加密/序列化字符串)
            String dataStr = body.getStr("data");
            if (StrUtil.isBlank(dataStr)) {
                logger.warn("缺少 data 字符串");
                return null;
            }

            // 3. 【关键步骤 1】解析 data 字符串,得到中间层 JSON
            JSONObject innerJson = JSONUtil.parseObj(dataStr);

            // 4. 【关键步骤 2】再次获取内部的 "data" 字段!
            // 原始结构: innerJson -> { "data": { "face":..., "back":... } }
            JSONObject realData = innerJson.getJSONObject("data");

            if (realData == null) {
                logger.warn("OCR 内部结构中缺少 data 字段,实际结构可能已变更。innerJson keys: {}", innerJson.keySet());
                return null;
            }

            VehicleLicenseResultDto result = new VehicleLicenseResultDto();

            // 5. 现在可以从 realData 中获取 face 和 back 了
            if (realData.containsKey("face")) {
                JSONObject faceJson = realData.getJSONObject("face");

                // ⚠️ 注意:阿里云 OCR 的 face 对象里,真实数据还在一个 "data" 字段里!
                // 结构: "face": { "algo_version":..., "data": { "owner":..., "plate":... } }
                // 我们需要的是 face.data 里面的内容,而不是 face 本身
                JSONObject actualFaceData = faceJson.getJSONObject("data");

                if (actualFaceData != null) {
                    VehicleLicenseFaceDto face = JSONUtil.toBean(actualFaceData, VehicleLicenseFaceDto.class);
                    result.setFace(face);
                    if (face != null) {
                        result.setPlateNumber(face.getLicensePlateNumber());
                    }
                } else {
                    logger.warn("Face 对象中缺少 data 字段");
                }
            }

            if (realData.containsKey("back")) {
                JSONObject backJson = realData.getJSONObject("back");
                // 同理,back 的真实数据也在 "data" 字段里
                JSONObject actualBackData = backJson.getJSONObject("data");

                if (actualBackData != null) {
                    VehicleLicenseBackDto back = JSONUtil.toBean(actualBackData, VehicleLicenseBackDto.class);
                    result.setBack(back);
                }
            }

            return result;

        } catch (Exception e) {
            logger.error("OCR 解析异常", e);
            e.printStackTrace(); // 打印详细堆栈以便调试
            return null;
        }
    }
}

7. Service 业务层

注入我们在第 4 步创建的 Client Bean。

java 复制代码
package com.chainLinker.common.service;

import com.chainLinker.common.dto.VehicleLicenseResultDto;

import java.util.concurrent.ExecutionException;

public interface OrcService {

    /**
     * 识别车辆行驶证
     * @param url 图片地址
     * @return 车辆行驶证信息
     */
    public VehicleLicenseResultDto recognizeVehicleLicense(String url) throws ExecutionException, InterruptedException;
}
java 复制代码
package com.chainLinker.common.service.impl;

import com.alibaba.fastjson2.JSON;
import com.aliyun.ocr_api20210707.Client;
import com.aliyun.ocr_api20210707.models.RecognizeVehicleLicenseRequest;
import com.aliyun.ocr_api20210707.models.RecognizeVehicleLicenseResponse;
import com.aliyun.tea.TeaException;
import com.aliyun.teautil.models.RuntimeOptions;
import com.chainLinker.common.dto.VehicleLicenseResultDto;
import com.chainLinker.common.service.OrcService;
import com.chainLinker.common.utils.VehicleLicenseParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class OrcServiceImpl implements OrcService {

    @Autowired
    private Client ocrClient;

    /**
     * 识别车辆行驶证
     * @param url 图片地址
     * @return 车辆行驶证信息
     */
    @Override
    public VehicleLicenseResultDto recognizeVehicleLicense(String url) {
        RecognizeVehicleLicenseRequest request = new RecognizeVehicleLicenseRequest()
                .setUrl(url);
        RuntimeOptions runtime = new RuntimeOptions();
        try {
            RecognizeVehicleLicenseResponse resp = ocrClient.recognizeVehicleLicenseWithOptions(request, runtime);
            String jsonResponse = JSON.toJSONString(resp);
            System.out.println("原始 JSON: " + jsonResponse);
            if (jsonResponse == null || jsonResponse.isEmpty()) {
                log.error("SDK 返回内容为空");
                return null;
            }
            VehicleLicenseResultDto result = VehicleLicenseParser.parse(jsonResponse);
            if (result == null) {
                log.warn("OCR 识别成功,但未能提取出有效数据结构");
            } else {
                log.info("OCR 解析成功,车牌: {}", result.getPlateNumber());
            }
            return result;
        } catch (TeaException e) {
            log.error("阿里云 OCR 接口调用失败: {}", e.getMessage());
            if (e.getData() != null) {
                log.error("诊断建议: {}", e.getData().get("Recommend"));
            }
            return null;
        } catch (Exception e) {
            log.error("发生未知异常", e);
            return null;
        }
    }
}

8. Controller 控制器

提供上传接口。

java 复制代码
package com.chainLinker.member.controller;

import com.chainLinker.common.core.controller.BaseController;
import com.chainLinker.common.core.domain.AjaxResult;
import com.chainLinker.common.service.OrcService;
import com.chainLinker.delivPass.domain.BookOrder;
import com.chainLinker.delivPass.service.IBookFormTemplateFieldService;
import com.chainLinker.member.service.IMemberBookService;
import com.chainLinker.member.service.IMemberSupplierService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.util.concurrent.ExecutionException;

/**
 * 司机客户端预约管理Controller
 */
@RestController
@RequestMapping("/member/book")
public class MemberBookController extends BaseController {

    @Autowired
    private OrcService orcService;

    /**
     * 识别车辆信息
     * @param url 图片地址
     * @return 车辆信息
     * @throws ExecutionException 线程异常
     * @throws InterruptedException 线程异常
     */
    @GetMapping("/recognizeVehicleLicense")
    public AjaxResult RecognizeVehicleLicense(String url) throws ExecutionException, InterruptedException {
        return AjaxResult.success(orcService.recognizeVehicleLicense(url));
    }
}

9. 测试与验证

预期结果

java 复制代码
{
    "msg": "操作成功",
    "code": 200,
    "data": {
        "face": null,
        "back": {
            "licensePlateNumber": "皖LV**93",
            "passengerCapacity": "5人",
            "totalWeight": "2040kg",
            "curbWeight": "1605kg",
            "permittedWeight": "",
            "overallDimension": "4620×1910×1780mm",
            "tractionWeight": "",
            "inspectionRecord": "",
            "energySign": "汽油",
            "recordNumber": "341*****4375",
            "remarks": "",
            "barcodeNumber": "3*X00285****0"
        },
        "plateNumber": null
    }
}
相关推荐
EnCi Zheng6 小时前
11a. 阿里云大模型API调用基础
人工智能·python·阿里云·云计算
小扎仙森8 小时前
关于阿里云实时语音翻译-Gummy加WebSocket实现翻译功能
websocket·阿里云·云计算
腾科IT教育8 小时前
广东广州华为认证考点在哪里
华为云·云计算·hcie·华为认证考试
热爱专研AI的学妹9 小时前
DataEyes 聚合平台对接 Claude 开发实战:从数据采集到智能分析全流程
大数据·人工智能·阿里云
旭日跑马踏云飞10 小时前
不需要账号、免登录使用ClaudeCode+国内模型
人工智能·阿里云·ai·腾讯云·ai编程
TG_yunshuguoji1 天前
亚马逊云代理商:CloudWatch 日志查询实战 5 步精准定位 AWS 故障
服务器·云计算·aws
gaize12131 天前
阿里云 GPU 云服务器|AI 训练渲染专用
服务器·人工智能·阿里云
开开心心_Every1 天前
PDF密码移除工具,解除打印编辑复制权限免费
linux·运维·服务器·pdf·web3·ocr·共识算法
TG_yunshuguoji1 天前
阿里云代理商:百炼用AI重新定义图像的诞生
人工智能·阿里云·云计算