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:
kkfluous
2026-03-12 20:33:21 +08:00
parent 0706b51acd
commit 78a6cde22d
50 changed files with 3886 additions and 0 deletions

View File

@@ -50,6 +50,13 @@
<version>${revision}</version>
</dependency>
<!-- OCR 模块 API -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-module-ocr-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- Spring Cloud 基础 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>

View File

@@ -0,0 +1,111 @@
package cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleLicenseRecognizeRespVO;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleRegistrationPageReqVO;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleRegistrationRespVO;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleRegistrationSaveReqVO;
import cn.iocoder.yudao.module.asset.convert.vehicleregistration.VehicleRegistrationConvert;
import cn.iocoder.yudao.module.asset.dal.dataobject.vehicleregistration.VehicleRegistrationDO;
import cn.iocoder.yudao.module.asset.service.vehicleregistration.VehicleRegistrationService;
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 jakarta.validation.Valid;
import java.io.IOException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 车辆上牌记录 Controller
*
* @author 芋道源码
*/
@Tag(name = "管理后台 - 车辆上牌记录")
@RestController
@RequestMapping("/asset/vehicle-registration")
@Validated
@Slf4j
public class VehicleRegistrationController {
@Resource
private VehicleRegistrationService vehicleRegistrationService;
@PostMapping("/recognize-license")
@Operation(summary = "识别行驶证")
@PreAuthorize("@ss.hasPermission('asset:vehicle-registration:recognize')")
public CommonResult<VehicleLicenseRecognizeRespVO> recognizeVehicleLicense(
@Parameter(description = "行驶证图片文件", required = true)
@RequestParam("file") MultipartFile file) throws IOException {
log.info("[recognizeVehicleLicense][开始识别行驶证,文件名:{},大小:{}]",
file.getOriginalFilename(), file.getSize());
// 读取图片数据
byte[] imageData = file.getBytes();
// 调用识别服务
VehicleLicenseRecognizeRespVO result = vehicleRegistrationService.recognizeVehicleLicense(imageData);
return success(result);
}
@PostMapping("/create")
@Operation(summary = "创建车辆上牌记录")
@PreAuthorize("@ss.hasPermission('asset:vehicle-registration:create')")
public CommonResult<Long> createVehicleRegistration(@Valid @RequestBody VehicleRegistrationSaveReqVO createReqVO) {
return success(vehicleRegistrationService.createVehicleRegistration(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新车辆上牌记录")
@PreAuthorize("@ss.hasPermission('asset:vehicle-registration:update')")
public CommonResult<Boolean> updateVehicleRegistration(@Valid @RequestBody VehicleRegistrationSaveReqVO updateReqVO) {
vehicleRegistrationService.updateVehicleRegistration(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除车辆上牌记录")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('asset:vehicle-registration:delete')")
public CommonResult<Boolean> deleteVehicleRegistration(@RequestParam("id") Long id) {
vehicleRegistrationService.deleteVehicleRegistration(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得车辆上牌记录")
@Parameter(name = "id", description = "编号", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('asset:vehicle-registration:query')")
public CommonResult<VehicleRegistrationRespVO> getVehicleRegistration(@RequestParam("id") Long id) {
VehicleRegistrationDO registration = vehicleRegistrationService.getVehicleRegistration(id);
return success(VehicleRegistrationConvert.INSTANCE.convert(registration));
}
@GetMapping("/page")
@Operation(summary = "获得车辆上牌记录分页")
@PreAuthorize("@ss.hasPermission('asset:vehicle-registration:query')")
public CommonResult<PageResult<VehicleRegistrationRespVO>> getVehicleRegistrationPage(@Valid VehicleRegistrationPageReqVO pageReqVO) {
PageResult<VehicleRegistrationDO> pageResult = vehicleRegistrationService.getVehicleRegistrationPage(pageReqVO);
return success(VehicleRegistrationConvert.INSTANCE.convertPage(pageResult));
}
@PostMapping("/confirm")
@Operation(summary = "确认上牌记录(更新车辆信息)")
@Parameter(name = "id", description = "上牌记录ID", required = true)
@PreAuthorize("@ss.hasPermission('asset:vehicle-registration:update')")
public CommonResult<Boolean> confirmRegistration(@RequestParam("id") Long id) {
vehicleRegistrationService.confirmRegistration(id);
return success(true);
}
}

View File

@@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 行驶证识别 Response VO
*/
@Schema(description = "管理后台 - 行驶证识别 Response VO")
@Data
public class VehicleLicenseRecognizeRespVO {
@Schema(description = "车辆识别代号VIN", example = "LB9A32A22R0LS1439")
private String vin;
@Schema(description = "号牌号码", example = "粤AGR5547")
private String plateNo;
@Schema(description = "品牌型号", example = "帕力安牌XDQ5041XLCFCEV")
private String brand;
@Schema(description = "车辆类型", example = "轻型厢式货车")
private String vehicleType;
@Schema(description = "所有人", example = "广州开发区交投氯能运营管理有限公司")
private String owner;
@Schema(description = "使用性质", example = "货运")
private String useCharacter;
@Schema(description = "发动机号码", example = "268E7AEL153")
private String engineNo;
@Schema(description = "注册日期", example = "2025-02-19")
private LocalDate registerDate;
@Schema(description = "发证日期", example = "2025-10-21")
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;
// ==================== 扩展信息 ====================
@Schema(description = "车辆ID如果找到匹配车辆", example = "1")
private Long vehicleId;
@Schema(description = "匹配的车型ID", example = "1")
private Long vehicleModelId;
@Schema(description = "匹配置信度0-100", example = "95.5")
private BigDecimal matchConfidence;
@Schema(description = "匹配方式exact/fuzzy/none", example = "exact")
private String matchMethod;
@Schema(description = "OCR厂商", example = "baidu")
private String ocrProvider;
@Schema(description = "OCR识别耗时毫秒", example = "988")
private Integer ocrCostTime;
}

View File

@@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
/**
* 车辆上牌记录 Base VO
*/
@Data
public class VehicleRegistrationBaseVO {
@Schema(description = "车辆ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long vehicleId;
@Schema(description = "车辆识别代号VIN", requiredMode = Schema.RequiredMode.REQUIRED, example = "LB9A32A22R0LS1439")
private String vin;
@Schema(description = "车牌号", requiredMode = Schema.RequiredMode.REQUIRED, example = "粤AGR5547")
private String plateNo;
@Schema(description = "上牌日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2025-02-19")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate plateDate;
@Schema(description = "操作员", example = "张三")
private String operator;
@Schema(description = "OCR识别的品牌型号", example = "帕力安牌XDQ5041XLCFCEV")
private String recognizedBrand;
@Schema(description = "OCR识别的车型", example = "轻型厢式货车")
private String recognizedModel;
@Schema(description = "车辆类型", example = "轻型厢式货车")
private String vehicleType;
@Schema(description = "所有人", example = "广州开发区交投氯能运营管理有限公司")
private String owner;
@Schema(description = "使用性质", example = "货运")
private String useCharacter;
@Schema(description = "发动机号码", example = "268E7AEL153")
private String engineNo;
@Schema(description = "注册日期", example = "2025-02-19")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate registerDate;
@Schema(description = "发证日期", example = "2025-10-21")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate issueDate;
@Schema(description = "检验记录", example = "2026-06")
private String inspectionRecord;
@Schema(description = "强制报废期止", example = "2035-12-31")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
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;
@Schema(description = "匹配的车型ID", example = "1")
private Long vehicleModelId;
@Schema(description = "行驶证照片URL", example = "https://example.com/photo.jpg")
private String photoUrl;
@Schema(description = "照片大小(字节)", example = "890000")
private Integer photoSize;
@Schema(description = "OCR厂商", example = "baidu")
private String ocrProvider;
@Schema(description = "状态0-待确认 1-已确认 2-已作废)", example = "0")
private Integer status;
@Schema(description = "备注", example = "备注信息")
private String remark;
}

View File

@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
/**
* 车辆上牌记录分页查询 Request VO
*/
@Schema(description = "管理后台 - 车辆上牌记录分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class VehicleRegistrationPageReqVO extends PageParam {
@Schema(description = "车辆ID", example = "1")
private Long vehicleId;
@Schema(description = "车辆识别代号VIN", example = "LB9A32A22R0LS1439")
private String vin;
@Schema(description = "车牌号", example = "粤AGR5547")
private String plateNo;
@Schema(description = "上牌日期开始", example = "2025-01-01")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate plateDateStart;
@Schema(description = "上牌日期结束", example = "2025-12-31")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate plateDateEnd;
@Schema(description = "操作员", example = "张三")
private String operator;
@Schema(description = "状态0-待确认 1-已确认 2-已作废)", example = "0")
private Integer status;
@Schema(description = "创建时间开始", example = "2025-01-01 00:00:00")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime createTimeStart;
@Schema(description = "创建时间结束", example = "2025-12-31 23:59:59")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime createTimeEnd;
}

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 车辆上牌记录 Response VO
*/
@Schema(description = "管理后台 - 车辆上牌记录 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class VehicleRegistrationRespVO extends VehicleRegistrationBaseVO {
@Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "匹配置信度0-100", example = "95.5")
private BigDecimal matchConfidence;
@Schema(description = "匹配方式exact/fuzzy/manual", example = "exact")
private String matchMethod;
@Schema(description = "OCR识别耗时毫秒", example = "988")
private Integer ocrCostTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import jakarta.validation.constraints.NotNull;
/**
* 车辆上牌记录创建/更新 Request VO
*/
@Schema(description = "管理后台 - 车辆上牌记录创建/更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class VehicleRegistrationSaveReqVO extends VehicleRegistrationBaseVO {
@Schema(description = "主键ID更新时必填", example = "1")
private Long id;
}

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.asset.convert.vehicleregistration;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleRegistrationRespVO;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleRegistrationSaveReqVO;
import cn.iocoder.yudao.module.asset.dal.dataobject.vehicleregistration.VehicleRegistrationDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 车辆上牌记录 Convert
*
* @author 芋道源码
*/
@Mapper
public interface VehicleRegistrationConvert {
VehicleRegistrationConvert INSTANCE = Mappers.getMapper(VehicleRegistrationConvert.class);
VehicleRegistrationDO convert(VehicleRegistrationSaveReqVO bean);
VehicleRegistrationRespVO convert(VehicleRegistrationDO bean);
List<VehicleRegistrationRespVO> convertList(List<VehicleRegistrationDO> list);
PageResult<VehicleRegistrationRespVO> convertPage(PageResult<VehicleRegistrationDO> page);
}

View File

@@ -0,0 +1,183 @@
package cn.iocoder.yudao.module.asset.dal.dataobject.vehicleregistration;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 车辆上牌记录 DO
*
* @author 芋道源码
*/
@TableName("asset_vehicle_registration")
@KeySequence("asset_vehicle_registration_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VehicleRegistrationDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 车辆ID
*/
private Long vehicleId;
/**
* 车辆识别代号VIN
*/
private String vin;
/**
* 车牌号
*/
private String plateNo;
/**
* 上牌日期
*/
private LocalDate plateDate;
/**
* 操作员
*/
private String operator;
// ==================== OCR 识别信息 ====================
/**
* OCR识别的品牌型号
*/
private String recognizedBrand;
/**
* OCR识别的车型
*/
private String recognizedModel;
/**
* 车辆类型
*/
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;
// ==================== 匹配信息 ====================
/**
* 匹配的车型ID
*/
private Long vehicleModelId;
/**
* 匹配置信度0-100
*/
private BigDecimal matchConfidence;
/**
* 匹配方式exact/fuzzy/manual
*/
private String matchMethod;
// ==================== 照片信息 ====================
/**
* 行驶证照片URL
*/
private String photoUrl;
/**
* 照片大小(字节)
*/
private Integer photoSize;
// ==================== OCR 信息 ====================
/**
* OCR厂商
*/
private String ocrProvider;
/**
* OCR识别耗时毫秒
*/
private Integer ocrCostTime;
/**
* OCR原始结果JSON
*/
private String ocrRawResult;
// ==================== 状态信息 ====================
/**
* 状态0-待确认 1-已确认 2-已作废)
*/
private Integer status;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.asset.dal.mysql.vehicleregistration;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleRegistrationPageReqVO;
import cn.iocoder.yudao.module.asset.dal.dataobject.vehicleregistration.VehicleRegistrationDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 车辆上牌记录 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface VehicleRegistrationMapper extends BaseMapperX<VehicleRegistrationDO> {
default PageResult<VehicleRegistrationDO> selectPage(VehicleRegistrationPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<VehicleRegistrationDO>()
.eqIfPresent(VehicleRegistrationDO::getVehicleId, reqVO.getVehicleId())
.likeIfPresent(VehicleRegistrationDO::getVin, reqVO.getVin())
.likeIfPresent(VehicleRegistrationDO::getPlateNo, reqVO.getPlateNo())
.betweenIfPresent(VehicleRegistrationDO::getPlateDate, reqVO.getPlateDateStart(), reqVO.getPlateDateEnd())
.likeIfPresent(VehicleRegistrationDO::getOperator, reqVO.getOperator())
.eqIfPresent(VehicleRegistrationDO::getStatus, reqVO.getStatus())
.betweenIfPresent(VehicleRegistrationDO::getCreateTime, reqVO.getCreateTimeStart(), reqVO.getCreateTimeEnd())
.orderByDesc(VehicleRegistrationDO::getId));
}
}

View File

@@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.asset.service.vehicleregistration;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleLicenseRecognizeRespVO;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleRegistrationPageReqVO;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleRegistrationSaveReqVO;
import cn.iocoder.yudao.module.asset.dal.dataobject.vehicleregistration.VehicleRegistrationDO;
import jakarta.validation.Valid;
/**
* 车辆上牌记录 Service 接口
*
* @author 芋道源码
*/
public interface VehicleRegistrationService {
/**
* 识别行驶证
*
* @param imageData 图片数据
* @return 识别结果
*/
VehicleLicenseRecognizeRespVO recognizeVehicleLicense(byte[] imageData);
/**
* 创建车辆上牌记录
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createVehicleRegistration(@Valid VehicleRegistrationSaveReqVO createReqVO);
/**
* 更新车辆上牌记录
*
* @param updateReqVO 更新信息
*/
void updateVehicleRegistration(@Valid VehicleRegistrationSaveReqVO updateReqVO);
/**
* 删除车辆上牌记录
*
* @param id 编号
*/
void deleteVehicleRegistration(Long id);
/**
* 获得车辆上牌记录
*
* @param id 编号
* @return 车辆上牌记录
*/
VehicleRegistrationDO getVehicleRegistration(Long id);
/**
* 获得车辆上牌记录分页
*
* @param pageReqVO 分页查询
* @return 车辆上牌记录分页
*/
PageResult<VehicleRegistrationDO> getVehicleRegistrationPage(VehicleRegistrationPageReqVO pageReqVO);
/**
* 确认上牌记录(更新车辆信息)
*
* @param id 上牌记录ID
*/
void confirmRegistration(Long id);
}

View File

@@ -0,0 +1,213 @@
package cn.iocoder.yudao.module.asset.service.vehicleregistration;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleLicenseRecognizeRespVO;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleRegistrationPageReqVO;
import cn.iocoder.yudao.module.asset.controller.admin.vehicleregistration.vo.VehicleRegistrationSaveReqVO;
import cn.iocoder.yudao.module.asset.convert.vehicleregistration.VehicleRegistrationConvert;
import cn.iocoder.yudao.module.asset.dal.dataobject.vehicle.VehicleBaseDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.vehicleregistration.VehicleRegistrationDO;
import cn.iocoder.yudao.module.asset.dal.mysql.vehicle.VehicleBaseMapper;
import cn.iocoder.yudao.module.asset.dal.mysql.vehicleregistration.VehicleRegistrationMapper;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.ocr.api.OcrApi;
import cn.iocoder.yudao.module.ocr.api.dto.VehicleLicenseRespDTO;
import cn.hutool.core.codec.Base64;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* 车辆上牌记录 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class VehicleRegistrationServiceImpl implements VehicleRegistrationService {
@Resource
private VehicleRegistrationMapper vehicleRegistrationMapper;
@Resource
private VehicleBaseMapper vehicleBaseMapper;
@Resource
private OcrApi ocrApi;
@Override
public VehicleLicenseRecognizeRespVO recognizeVehicleLicense(byte[] imageData) {
long startTime = System.currentTimeMillis();
// Base64 编码图片数据
String imageDataBase64 = Base64.encode(imageData);
// 调用 OCR API 识别
CommonResult<VehicleLicenseRespDTO> ocrResult = ocrApi.recognizeVehicleLicense(imageDataBase64, null);
long costTime = System.currentTimeMillis() - startTime;
// 检查调用结果
if (ocrResult == null || !ocrResult.isSuccess() || ocrResult.getData() == null) {
log.error("[recognizeVehicleLicense][OCR识别失败结果{}]", ocrResult);
throw exception(new ErrorCode(500, "OCR识别失败"));
}
VehicleLicenseRespDTO ocrData = ocrResult.getData();
log.info("[recognizeVehicleLicense][OCR识别完成耗时{}msVIN{},车牌号:{}]",
costTime, ocrData.getVin(), ocrData.getPlateNo());
// 转换为响应 VO
VehicleLicenseRecognizeRespVO respVO = new VehicleLicenseRecognizeRespVO();
BeanUtil.copyProperties(ocrData, respVO);
respVO.setOcrProvider("baidu");
respVO.setOcrCostTime((int) costTime);
// 根据 VIN 查找车辆
if (StrUtil.isNotBlank(ocrData.getVin())) {
VehicleBaseDO vehicle = vehicleBaseMapper.selectOne(new LambdaQueryWrapper<VehicleBaseDO>()
.eq(VehicleBaseDO::getVin, ocrData.getVin())
.last("LIMIT 1"));
if (vehicle != null) {
respVO.setVehicleId(vehicle.getId());
respVO.setVehicleModelId(vehicle.getVehicleModelId());
respVO.setMatchMethod("exact");
respVO.setMatchConfidence(new BigDecimal("100.00"));
log.info("[recognizeVehicleLicense][找到匹配车辆车辆ID{}]", vehicle.getId());
} else {
respVO.setMatchMethod("none");
respVO.setMatchConfidence(BigDecimal.ZERO);
log.warn("[recognizeVehicleLicense][未找到匹配车辆VIN{}]", ocrData.getVin());
}
}
return respVO;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long createVehicleRegistration(VehicleRegistrationSaveReqVO createReqVO) {
// 验证车辆是否存在
VehicleBaseDO vehicle = validateVehicleExists(createReqVO.getVehicleId());
// 插入
VehicleRegistrationDO registration = VehicleRegistrationConvert.INSTANCE.convert(createReqVO);
registration.setStatus(0); // 待确认
vehicleRegistrationMapper.insert(registration);
log.info("[createVehicleRegistration][创建上牌记录成功ID{}车辆ID{},车牌号:{}]",
registration.getId(), registration.getVehicleId(), registration.getPlateNo());
return registration.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateVehicleRegistration(VehicleRegistrationSaveReqVO updateReqVO) {
// 校验存在
validateVehicleRegistrationExists(updateReqVO.getId());
// 验证车辆是否存在
if (updateReqVO.getVehicleId() != null) {
validateVehicleExists(updateReqVO.getVehicleId());
}
// 更新
VehicleRegistrationDO updateObj = VehicleRegistrationConvert.INSTANCE.convert(updateReqVO);
vehicleRegistrationMapper.updateById(updateObj);
log.info("[updateVehicleRegistration][更新上牌记录成功ID{}]", updateReqVO.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteVehicleRegistration(Long id) {
// 校验存在
validateVehicleRegistrationExists(id);
// 删除
vehicleRegistrationMapper.deleteById(id);
log.info("[deleteVehicleRegistration][删除上牌记录成功ID{}]", id);
}
@Override
public VehicleRegistrationDO getVehicleRegistration(Long id) {
return vehicleRegistrationMapper.selectById(id);
}
@Override
public PageResult<VehicleRegistrationDO> getVehicleRegistrationPage(VehicleRegistrationPageReqVO pageReqVO) {
return vehicleRegistrationMapper.selectPage(pageReqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void confirmRegistration(Long id) {
// 获取上牌记录
VehicleRegistrationDO registration = validateVehicleRegistrationExists(id);
// 验证状态
if (registration.getStatus() != 0) {
throw exception(new ErrorCode(400, "上牌记录已确认或已作废"));
}
// 更新车辆信息
VehicleBaseDO vehicle = validateVehicleExists(registration.getVehicleId());
VehicleBaseDO updateVehicle = new VehicleBaseDO();
updateVehicle.setId(vehicle.getId());
updateVehicle.setPlateNo(registration.getPlateNo());
updateVehicle.setVin(registration.getVin());
updateVehicle.setEngineNo(registration.getEngineNo());
updateVehicle.setRegisterDate(registration.getRegisterDate());
updateVehicle.setScrapDate(registration.getScrapDate());
if (StrUtil.isNotBlank(registration.getInspectionRecord())) {
updateVehicle.setInspectExpire(registration.getInspectionRecord());
}
if (registration.getVehicleModelId() != null) {
updateVehicle.setVehicleModelId(registration.getVehicleModelId());
}
vehicleBaseMapper.updateById(updateVehicle);
// 更新上牌记录状态为已确认
VehicleRegistrationDO updateRegistration = new VehicleRegistrationDO();
updateRegistration.setId(id);
updateRegistration.setStatus(1);
vehicleRegistrationMapper.updateById(updateRegistration);
log.info("[confirmRegistration][确认上牌记录成功ID{}车辆ID{}]", id, vehicle.getId());
}
// ==================== 私有方法 ====================
private VehicleRegistrationDO validateVehicleRegistrationExists(Long id) {
VehicleRegistrationDO registration = vehicleRegistrationMapper.selectById(id);
if (registration == null) {
throw exception(new ErrorCode(400, "上牌记录不存在"));
}
return registration;
}
private VehicleBaseDO validateVehicleExists(Long vehicleId) {
VehicleBaseDO vehicle = vehicleBaseMapper.selectById(vehicleId);
if (vehicle == null) {
throw exception(new ErrorCode(400, "车辆不存在,请先录入车辆信息"));
}
return vehicle;
}
}