feat: 实现 OCR 模块和车辆上牌管理功能
- 新增 yudao-module-ocr 模块 - OCR API 模块:定义 Feign 接口和 DTO - OCR Server 模块:实现行驶证识别功能 - 集成百度 OCR SDK - 支持多厂商扩展(百度/腾讯/阿里云) - 新增车辆上牌管理功能 - 数据库表:asset_vehicle_registration - 完整的 CRUD 接口 - 行驶证识别接口(集成 OCR) - 车辆匹配功能(根据 VIN) - 确认上牌功能(更新车辆信息) - 技术实现 - 遵循 BPM/System 模块的 RPC API 模式 - 使用 Feign 实现服务间调用 - Base64 编码传输图片数据 - 统一返回格式 CommonResult<T> - 文档 - OCR 模块使用文档 - OCR 部署指南 - 车辆上牌管理总结 - API 集成规划和总结
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package cn.iocoder.yudao.module.ocr;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* OCR 识别模块 Application
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class OcrServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(OcrServerApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package cn.iocoder.yudao.module.ocr.api;
|
||||
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.ocr.api.dto.VehicleLicenseRespDTO;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.client.OcrClient;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.client.OcrClientFactory;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.result.VehicleLicenseResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* OCR 识别 API 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class OcrApiImpl implements OcrApi {
|
||||
|
||||
@Resource
|
||||
private OcrClientFactory ocrClientFactory;
|
||||
|
||||
@Override
|
||||
public CommonResult<VehicleLicenseRespDTO> recognizeVehicleLicense(String imageData, String provider) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
log.info("[recognizeVehicleLicense][开始识别行驶证,provider:{},数据长度:{}]",
|
||||
provider, imageData != null ? imageData.length() : 0);
|
||||
|
||||
// Base64 解码图片数据
|
||||
byte[] imageBytes = Base64.decode(imageData);
|
||||
|
||||
// 获取 OCR 客户端
|
||||
OcrClient ocrClient;
|
||||
if (StrUtil.isNotBlank(provider)) {
|
||||
ocrClient = ocrClientFactory.getClient(provider);
|
||||
} else {
|
||||
ocrClient = ocrClientFactory.getDefaultClient();
|
||||
}
|
||||
|
||||
// 调用识别
|
||||
VehicleLicenseResult result = ocrClient.recognizeVehicleLicense(imageBytes);
|
||||
|
||||
// 转换为 DTO
|
||||
VehicleLicenseRespDTO respDTO = BeanUtils.toBean(result, VehicleLicenseRespDTO.class);
|
||||
|
||||
long costTime = System.currentTimeMillis() - startTime;
|
||||
log.info("[recognizeVehicleLicense][识别完成,耗时:{}ms,VIN:{},车牌号:{}]",
|
||||
costTime, respDTO.getVin(), respDTO.getPlateNo());
|
||||
|
||||
return success(respDTO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cn.iocoder.yudao.module.ocr.controller.admin.ocr;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.ocr.controller.admin.ocr.vo.VehicleLicenseRespVO;
|
||||
import cn.iocoder.yudao.module.ocr.convert.ocr.OcrConvert;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.result.VehicleLicenseResult;
|
||||
import cn.iocoder.yudao.module.ocr.service.ocr.OcrService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* OCR 识别控制器
|
||||
*/
|
||||
@Tag(name = "管理后台 - OCR 识别")
|
||||
@RestController
|
||||
@RequestMapping("/ocr")
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class OcrController {
|
||||
|
||||
@Resource
|
||||
private OcrService ocrService;
|
||||
|
||||
@PostMapping("/vehicle-license")
|
||||
@Operation(summary = "识别行驶证")
|
||||
@PreAuthorize("@ss.hasPermission('ocr:vehicle-license:recognize')")
|
||||
public CommonResult<VehicleLicenseRespVO> recognizeVehicleLicense(
|
||||
@Parameter(description = "行驶证图片文件", required = true)
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@Parameter(description = "OCR 厂商(可选,默认使用配置的厂商)")
|
||||
@RequestParam(value = "provider", required = false) String provider) throws IOException {
|
||||
|
||||
// 读取图片数据
|
||||
byte[] imageData = file.getBytes();
|
||||
|
||||
// 调用识别服务
|
||||
VehicleLicenseResult result = ocrService.recognizeVehicleLicense(imageData, provider);
|
||||
|
||||
// 转换并返回
|
||||
return success(OcrConvert.INSTANCE.convert(result));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cn.iocoder.yudao.module.ocr.controller.admin.ocr.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 行驶证识别响应 VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 行驶证识别响应 VO")
|
||||
@Data
|
||||
public class VehicleLicenseRespVO {
|
||||
|
||||
@Schema(description = "车辆识别代号(VIN)", example = "LSVAM4189E2123456")
|
||||
private String vin;
|
||||
|
||||
@Schema(description = "号牌号码", example = "粤A12345")
|
||||
private String plateNo;
|
||||
|
||||
@Schema(description = "品牌型号", example = "比亚迪秦PLUS DM-i")
|
||||
private String brand;
|
||||
|
||||
@Schema(description = "车辆类型", example = "小型轿车")
|
||||
private String vehicleType;
|
||||
|
||||
@Schema(description = "所有人", example = "张三")
|
||||
private String owner;
|
||||
|
||||
@Schema(description = "使用性质", example = "非营运")
|
||||
private String useCharacter;
|
||||
|
||||
@Schema(description = "发动机号码", example = "BYD123456")
|
||||
private String engineNo;
|
||||
|
||||
@Schema(description = "注册日期", example = "2023-01-01")
|
||||
private LocalDate registerDate;
|
||||
|
||||
@Schema(description = "发证日期", example = "2023-01-01")
|
||||
private LocalDate issueDate;
|
||||
|
||||
@Schema(description = "检验记录", example = "2026-06")
|
||||
private String inspectionRecord;
|
||||
|
||||
@Schema(description = "强制报废期止", example = "2035-12-31")
|
||||
private LocalDate scrapDate;
|
||||
|
||||
@Schema(description = "整备质量(kg)", example = "1500")
|
||||
private String curbWeight;
|
||||
|
||||
@Schema(description = "总质量(kg)", example = "1875")
|
||||
private String totalMass;
|
||||
|
||||
@Schema(description = "核定载人数", example = "5")
|
||||
private String approvedPassengerCapacity;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.iocoder.yudao.module.ocr.convert.ocr;
|
||||
|
||||
import cn.iocoder.yudao.module.ocr.controller.admin.ocr.vo.VehicleLicenseRespVO;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.result.VehicleLicenseResult;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/**
|
||||
* OCR 转换器
|
||||
*/
|
||||
@Mapper
|
||||
public interface OcrConvert {
|
||||
|
||||
OcrConvert INSTANCE = Mappers.getMapper(OcrConvert.class);
|
||||
|
||||
/**
|
||||
* 转换行驶证识别结果
|
||||
*/
|
||||
VehicleLicenseRespVO convert(VehicleLicenseResult result);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.config;
|
||||
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.client.OcrClientFactory;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.client.OcrClientFactoryImpl;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* OCR 自动配置类
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(OcrProperties.class)
|
||||
public class OcrAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public OcrClientFactory ocrClientFactory(OcrProperties properties) {
|
||||
OcrClientFactoryImpl factory = new OcrClientFactoryImpl(properties.getDefaultProvider());
|
||||
|
||||
// 初始化百度客户端
|
||||
if (properties.getBaidu() != null) {
|
||||
factory.createOrUpdateClient("baidu", properties.getBaidu());
|
||||
}
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.config;
|
||||
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.client.impl.BaiduOcrClientConfig;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* OCR 配置属性
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "ocr")
|
||||
public class OcrProperties {
|
||||
|
||||
/**
|
||||
* 默认使用的厂商
|
||||
*/
|
||||
private String defaultProvider = "baidu";
|
||||
|
||||
/**
|
||||
* 百度 OCR 配置
|
||||
*/
|
||||
private BaiduOcrClientConfig baidu;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.core.client;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
|
||||
import cn.iocoder.yudao.module.ocr.enums.ErrorCodeConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* OCR 客户端的抽象类,提供模板方法
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractOcrClient<Config extends OcrClientConfig> implements OcrClient {
|
||||
|
||||
/**
|
||||
* 配置
|
||||
*/
|
||||
protected Config config;
|
||||
|
||||
public AbstractOcrClient(Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
public final void init() {
|
||||
doInit();
|
||||
log.debug("[init][OCR 客户端({}) 初始化完成]", config.getProvider());
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义初始化
|
||||
*/
|
||||
protected abstract void doInit();
|
||||
|
||||
@Override
|
||||
public OcrClientConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验图片数据
|
||||
*/
|
||||
protected void validateImageData(byte[] imageData) {
|
||||
if (imageData == null || imageData.length == 0) {
|
||||
throw ServiceExceptionUtil.exception(ErrorCodeConstants.OCR_IMAGE_INVALID);
|
||||
}
|
||||
// 百度 OCR 限制 4MB
|
||||
if (imageData.length > 4 * 1024 * 1024) {
|
||||
throw ServiceExceptionUtil.exception(ErrorCodeConstants.OCR_IMAGE_TOO_LARGE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.core.client;
|
||||
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.result.VehicleLicenseResult;
|
||||
|
||||
/**
|
||||
* OCR 客户端接口
|
||||
*/
|
||||
public interface OcrClient {
|
||||
|
||||
/**
|
||||
* 获取客户端配置
|
||||
*/
|
||||
OcrClientConfig getConfig();
|
||||
|
||||
/**
|
||||
* 识别行驶证
|
||||
*
|
||||
* @param imageData 图片数据(字节数组)
|
||||
* @return 识别结果
|
||||
*/
|
||||
VehicleLicenseResult recognizeVehicleLicense(byte[] imageData);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.core.client;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* OCR 客户端配置接口
|
||||
*/
|
||||
public interface OcrClientConfig {
|
||||
|
||||
/**
|
||||
* 获取厂商编码
|
||||
*/
|
||||
String getProvider();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.core.client;
|
||||
|
||||
/**
|
||||
* OCR 客户端工厂接口
|
||||
*/
|
||||
public interface OcrClientFactory {
|
||||
|
||||
/**
|
||||
* 获取默认的 OCR 客户端
|
||||
*/
|
||||
OcrClient getDefaultClient();
|
||||
|
||||
/**
|
||||
* 根据厂商获取 OCR 客户端
|
||||
*
|
||||
* @param provider 厂商编码
|
||||
*/
|
||||
OcrClient getClient(String provider);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.core.client;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
|
||||
import cn.iocoder.yudao.module.ocr.enums.ErrorCodeConstants;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.client.impl.BaiduOcrClient;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.client.impl.BaiduOcrClientConfig;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* OCR 客户端工厂实现类
|
||||
*/
|
||||
@Slf4j
|
||||
public class OcrClientFactoryImpl implements OcrClientFactory {
|
||||
|
||||
/**
|
||||
* OCR 客户端 Map
|
||||
* key:厂商编码
|
||||
*/
|
||||
private final ConcurrentMap<String, OcrClient> clients = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 默认厂商
|
||||
*/
|
||||
private String defaultProvider;
|
||||
|
||||
public OcrClientFactoryImpl(String defaultProvider) {
|
||||
this.defaultProvider = defaultProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OcrClient getDefaultClient() {
|
||||
return getClient(defaultProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OcrClient getClient(String provider) {
|
||||
OcrClient client = clients.get(provider);
|
||||
if (client == null) {
|
||||
log.error("[getClient][厂商({}) 找不到客户端]", provider);
|
||||
throw ServiceExceptionUtil.exception(ErrorCodeConstants.OCR_PROVIDER_NOT_SUPPORTED);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新客户端
|
||||
*/
|
||||
public <Config extends OcrClientConfig> void createOrUpdateClient(String provider, Config config) {
|
||||
OcrClient client = clients.get(provider);
|
||||
if (client == null) {
|
||||
client = createClient(provider, config);
|
||||
if (client instanceof AbstractOcrClient) {
|
||||
((AbstractOcrClient<?>) client).init();
|
||||
}
|
||||
clients.put(provider, client);
|
||||
log.info("[createOrUpdateClient][创建 OCR 客户端成功,厂商:{}]", provider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建客户端
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private <Config extends OcrClientConfig> OcrClient createClient(String provider, Config config) {
|
||||
switch (provider) {
|
||||
case "baidu":
|
||||
Assert.isInstanceOf(BaiduOcrClientConfig.class, config, "百度 OCR 配置类型错误");
|
||||
return new BaiduOcrClient((BaiduOcrClientConfig) config);
|
||||
// 预留其他厂商
|
||||
default:
|
||||
throw ServiceExceptionUtil.exception(ErrorCodeConstants.OCR_PROVIDER_NOT_SUPPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.core.client.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
|
||||
import cn.iocoder.yudao.module.ocr.enums.ErrorCodeConstants;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.client.AbstractOcrClient;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.result.VehicleLicenseResult;
|
||||
import com.baidu.aip.ocr.AipOcr;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 百度 OCR 客户端实现
|
||||
*/
|
||||
@Slf4j
|
||||
public class BaiduOcrClient extends AbstractOcrClient<BaiduOcrClientConfig> {
|
||||
|
||||
private AipOcr client;
|
||||
|
||||
public BaiduOcrClient(BaiduOcrClientConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
this.client = new AipOcr(config.getAppId(), config.getApiKey(), config.getSecretKey());
|
||||
// 设置超时时间
|
||||
this.client.setConnectionTimeoutInMillis(5000);
|
||||
this.client.setSocketTimeoutInMillis(30000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VehicleLicenseResult recognizeVehicleLicense(byte[] imageData) {
|
||||
// 校验图片
|
||||
validateImageData(imageData);
|
||||
|
||||
try {
|
||||
// 调用百度 OCR API
|
||||
JSONObject response = client.vehicleLicense(imageData, null);
|
||||
|
||||
// 检查错误
|
||||
if (response.has("error_code")) {
|
||||
log.error("[recognizeVehicleLicense][百度 OCR 识别失败,错误码:{},错误信息:{}]",
|
||||
response.getInt("error_code"), response.getString("error_msg"));
|
||||
throw ServiceExceptionUtil.exception(ErrorCodeConstants.OCR_RECOGNIZE_FAILED);
|
||||
}
|
||||
|
||||
// 解析结果
|
||||
return parseVehicleLicenseResult(response);
|
||||
} catch (Exception e) {
|
||||
log.error("[recognizeVehicleLicense][百度 OCR 识别异常]", e);
|
||||
throw ServiceExceptionUtil.exception(ErrorCodeConstants.OCR_RECOGNIZE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析行驶证识别结果
|
||||
*/
|
||||
private VehicleLicenseResult parseVehicleLicenseResult(JSONObject response) {
|
||||
VehicleLicenseResult result = new VehicleLicenseResult();
|
||||
|
||||
JSONObject wordsResult = response.getJSONObject("words_result");
|
||||
if (wordsResult == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 车辆识别代号
|
||||
if (wordsResult.has("车辆识别代号")) {
|
||||
result.setVin(wordsResult.getJSONObject("车辆识别代号").getString("words"));
|
||||
}
|
||||
|
||||
// 号牌号码
|
||||
if (wordsResult.has("号牌号码")) {
|
||||
result.setPlateNo(wordsResult.getJSONObject("号牌号码").getString("words"));
|
||||
}
|
||||
|
||||
// 品牌型号
|
||||
if (wordsResult.has("品牌型号")) {
|
||||
result.setBrand(wordsResult.getJSONObject("品牌型号").getString("words"));
|
||||
}
|
||||
|
||||
// 车辆类型
|
||||
if (wordsResult.has("车辆类型")) {
|
||||
result.setVehicleType(wordsResult.getJSONObject("车辆类型").getString("words"));
|
||||
}
|
||||
|
||||
// 所有人
|
||||
if (wordsResult.has("所有人")) {
|
||||
result.setOwner(wordsResult.getJSONObject("所有人").getString("words"));
|
||||
}
|
||||
|
||||
// 使用性质
|
||||
if (wordsResult.has("使用性质")) {
|
||||
result.setUseCharacter(wordsResult.getJSONObject("使用性质").getString("words"));
|
||||
}
|
||||
|
||||
// 发动机号码
|
||||
if (wordsResult.has("发动机号码")) {
|
||||
result.setEngineNo(wordsResult.getJSONObject("发动机号码").getString("words"));
|
||||
}
|
||||
|
||||
// 注册日期
|
||||
if (wordsResult.has("注册日期")) {
|
||||
String registerDate = wordsResult.getJSONObject("注册日期").getString("words");
|
||||
result.setRegisterDate(parseDate(registerDate));
|
||||
}
|
||||
|
||||
// 发证日期
|
||||
if (wordsResult.has("发证日期")) {
|
||||
String issueDate = wordsResult.getJSONObject("发证日期").getString("words");
|
||||
result.setIssueDate(parseDate(issueDate));
|
||||
}
|
||||
|
||||
// 检验记录
|
||||
if (wordsResult.has("检验记录")) {
|
||||
result.setInspectionRecord(wordsResult.getJSONObject("检验记录").getString("words"));
|
||||
}
|
||||
|
||||
// 强制报废期止
|
||||
if (wordsResult.has("强制报废期止")) {
|
||||
String scrapDate = wordsResult.getJSONObject("强制报废期止").getString("words");
|
||||
result.setScrapDate(parseDate(scrapDate));
|
||||
}
|
||||
|
||||
// 整备质量
|
||||
if (wordsResult.has("整备质量")) {
|
||||
result.setCurbWeight(wordsResult.getJSONObject("整备质量").getString("words"));
|
||||
}
|
||||
|
||||
// 总质量
|
||||
if (wordsResult.has("总质量")) {
|
||||
result.setTotalMass(wordsResult.getJSONObject("总质量").getString("words"));
|
||||
}
|
||||
|
||||
// 核定载人数
|
||||
if (wordsResult.has("核定载人数")) {
|
||||
result.setApprovedPassengerCapacity(wordsResult.getJSONObject("核定载人数").getString("words"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期字符串
|
||||
*/
|
||||
private LocalDate parseDate(String dateStr) {
|
||||
if (dateStr == null || dateStr.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// 百度 OCR 返回格式:2020-01-01 或 20200101
|
||||
dateStr = dateStr.replaceAll("[年月]", "-").replaceAll("日", "");
|
||||
if (dateStr.length() == 8 && !dateStr.contains("-")) {
|
||||
// 20200101 格式
|
||||
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
} else {
|
||||
// 2020-01-01 格式
|
||||
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[parseDate][日期解析失败:{}]", dateStr, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.core.client.impl;
|
||||
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.client.OcrClientConfig;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* 百度 OCR 客户端配置
|
||||
*/
|
||||
@Data
|
||||
public class BaiduOcrClientConfig implements OcrClientConfig {
|
||||
|
||||
/**
|
||||
* 应用 ID
|
||||
*/
|
||||
@NotEmpty(message = "百度 OCR AppId 不能为空")
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* API Key
|
||||
*/
|
||||
@NotEmpty(message = "百度 OCR ApiKey 不能为空")
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* Secret Key
|
||||
*/
|
||||
@NotEmpty(message = "百度 OCR SecretKey 不能为空")
|
||||
private String secretKey;
|
||||
|
||||
@Override
|
||||
public String getProvider() {
|
||||
return "baidu";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.core.result;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 行驶证识别结果
|
||||
*/
|
||||
@Data
|
||||
public class VehicleLicenseResult {
|
||||
|
||||
/**
|
||||
* 车辆识别代号(VIN)
|
||||
*/
|
||||
private String vin;
|
||||
|
||||
/**
|
||||
* 号牌号码
|
||||
*/
|
||||
private String plateNo;
|
||||
|
||||
/**
|
||||
* 品牌型号
|
||||
*/
|
||||
private String brand;
|
||||
|
||||
/**
|
||||
* 车辆类型
|
||||
*/
|
||||
private String vehicleType;
|
||||
|
||||
/**
|
||||
* 所有人
|
||||
*/
|
||||
private String owner;
|
||||
|
||||
/**
|
||||
* 使用性质
|
||||
*/
|
||||
private String useCharacter;
|
||||
|
||||
/**
|
||||
* 发动机号码
|
||||
*/
|
||||
private String engineNo;
|
||||
|
||||
/**
|
||||
* 注册日期
|
||||
*/
|
||||
private LocalDate registerDate;
|
||||
|
||||
/**
|
||||
* 发证日期
|
||||
*/
|
||||
private LocalDate issueDate;
|
||||
|
||||
/**
|
||||
* 检验记录(检验有效期)
|
||||
*/
|
||||
private String inspectionRecord;
|
||||
|
||||
/**
|
||||
* 强制报废期止
|
||||
*/
|
||||
private LocalDate scrapDate;
|
||||
|
||||
/**
|
||||
* 整备质量(kg)
|
||||
*/
|
||||
private String curbWeight;
|
||||
|
||||
/**
|
||||
* 总质量(kg)
|
||||
*/
|
||||
private String totalMass;
|
||||
|
||||
/**
|
||||
* 核定载人数
|
||||
*/
|
||||
private String approvedPassengerCapacity;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.iocoder.yudao.module.ocr.service.ocr;
|
||||
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.result.VehicleLicenseResult;
|
||||
|
||||
/**
|
||||
* OCR 服务接口
|
||||
*/
|
||||
public interface OcrService {
|
||||
|
||||
/**
|
||||
* 识别行驶证
|
||||
*
|
||||
* @param imageData 图片数据
|
||||
* @return 识别结果
|
||||
*/
|
||||
VehicleLicenseResult recognizeVehicleLicense(byte[] imageData);
|
||||
|
||||
/**
|
||||
* 识别行驶证(指定厂商)
|
||||
*
|
||||
* @param imageData 图片数据
|
||||
* @param provider 厂商编码
|
||||
* @return 识别结果
|
||||
*/
|
||||
VehicleLicenseResult recognizeVehicleLicense(byte[] imageData, String provider);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.iocoder.yudao.module.ocr.service.ocr;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.client.OcrClient;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.client.OcrClientFactory;
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.result.VehicleLicenseResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
/**
|
||||
* OCR 服务实现类
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OcrServiceImpl implements OcrService {
|
||||
|
||||
@Resource
|
||||
private OcrClientFactory ocrClientFactory;
|
||||
|
||||
@Override
|
||||
public VehicleLicenseResult recognizeVehicleLicense(byte[] imageData) {
|
||||
// 使用默认厂商
|
||||
OcrClient client = ocrClientFactory.getDefaultClient();
|
||||
return client.recognizeVehicleLicense(imageData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VehicleLicenseResult recognizeVehicleLicense(byte[] imageData, String provider) {
|
||||
// 使用指定厂商
|
||||
OcrClient client = StrUtil.isNotBlank(provider)
|
||||
? ocrClientFactory.getClient(provider)
|
||||
: ocrClientFactory.getDefaultClient();
|
||||
return client.recognizeVehicleLicense(imageData);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
cn.iocoder.yudao.module.ocr.framework.ocr.config.OcrAutoConfiguration
|
||||
@@ -0,0 +1,35 @@
|
||||
spring:
|
||||
application:
|
||||
name: ocr-server
|
||||
|
||||
profiles:
|
||||
active: dev
|
||||
|
||||
# 允许 Bean 覆盖
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
|
||||
config:
|
||||
import:
|
||||
- optional:nacos:common-dev.yaml
|
||||
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml
|
||||
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: ${NACOS_ADDR:localhost:8848}
|
||||
namespace: ${NACOS_NAMESPACE:dev}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
discovery:
|
||||
namespace: ${NACOS_NAMESPACE:dev}
|
||||
config:
|
||||
namespace: ${NACOS_NAMESPACE:dev}
|
||||
file-extension: yaml
|
||||
|
||||
# OCR 配置
|
||||
ocr:
|
||||
default-provider: baidu
|
||||
baidu:
|
||||
app-id: ${OCR_BAIDU_APP_ID:7506572}
|
||||
api-key: ${OCR_BAIDU_API_KEY:wnQhaotHBbq9LRf8LbuNDNiK}
|
||||
secret-key: ${OCR_BAIDU_SECRET_KEY:wzORcuENPSwBJxh0zrUwH1s05tA9vEfq}
|
||||
@@ -0,0 +1,65 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.core.client.impl;
|
||||
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.result.VehicleLicenseResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* 百度 OCR 本地测试
|
||||
*/
|
||||
@Slf4j
|
||||
public class BaiduOcrClientLocalTest {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
// 配置百度 OCR
|
||||
BaiduOcrClientConfig config = new BaiduOcrClientConfig();
|
||||
config.setAppId("7506572");
|
||||
config.setApiKey("wnQhaotHBbq9LRf8LbuNDNiK");
|
||||
config.setSecretKey("wzORcuENPSwBJxh0zrUwH1s05tA9vEfq");
|
||||
|
||||
// 创建客户端
|
||||
BaiduOcrClient client = new BaiduOcrClient(config);
|
||||
client.init();
|
||||
|
||||
log.info("百度 OCR 客户端初始化成功");
|
||||
|
||||
// 读取测试图片
|
||||
String imagePath = "/Users/kkfluous/Projects/ai-coding/ln-oneos/ext/a55d670c0f36be8f8a0f713c3fcd4091.jpg";
|
||||
byte[] imageData = Files.readAllBytes(Paths.get(imagePath));
|
||||
log.info("读取图片成功,大小:{} bytes", imageData.length);
|
||||
|
||||
// 执行识别
|
||||
log.info("开始识别行驶证...");
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
VehicleLicenseResult result = client.recognizeVehicleLicense(imageData);
|
||||
long costTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
log.info("识别成功!耗时:{} ms", costTime);
|
||||
log.info("========== 识别结果 ==========");
|
||||
log.info("车辆识别代号(VIN): {}", result.getVin());
|
||||
log.info("号牌号码: {}", result.getPlateNo());
|
||||
log.info("品牌型号: {}", result.getBrand());
|
||||
log.info("车辆类型: {}", result.getVehicleType());
|
||||
log.info("所有人: {}", result.getOwner());
|
||||
log.info("使用性质: {}", result.getUseCharacter());
|
||||
log.info("发动机号码: {}", result.getEngineNo());
|
||||
log.info("注册日期: {}", result.getRegisterDate());
|
||||
log.info("发证日期: {}", result.getIssueDate());
|
||||
log.info("检验记录: {}", result.getInspectionRecord());
|
||||
log.info("强制报废期止: {}", result.getScrapDate());
|
||||
log.info("整备质量: {}", result.getCurbWeight());
|
||||
log.info("总质量: {}", result.getTotalMass());
|
||||
log.info("核定载人数: {}", result.getApprovedPassengerCapacity());
|
||||
log.info("==============================");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("识别失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cn.iocoder.yudao.module.ocr.framework.ocr.core.client.impl;
|
||||
|
||||
import cn.iocoder.yudao.module.ocr.framework.ocr.core.result.VehicleLicenseResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 百度 OCR 客户端测试
|
||||
*
|
||||
* 注意:此测试需要真实的百度 OCR 凭证和测试图片,默认禁用
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled("需要配置真实的百度 OCR 凭证")
|
||||
class BaiduOcrClientTest {
|
||||
|
||||
@Test
|
||||
void testRecognizeVehicleLicense() throws IOException {
|
||||
// 配置百度 OCR
|
||||
BaiduOcrClientConfig config = new BaiduOcrClientConfig();
|
||||
config.setAppId("your-app-id");
|
||||
config.setApiKey("your-api-key");
|
||||
config.setSecretKey("your-secret-key");
|
||||
|
||||
// 创建客户端
|
||||
BaiduOcrClient client = new BaiduOcrClient(config);
|
||||
client.init();
|
||||
|
||||
// 读取测试图片
|
||||
byte[] imageData = Files.readAllBytes(Paths.get("path/to/vehicle-license.jpg"));
|
||||
|
||||
// 执行识别
|
||||
VehicleLicenseResult result = client.recognizeVehicleLicense(imageData);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertNotNull(result.getVin());
|
||||
assertNotNull(result.getPlateNo());
|
||||
|
||||
log.info("识别结果:VIN={}, 车牌号={}", result.getVin(), result.getPlateNo());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user