1. 准备工作
在开始写代码前,请确保你已经完成了以下两步:
- 注册阿里云账号并实名认证。
- 开通 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
}
}