✨ 车辆认证扩到6张证件图,字段与车辆档案对齐;补齐建车必填列
司机端车辆认证此前只有 3 张图、5 个字段,且语义与车辆管理不一致 (「副页」字段里装的其实是主页反面)。本次把证件内容整体对齐车辆表列: - 车辆表新增 owner_name(所有人:行驶证登记的单位名称,不是司机)与 vin; - 提交请求与司机端车辆卡片全量对齐表列,行驶证四张图各自独立、 不再把「主页反面」与「副页正面」挤在同一个字段; - 业务关系新增为司机自选(自有/管理挂靠/业务合作),不再由后端替用户猜。 顺带修掉三处会直接让功能不可用的缺陷: 1. 司机建车必失败:organization_name、approved_load_kg、business_relation、 energy_type、registration_no 五列 NOT NULL 且无默认值,而建车链路一个都不设置, 严格模式下 INSERT 直接报「doesn't have a default value」。已补齐并加服务端校验 (司机提交链路此前一句都不查,全靠前端)。 2. OCR 自动填充从未生效:后端按契约透传百度原始结果(中文键), 前端读的却是扁平字段名,永远 undefined 且不报错。新增 OcrFieldSupport 做归一化(键名别名容错、数值/日期/「长期」归一化、外廓尺寸拆三段、 有效期区间拆起止),响应的 fields 即前端直接回填的字段。 3. 审核通过不写回车辆档案:状态已通过、内容却还是旧的,自相矛盾。 现把生效快照的证件内容与指针合并成同一次 UPDATE 写回;快照是改造前 旧格式时明确报错,而不是静默写一堆空值。
This commit is contained in:
+49
-21
@@ -51,37 +51,65 @@ public class VehicleCertSubmitRequest implements Serializable {
|
||||
private Long id;
|
||||
@Schema(description = "车牌号")
|
||||
private String plateNo;
|
||||
@Schema(description = "车辆类型")
|
||||
private String vehicleType;
|
||||
@Schema(description = "车辆识别代号(车架号)")
|
||||
private String vin;
|
||||
@Schema(description = "发动机号")
|
||||
private String engineNo;
|
||||
@Schema(description = "所有人:行驶证登记的单位名称,不是司机")
|
||||
private String ownerName;
|
||||
@Schema(description = "车牌颜色")
|
||||
private String plateColor;
|
||||
@Schema(description = "车辆类型")
|
||||
private String vehicleType;
|
||||
@Schema(description = "车辆识别代号(车架号),选填")
|
||||
private String vin;
|
||||
|
||||
@Schema(description = "外廓长度,毫米")
|
||||
private Integer outerLength;
|
||||
@Schema(description = "外廓宽度,毫米")
|
||||
private Integer outerWidth;
|
||||
@Schema(description = "外廓高度,毫米")
|
||||
private Integer outerHeight;
|
||||
@Schema(description = "核定载质量,KG")
|
||||
private Integer approvedLoadKg;
|
||||
@Schema(description = "准牵引总质量,KG")
|
||||
private Integer tractionMassKg;
|
||||
@Schema(description = "能源类型")
|
||||
private String energyType;
|
||||
@Schema(description = "业务关系:自有 / 管理挂靠 / 业务合作")
|
||||
private String businessRelation;
|
||||
@Schema(description = "强制报废日期")
|
||||
private LocalDate compulsoryScrapDate;
|
||||
@Schema(description = "强制报废长期有效:0否 1是")
|
||||
private Integer compulsoryScrapLongTerm;
|
||||
|
||||
@Schema(description = "行驶证档案编号")
|
||||
private String licenseNo;
|
||||
@Schema(description = "行驶证主页照片")
|
||||
private String licenseFrontUrl;
|
||||
@Schema(description = "行驶证副页照片")
|
||||
private String licenseBackUrl;
|
||||
private String drivingLicenseNo;
|
||||
@Schema(description = "行驶证主页正面照片")
|
||||
private String drivingLicenseImage;
|
||||
@Schema(description = "行驶证主页反面照片")
|
||||
private String drivingLicenseMainBack;
|
||||
@Schema(description = "行驶证副页正面照片")
|
||||
private String drivingLicenseViceFront;
|
||||
@Schema(description = "行驶证副页反面照片")
|
||||
private String drivingLicenseViceBack;
|
||||
@Schema(description = "行驶证有效期起")
|
||||
private LocalDate licenseValidStart;
|
||||
private LocalDate drivingLicenseStartDate;
|
||||
@Schema(description = "行驶证有效期止")
|
||||
private LocalDate licenseValidEnd;
|
||||
@Schema(description = "行驶证长期有效")
|
||||
private Integer licenseLongTerm;
|
||||
private LocalDate drivingLicenseEndDate;
|
||||
@Schema(description = "行驶证长期有效:0否 1是")
|
||||
private Integer drivingLicenseLongTerm;
|
||||
|
||||
@Schema(description = "道路运输证号")
|
||||
private String roadTransportNo;
|
||||
private String roadTransportCertNo;
|
||||
@Schema(description = "道路运输证照片")
|
||||
private String roadTransportUrl;
|
||||
private String roadTransportCertImage;
|
||||
@Schema(description = "道路运输证有效期起")
|
||||
private LocalDate roadTransportValidStart;
|
||||
private LocalDate roadTransportCertStartDate;
|
||||
@Schema(description = "道路运输证有效期止")
|
||||
private LocalDate roadTransportValidEnd;
|
||||
@Schema(description = "道路运输证长期有效")
|
||||
private Integer roadTransportLongTerm;
|
||||
private LocalDate roadTransportCertEndDate;
|
||||
@Schema(description = "道路运输证长期有效:0否 1是")
|
||||
private Integer roadTransportCertLongTerm;
|
||||
|
||||
@Schema(description = "机动车登记编号")
|
||||
private String registrationNo;
|
||||
@Schema(description = "机动车登记本照片")
|
||||
private String registrationImage;
|
||||
|
||||
}
|
||||
|
||||
+10
@@ -66,6 +66,11 @@ public class TransportVehicle extends TenantEntity {
|
||||
*/
|
||||
@Schema(description = "车牌号")
|
||||
private String plateNo;
|
||||
/**
|
||||
* 所有人:行驶证登记的单位名称,不是司机
|
||||
*/
|
||||
@Schema(description = "所有人(行驶证登记的单位名称,不是司机)")
|
||||
private String ownerName;
|
||||
/**
|
||||
* 车牌颜色
|
||||
*/
|
||||
@@ -76,6 +81,11 @@ public class TransportVehicle extends TenantEntity {
|
||||
*/
|
||||
@Schema(description = "车辆类型")
|
||||
private String vehicleType;
|
||||
/**
|
||||
* 车辆识别代号(车架号)
|
||||
*/
|
||||
@Schema(description = "车辆识别代号(车架号)")
|
||||
private String vin;
|
||||
/**
|
||||
* 外廓长度,毫米
|
||||
*/
|
||||
|
||||
+10
@@ -56,4 +56,14 @@ public class BaiduOcrResultVO implements Serializable {
|
||||
@Schema(description = "百度OCR原始返回结果")
|
||||
private Map<String, Object> result;
|
||||
|
||||
/**
|
||||
* 归一化后的业务字段
|
||||
* <p>
|
||||
* 键与提交请求里的字段同名({@code plateNo}、{@code ownerName}…),调用方直接回填即可,
|
||||
* 不需要认识百度那套中文键名。只包含**识别到**的字段,没识别到的不出现(而不是给空串),
|
||||
* 调用方据此就能区分「没识别到」与「识别到空值」,不会把用户已填的内容覆盖成空。
|
||||
*/
|
||||
@Schema(description = "归一化后的业务字段,键与提交请求同名")
|
||||
private Map<String, Object> fields;
|
||||
|
||||
}
|
||||
|
||||
+76
-16
@@ -44,29 +44,89 @@ public class DriverVehicleCardVO implements Serializable {
|
||||
@Schema(description = "车牌号")
|
||||
private String plateNo;
|
||||
|
||||
@Schema(description = "所有人(行驶证登记的单位名称,不是司机)")
|
||||
private String ownerName;
|
||||
|
||||
@Schema(description = "车牌颜色")
|
||||
private String plateColor;
|
||||
|
||||
@Schema(description = "车辆类型")
|
||||
private String vehicleType;
|
||||
|
||||
@Schema(description = "行驶证主页照片")
|
||||
private String licenseFrontUrl;
|
||||
|
||||
@Schema(description = "行驶证副页照片")
|
||||
private String licenseBackUrl;
|
||||
|
||||
@Schema(description = "道路运输证号")
|
||||
private String roadTransportNo;
|
||||
|
||||
@Schema(description = "道路运输证照片")
|
||||
private String roadTransportUrl;
|
||||
|
||||
@Schema(description = "车架号")
|
||||
@Schema(description = "车辆识别代号(车架号),选填")
|
||||
private String vin;
|
||||
|
||||
@Schema(description = "发动机号")
|
||||
private String engineNo;
|
||||
@Schema(description = "外廓长度,毫米")
|
||||
private Integer outerLength;
|
||||
|
||||
@Schema(description = "外廓宽度,毫米")
|
||||
private Integer outerWidth;
|
||||
|
||||
@Schema(description = "外廓高度,毫米")
|
||||
private Integer outerHeight;
|
||||
|
||||
@Schema(description = "核定载质量,KG")
|
||||
private Integer approvedLoadKg;
|
||||
|
||||
@Schema(description = "准牵引总质量,KG")
|
||||
private Integer tractionMassKg;
|
||||
|
||||
@Schema(description = "能源类型")
|
||||
private String energyType;
|
||||
|
||||
@Schema(description = "业务关系:自有 / 管理挂靠 / 业务合作")
|
||||
private String businessRelation;
|
||||
|
||||
@Schema(description = "强制报废日期")
|
||||
private String compulsoryScrapDate;
|
||||
|
||||
@Schema(description = "强制报废长期有效:0否 1是")
|
||||
private Integer compulsoryScrapLongTerm;
|
||||
|
||||
@Schema(description = "行驶证档案编号")
|
||||
private String drivingLicenseNo;
|
||||
|
||||
@Schema(description = "行驶证主页正面照片")
|
||||
private String drivingLicenseImage;
|
||||
|
||||
@Schema(description = "行驶证主页反面照片")
|
||||
private String drivingLicenseMainBack;
|
||||
|
||||
@Schema(description = "行驶证副页正面照片")
|
||||
private String drivingLicenseViceFront;
|
||||
|
||||
@Schema(description = "行驶证副页反面照片")
|
||||
private String drivingLicenseViceBack;
|
||||
|
||||
@Schema(description = "行驶证有效期起")
|
||||
private String drivingLicenseStartDate;
|
||||
|
||||
@Schema(description = "行驶证有效期止")
|
||||
private String licenseValidEnd;
|
||||
private String drivingLicenseEndDate;
|
||||
|
||||
@Schema(description = "行驶证长期有效:0否 1是")
|
||||
private Integer drivingLicenseLongTerm;
|
||||
|
||||
@Schema(description = "道路运输证号")
|
||||
private String roadTransportCertNo;
|
||||
|
||||
@Schema(description = "道路运输证照片")
|
||||
private String roadTransportCertImage;
|
||||
|
||||
@Schema(description = "道路运输证有效期起")
|
||||
private String roadTransportCertStartDate;
|
||||
|
||||
@Schema(description = "道路运输证有效期止")
|
||||
private String roadTransportCertEndDate;
|
||||
|
||||
@Schema(description = "道路运输证长期有效:0否 1是")
|
||||
private Integer roadTransportCertLongTerm;
|
||||
|
||||
@Schema(description = "机动车登记编号")
|
||||
private String registrationNo;
|
||||
|
||||
@Schema(description = "机动车登记本照片")
|
||||
private String registrationImage;
|
||||
|
||||
@Schema(description = "认证状态:0认证中 1认证通过 2认证驳回")
|
||||
private Integer certificationStatus;
|
||||
|
||||
+4
@@ -36,6 +36,7 @@ import org.springblade.transport.ocr.config.BaiduOcrProperties;
|
||||
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||
import org.springblade.transport.ocr.service.IBaiduOcrService;
|
||||
import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
|
||||
import org.springblade.transport.support.OcrFieldSupport;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -143,6 +144,9 @@ public class BaiduOcrServiceImpl implements IBaiduOcrService {
|
||||
response.setType(type.name());
|
||||
response.setSide(normalizedSide);
|
||||
response.setResult(objectMapper.convertValue(result, RESULT_TYPE));
|
||||
// 同时下发归一化字段:调用方不该去认识百度那套中文键名,
|
||||
// 且键名对不上时前端只会「静默什么都不填」,是最难查的一类问题
|
||||
response.setFields(OcrFieldSupport.extract(type, normalizedSide, result));
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
+68
@@ -25,6 +25,7 @@
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
@@ -32,6 +33,7 @@ import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.mapper.TransportCertRecordMapper;
|
||||
import org.springblade.transport.pojo.dto.VehicleCertSubmitRequest;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.entity.TransportCertRecord;
|
||||
import org.springblade.transport.pojo.entity.TransportVehicle;
|
||||
@@ -197,6 +199,11 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
vehicle.setCertificationRejectReason(reason);
|
||||
if (pass) {
|
||||
vehicle.setEffectiveCertRecordId(newEffectiveId);
|
||||
// 通过即生效:把这次快照的证件内容一并写回车辆档案,让「车辆档案 = 生效值」成立。
|
||||
// 不写回的话,审核通过之后管理端与司机端看到的仍是提交之前的旧值——
|
||||
// 状态显示「已通过」、内容却还是旧的,是最容易让人误判的一种不一致。
|
||||
// 与指针合并成同一次 UPDATE,不额外多写一次同一行。
|
||||
applySnapshotToVehicle(vehicle, record.getSnapshotJson());
|
||||
}
|
||||
transportVehicleService.updateById(vehicle);
|
||||
return;
|
||||
@@ -211,6 +218,67 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
driverService.updateById(driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把生效快照里的车辆证件字段搬进车辆档案(就地写入同一个 patch 对象)
|
||||
* <p>
|
||||
* 只搬**证件内容**,不碰两类字段:
|
||||
* <ul>
|
||||
* <li>{@code plateNo}:车牌是这辆车的身份,不是本次提交可以改的内容;</li>
|
||||
* <li>{@code organizationName} / {@code useDepartment}:内部管理归属,
|
||||
* 由管理端维护,不该被司机的提交内容覆盖。</li>
|
||||
* </ul>
|
||||
* 快照里为 null 的字段不会写(框架的 not_null 更新策略会跳过),
|
||||
* 因此不会把管理端填过、司机端没填的字段清空。
|
||||
*
|
||||
* @param vehicle 待更新的车辆 patch
|
||||
* @param snapshotJson 该条记录的全量提交内容
|
||||
*/
|
||||
private void applySnapshotToVehicle(TransportVehicle vehicle, String snapshotJson) {
|
||||
if (Func.isEmpty(snapshotJson)) {
|
||||
throw new ServiceException("该认证记录没有提交内容,无法写入车辆档案");
|
||||
}
|
||||
VehicleCertSubmitRequest snapshot;
|
||||
try {
|
||||
snapshot = JSON.parseObject(snapshotJson, VehicleCertSubmitRequest.class);
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("该认证记录的提交内容无法解析,未写入车辆档案");
|
||||
}
|
||||
// 字段名对不上时 JSON 会解析成「全是 null」而不报错(改造前的历史快照就是这种情况),
|
||||
// 那样写回等于什么也没写。故用证件内容里的关键字段判定「这份快照是否是当前格式」。
|
||||
if (Func.isEmpty(snapshot.getDrivingLicenseNo()) && Func.isEmpty(snapshot.getApprovedLoadKg())) {
|
||||
throw new ServiceException("该认证记录的提交内容与当前字段定义不匹配(可能是改造前的历史快照),"
|
||||
+ "未写入车辆档案;请重新提交一次认证");
|
||||
}
|
||||
vehicle.setOwnerName(snapshot.getOwnerName());
|
||||
vehicle.setPlateColor(snapshot.getPlateColor());
|
||||
vehicle.setVehicleType(snapshot.getVehicleType());
|
||||
vehicle.setVin(snapshot.getVin());
|
||||
vehicle.setOuterLength(snapshot.getOuterLength());
|
||||
vehicle.setOuterWidth(snapshot.getOuterWidth());
|
||||
vehicle.setOuterHeight(snapshot.getOuterHeight());
|
||||
vehicle.setApprovedLoadKg(snapshot.getApprovedLoadKg());
|
||||
vehicle.setTractionMassKg(snapshot.getTractionMassKg());
|
||||
vehicle.setEnergyType(snapshot.getEnergyType());
|
||||
vehicle.setBusinessRelation(snapshot.getBusinessRelation());
|
||||
vehicle.setCompulsoryScrapDate(snapshot.getCompulsoryScrapDate());
|
||||
vehicle.setCompulsoryScrapLongTerm(snapshot.getCompulsoryScrapLongTerm());
|
||||
vehicle.setDrivingLicenseNo(snapshot.getDrivingLicenseNo());
|
||||
vehicle.setDrivingLicenseImage(snapshot.getDrivingLicenseImage());
|
||||
vehicle.setDrivingLicenseMainBack(snapshot.getDrivingLicenseMainBack());
|
||||
vehicle.setDrivingLicenseViceFront(snapshot.getDrivingLicenseViceFront());
|
||||
vehicle.setDrivingLicenseViceBack(snapshot.getDrivingLicenseViceBack());
|
||||
vehicle.setDrivingLicenseStartDate(snapshot.getDrivingLicenseStartDate());
|
||||
vehicle.setDrivingLicenseEndDate(snapshot.getDrivingLicenseEndDate());
|
||||
vehicle.setDrivingLicenseLongTerm(snapshot.getDrivingLicenseLongTerm());
|
||||
vehicle.setRoadTransportCertNo(snapshot.getRoadTransportCertNo());
|
||||
vehicle.setRoadTransportCertImage(snapshot.getRoadTransportCertImage());
|
||||
vehicle.setRoadTransportCertStartDate(snapshot.getRoadTransportCertStartDate());
|
||||
vehicle.setRoadTransportCertEndDate(snapshot.getRoadTransportCertEndDate());
|
||||
vehicle.setRoadTransportCertLongTerm(snapshot.getRoadTransportCertLongTerm());
|
||||
vehicle.setRegistrationNo(snapshot.getRegistrationNo());
|
||||
vehicle.setRegistrationImage(snapshot.getRegistrationImage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 取该对象当前的生效快照指针
|
||||
*
|
||||
|
||||
+44
-17
@@ -42,6 +42,7 @@ import org.springblade.transport.support.VehiclePlateSupport;
|
||||
import org.springblade.transport.wrapper.DriverWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -119,29 +120,55 @@ public class DriverAppServiceImpl implements IDriverAppService {
|
||||
DriverVehicleCardVO card = new DriverVehicleCardVO();
|
||||
card.setId(vehicle.getId());
|
||||
card.setPlateNo(vehicle.getPlateNo());
|
||||
card.setOwnerName(Func.toStr(vehicle.getOwnerName(), ""));
|
||||
card.setPlateColor(Func.toStr(vehicle.getPlateColor(), ""));
|
||||
card.setVehicleType(vehicle.getVehicleType());
|
||||
card.setLicenseFrontUrl(Func.toStr(vehicle.getDrivingLicenseImage(), ""));
|
||||
card.setLicenseBackUrl(firstNotEmpty(vehicle.getDrivingLicenseViceFront(), vehicle.getDrivingLicenseMainBack()));
|
||||
card.setRoadTransportNo(Func.toStr(vehicle.getRoadTransportCertNo(), ""));
|
||||
card.setRoadTransportUrl(Func.toStr(vehicle.getRoadTransportCertImage(), ""));
|
||||
card.setVin("");
|
||||
card.setEngineNo("");
|
||||
if (vehicle.getDrivingLicenseEndDate() != null) {
|
||||
card.setLicenseValidEnd(vehicle.getDrivingLicenseEndDate().toString());
|
||||
} else if (Integer.valueOf(1).equals(vehicle.getDrivingLicenseLongTerm())) {
|
||||
card.setLicenseValidEnd("长期");
|
||||
} else {
|
||||
card.setLicenseValidEnd("");
|
||||
}
|
||||
card.setVin(Func.toStr(vehicle.getVin(), ""));
|
||||
card.setOuterLength(vehicle.getOuterLength());
|
||||
card.setOuterWidth(vehicle.getOuterWidth());
|
||||
card.setOuterHeight(vehicle.getOuterHeight());
|
||||
card.setApprovedLoadKg(vehicle.getApprovedLoadKg());
|
||||
card.setTractionMassKg(vehicle.getTractionMassKg());
|
||||
card.setEnergyType(Func.toStr(vehicle.getEnergyType(), ""));
|
||||
card.setBusinessRelation(Func.toStr(vehicle.getBusinessRelation(), ""));
|
||||
card.setCompulsoryScrapDate(dateText(vehicle.getCompulsoryScrapDate(), vehicle.getCompulsoryScrapLongTerm()));
|
||||
card.setCompulsoryScrapLongTerm(vehicle.getCompulsoryScrapLongTerm());
|
||||
card.setDrivingLicenseNo(Func.toStr(vehicle.getDrivingLicenseNo(), ""));
|
||||
// 行驶证四张图各自独立,不再把「主页反面」与「副页正面」挤在同一个字段里:
|
||||
// 它们本来就是证件上不同的四面,混在一起会让「重新认证」回填出错的图。
|
||||
card.setDrivingLicenseImage(Func.toStr(vehicle.getDrivingLicenseImage(), ""));
|
||||
card.setDrivingLicenseMainBack(Func.toStr(vehicle.getDrivingLicenseMainBack(), ""));
|
||||
card.setDrivingLicenseViceFront(Func.toStr(vehicle.getDrivingLicenseViceFront(), ""));
|
||||
card.setDrivingLicenseViceBack(Func.toStr(vehicle.getDrivingLicenseViceBack(), ""));
|
||||
card.setDrivingLicenseStartDate(dateText(vehicle.getDrivingLicenseStartDate(), null));
|
||||
card.setDrivingLicenseEndDate(dateText(vehicle.getDrivingLicenseEndDate(), vehicle.getDrivingLicenseLongTerm()));
|
||||
card.setDrivingLicenseLongTerm(vehicle.getDrivingLicenseLongTerm());
|
||||
card.setRoadTransportCertNo(Func.toStr(vehicle.getRoadTransportCertNo(), ""));
|
||||
card.setRoadTransportCertImage(Func.toStr(vehicle.getRoadTransportCertImage(), ""));
|
||||
card.setRoadTransportCertStartDate(dateText(vehicle.getRoadTransportCertStartDate(), null));
|
||||
card.setRoadTransportCertEndDate(dateText(vehicle.getRoadTransportCertEndDate(), vehicle.getRoadTransportCertLongTerm()));
|
||||
card.setRoadTransportCertLongTerm(vehicle.getRoadTransportCertLongTerm());
|
||||
card.setRegistrationNo(Func.toStr(vehicle.getRegistrationNo(), ""));
|
||||
card.setRegistrationImage(Func.toStr(vehicle.getRegistrationImage(), ""));
|
||||
card.setCertificationStatus(vehicle.getCertificationStatus());
|
||||
return card;
|
||||
}
|
||||
|
||||
private String firstNotEmpty(String first, String second) {
|
||||
if (Func.isNotEmpty(first)) {
|
||||
return first;
|
||||
/**
|
||||
* 日期字段的展示值:长期有效显示「长期」,没填显示空串,其余显示日期
|
||||
* <p>
|
||||
* 卡片是「重新认证」表单的回填来源,空值必须是空串而不是「null」这类字符串,
|
||||
* 否则用户会在日期框里看到一个字面量 null。
|
||||
*
|
||||
* @param date 日期
|
||||
* @param longTerm 长期有效标志:1 表示长期
|
||||
* @return 展示文本
|
||||
*/
|
||||
private String dateText(LocalDate date, Integer longTerm) {
|
||||
if (date != null) {
|
||||
return date.toString();
|
||||
}
|
||||
return Func.toStr(second, "");
|
||||
return Integer.valueOf(1).equals(longTerm) ? "长期" : "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+141
-11
@@ -49,9 +49,11 @@ import org.springblade.transport.wrapper.TransportCertRecordWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -70,11 +72,116 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class DriverCertServiceImpl implements IDriverCertService {
|
||||
|
||||
/**
|
||||
* 证件文本字段的长度上限:与车辆表列宽及管理端校验保持一致
|
||||
*/
|
||||
private static final int SHORT_TEXT_MAX_LENGTH = 50;
|
||||
|
||||
/**
|
||||
* 所有人(行驶证登记的单位名称)长度上限
|
||||
*/
|
||||
private static final int OWNER_NAME_MAX_LENGTH = 100;
|
||||
|
||||
private final IDriverAppService driverAppService;
|
||||
private final IDriverService driverService;
|
||||
private final ITransportVehicleService transportVehicleService;
|
||||
private final ITransportCertRecordService transportCertRecordService;
|
||||
|
||||
/**
|
||||
* 司机端提交的车辆认证内容校验
|
||||
* <p>
|
||||
* 服务端必须独立校验一遍,不能依赖小程序的前端校验:前端可绕过,
|
||||
* 且这些字段里 {@code organization_name}、{@code approved_load_kg}、
|
||||
* {@code business_relation}、{@code energy_type}、{@code registration_no}
|
||||
* 都是 NOT NULL 且无默认值——缺一个这条 INSERT 就直接报数据库错误,
|
||||
* 用户看到的是一串英文异常,而不是「哪个字段没填」。
|
||||
*
|
||||
* @param driver 当前司机
|
||||
* @param request 提交内容
|
||||
*/
|
||||
private void validateCertContent(Driver driver, VehicleCertSubmitRequest request) {
|
||||
if (Func.isEmpty(driver.getOrganizationName())) {
|
||||
throw new ServiceException("司机档案缺少所属组织,请联系管理员补充后再提交");
|
||||
}
|
||||
requireText(request.getPlateColor(), "车牌颜色不能为空", SHORT_TEXT_MAX_LENGTH);
|
||||
requireText(request.getVehicleType(), "车辆类型不能为空", SHORT_TEXT_MAX_LENGTH);
|
||||
requireText(request.getOwnerName(), "所有人不能为空", OWNER_NAME_MAX_LENGTH);
|
||||
requireText(request.getEnergyType(), "能源类型不能为空", SHORT_TEXT_MAX_LENGTH);
|
||||
requireText(request.getBusinessRelation(), "业务关系不能为空", SHORT_TEXT_MAX_LENGTH);
|
||||
requireText(request.getDrivingLicenseNo(), "行驶证档案编号不能为空", SHORT_TEXT_MAX_LENGTH);
|
||||
requireText(request.getRoadTransportCertNo(), "道路运输证号不能为空", SHORT_TEXT_MAX_LENGTH);
|
||||
Optional.ofNullable(request.getVin())
|
||||
.filter(Func::isNotEmpty)
|
||||
.ifPresent(vin -> requireText(vin, "车架号格式不正确", 17));
|
||||
requireNonNegative(request.getApprovedLoadKg(), "核定载质量不能为空", "核定载质量不能小于0");
|
||||
requireNonNegative(request.getTractionMassKg(), "准牵引总质量不能为空", "准牵引总质量不能小于0");
|
||||
requireNonNegative(request.getOuterLength(), "外廓长度不能为空", "外廓长度不能小于0");
|
||||
requireNonNegative(request.getOuterWidth(), "外廓宽度不能为空", "外廓宽度不能小于0");
|
||||
requireNonNegative(request.getOuterHeight(), "外廓高度不能为空", "外廓高度不能小于0");
|
||||
requireDateRange(request.getDrivingLicenseStartDate(), request.getDrivingLicenseEndDate(),
|
||||
request.getDrivingLicenseLongTerm(), "请选择行驶证有效期", "行驶证有效期止不能早于有效期起");
|
||||
requireDateRange(request.getRoadTransportCertStartDate(), request.getRoadTransportCertEndDate(),
|
||||
request.getRoadTransportCertLongTerm(), "请选择道路运输证有效期", "道路运输证有效期止不能早于有效期起");
|
||||
if (Func.isEmpty(request.getCompulsoryScrapDate())
|
||||
&& !Integer.valueOf(1).equals(request.getCompulsoryScrapLongTerm())) {
|
||||
throw new ServiceException("请选择强制报废日期");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本必填 + 长度校验
|
||||
*
|
||||
* @param value 待校验值
|
||||
* @param emptyText 为空时的提示
|
||||
* @param maxLength 长度上限
|
||||
*/
|
||||
private void requireText(String value, String emptyText, int maxLength) {
|
||||
if (Func.isEmpty(value)) {
|
||||
throw new ServiceException(emptyText);
|
||||
}
|
||||
if (value.trim().length() > maxLength) {
|
||||
throw new ServiceException(emptyText.replace("不能为空", "") + "不能超过" + maxLength + "字");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数值必填 + 非负校验
|
||||
*
|
||||
* @param value 待校验值
|
||||
* @param emptyText 为空时的提示
|
||||
* @param negativeText 为负数时的提示
|
||||
*/
|
||||
private void requireNonNegative(Integer value, String emptyText, String negativeText) {
|
||||
if (value == null) {
|
||||
throw new ServiceException(emptyText);
|
||||
}
|
||||
if (value < 0) {
|
||||
throw new ServiceException(negativeText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 有效期校验:未勾「长期有效」时起止必填,且止不得早于起
|
||||
*
|
||||
* @param start 有效期起
|
||||
* @param end 有效期止
|
||||
* @param longTerm 长期有效标志:1 表示长期
|
||||
* @param emptyText 缺值提示
|
||||
* @param rangeText 区间颠倒提示
|
||||
*/
|
||||
private void requireDateRange(LocalDate start, LocalDate end, Integer longTerm,
|
||||
String emptyText, String rangeText) {
|
||||
if (Integer.valueOf(1).equals(longTerm)) {
|
||||
return;
|
||||
}
|
||||
if (start == null || end == null) {
|
||||
throw new ServiceException(emptyText);
|
||||
}
|
||||
if (end.isBefore(start)) {
|
||||
throw new ServiceException(rangeText);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long submitPersonCert(PersonCertSubmitRequest request) {
|
||||
@@ -330,21 +437,44 @@ public class DriverCertServiceImpl implements IDriverCertService {
|
||||
*/
|
||||
private TransportVehicle createVehicleForDriver(Driver driver, VehicleCertSubmitRequest request,
|
||||
String normalizedPlate) {
|
||||
validateCertContent(driver, request);
|
||||
|
||||
TransportVehicle vehicle = new TransportVehicle();
|
||||
vehicle.setPlateNo(normalizedPlate);
|
||||
// 所属组织取司机档案:车辆表的该列 NOT NULL 且无默认值,不填这条 INSERT 直接报
|
||||
// 「Field 'organization_name' doesn't have a default value」(严格模式下必错)。
|
||||
// 车辆归到司机所属组织名下,管理端按组织查看时才看得到这台车。
|
||||
vehicle.setOrganizationName(driver.getOrganizationName());
|
||||
vehicle.setOwnerName(request.getOwnerName());
|
||||
vehicle.setPlateColor(request.getPlateColor());
|
||||
vehicle.setVehicleType(request.getVehicleType());
|
||||
vehicle.setDrivingLicenseNo(request.getLicenseNo());
|
||||
vehicle.setDrivingLicenseStartDate(request.getLicenseValidStart());
|
||||
vehicle.setDrivingLicenseEndDate(request.getLicenseValidEnd());
|
||||
vehicle.setDrivingLicenseLongTerm(request.getLicenseLongTerm());
|
||||
vehicle.setDrivingLicenseImage(request.getLicenseFrontUrl());
|
||||
vehicle.setDrivingLicenseMainBack(request.getLicenseBackUrl());
|
||||
vehicle.setRoadTransportCertNo(request.getRoadTransportNo());
|
||||
vehicle.setRoadTransportCertStartDate(request.getRoadTransportValidStart());
|
||||
vehicle.setRoadTransportCertEndDate(request.getRoadTransportValidEnd());
|
||||
vehicle.setRoadTransportCertLongTerm(request.getRoadTransportLongTerm());
|
||||
vehicle.setRoadTransportCertImage(request.getRoadTransportUrl());
|
||||
vehicle.setVin(request.getVin());
|
||||
vehicle.setOuterLength(request.getOuterLength());
|
||||
vehicle.setOuterWidth(request.getOuterWidth());
|
||||
vehicle.setOuterHeight(request.getOuterHeight());
|
||||
vehicle.setApprovedLoadKg(request.getApprovedLoadKg());
|
||||
vehicle.setTractionMassKg(request.getTractionMassKg());
|
||||
vehicle.setEnergyType(request.getEnergyType());
|
||||
vehicle.setBusinessRelation(request.getBusinessRelation());
|
||||
vehicle.setCompulsoryScrapDate(request.getCompulsoryScrapDate());
|
||||
vehicle.setCompulsoryScrapLongTerm(request.getCompulsoryScrapLongTerm());
|
||||
// 行驶证四张图与车辆表四列一一对应,不再把「主页反面」塞进语义含糊的「副页」字段
|
||||
vehicle.setDrivingLicenseNo(request.getDrivingLicenseNo());
|
||||
vehicle.setDrivingLicenseImage(request.getDrivingLicenseImage());
|
||||
vehicle.setDrivingLicenseMainBack(request.getDrivingLicenseMainBack());
|
||||
vehicle.setDrivingLicenseViceFront(request.getDrivingLicenseViceFront());
|
||||
vehicle.setDrivingLicenseViceBack(request.getDrivingLicenseViceBack());
|
||||
vehicle.setDrivingLicenseStartDate(request.getDrivingLicenseStartDate());
|
||||
vehicle.setDrivingLicenseEndDate(request.getDrivingLicenseEndDate());
|
||||
vehicle.setDrivingLicenseLongTerm(request.getDrivingLicenseLongTerm());
|
||||
vehicle.setRoadTransportCertNo(request.getRoadTransportCertNo());
|
||||
vehicle.setRoadTransportCertStartDate(request.getRoadTransportCertStartDate());
|
||||
vehicle.setRoadTransportCertEndDate(request.getRoadTransportCertEndDate());
|
||||
vehicle.setRoadTransportCertLongTerm(request.getRoadTransportCertLongTerm());
|
||||
vehicle.setRoadTransportCertImage(request.getRoadTransportCertImage());
|
||||
// 机动车登记编号该列同样 NOT NULL:司机端是选填,缺值写空串而不是 null
|
||||
vehicle.setRegistrationNo(Func.toStr(request.getRegistrationNo(), ""));
|
||||
vehicle.setRegistrationImage(request.getRegistrationImage());
|
||||
vehicle.setStatus(BladeConstant.DB_STATUS_NORMAL);
|
||||
transportVehicleService.save(vehicle);
|
||||
|
||||
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.support;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 把百度 OCR 的原始结果归一化成业务字段
|
||||
* <p>
|
||||
* 百度返回的 {@code words_result} 键是**中文**(「号牌号码」「车辆识别代码」…),
|
||||
* 同一含义在纸质证与电子证照之间还有不同写法。让小程序去认识这些中文键,
|
||||
* 等于把百度接口的形状泄漏到前端,接口一改就要动多个端。
|
||||
* <p>
|
||||
* 归一化后下发的键与业务字段同名({@code plateNo}、{@code ownerName}…),前端拿到即可回填;
|
||||
* 原始结果仍一并下发,归一化没覆盖到的字段不至于丢失。
|
||||
* <p>
|
||||
* <b>必须做别名容错的原因</b>:官方文档列举的是「号牌号码」「车辆识别代码」,
|
||||
* 而实际返回里也出现过「车牌号码」「车辆识别代号」这类写法,电子行驶证还多出
|
||||
* 「强制报废期止」「能源种类」等字段。写死单个键名的后果是**识别成功但什么也没填**——
|
||||
* 不报错、不抛异常,最难排查。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class OcrFieldSupport {
|
||||
|
||||
/**
|
||||
* {@code words_result} 里每个条目形如 {@code {"words": "桂A12345"}}
|
||||
*/
|
||||
private static final String WORDS = "words";
|
||||
|
||||
/**
|
||||
* 表示「长期有效」的词:命中后不产出日期,而把对应的长期有效标志置 1
|
||||
*/
|
||||
private static final String LONG_TERM_TEXT = "长期";
|
||||
|
||||
/**
|
||||
* 日期写法:4 位年 + 1~2 位月 + 1~2 位日,中间允许任意非数字分隔(年月日 / 点 / 横线)
|
||||
*/
|
||||
private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})\\D+(\\d{1,2})\\D+(\\d{1,2})");
|
||||
|
||||
private OcrFieldSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按证件类型把原始结果归一化
|
||||
*
|
||||
* @param type 证件类型
|
||||
* @param side 证件面:front / back(目前两个面共用一套字段别名)
|
||||
* @param result 百度原始返回
|
||||
* @return 归一化字段(键与业务字段同名);识别不到任何字段时返回空 Map
|
||||
*/
|
||||
public static Map<String, Object> extract(BaiduOcrType type, String side, JsonNode result) {
|
||||
Map<String, String> words = readWords(result);
|
||||
Map<String, Object> fields = new LinkedHashMap<>();
|
||||
if (words.isEmpty()) {
|
||||
return fields;
|
||||
}
|
||||
for (Map.Entry<String, List<String>> entry : aliasTable(type).entrySet()) {
|
||||
String raw = firstMatch(words, entry.getValue());
|
||||
if (Func.isEmpty(raw)) {
|
||||
continue;
|
||||
}
|
||||
putField(fields, entry.getKey(), raw);
|
||||
}
|
||||
// 外廓尺寸是唯一「一项要拆成三个字段」的,单独处理
|
||||
splitOuterSize(words, fields);
|
||||
// 道路运输证的「有效期」常写成一个区间:止期已由上面取到最后一段,
|
||||
// 起期要从同一段文本里补,否则用户得照着一张图再敲一遍
|
||||
splitRangeStart(words, fields, "有效期", "roadTransportCertStartDate");
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从「有效期」这类区间文本里取出起期,补进对应的起期字段
|
||||
*
|
||||
* @param words 识别结果
|
||||
* @param fields 结果集
|
||||
* @param alias 区间所在的键名
|
||||
* @param startKey 要补的起期字段名
|
||||
*/
|
||||
private static void splitRangeStart(Map<String, String> words, Map<String, Object> fields,
|
||||
String alias, String startKey) {
|
||||
if (fields.containsKey(startKey)) {
|
||||
return;
|
||||
}
|
||||
String text = firstMatch(words, List.of(alias));
|
||||
if (Func.isEmpty(text)) {
|
||||
return;
|
||||
}
|
||||
List<String> dates = matchDates(text);
|
||||
// 至少两段才算区间:只有一段说明那是止期本身,硬当起期会填错
|
||||
if (dates.size() >= 2) {
|
||||
fields.put(startKey, dates.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 {@code words_result} 成「中文键 → 文本值」
|
||||
*
|
||||
* @param result 百度原始返回
|
||||
* @return 键值对;结构不符时返回空 Map
|
||||
*/
|
||||
private static Map<String, String> readWords(JsonNode result) {
|
||||
Map<String, String> words = new LinkedHashMap<>();
|
||||
if (result == null) {
|
||||
return words;
|
||||
}
|
||||
JsonNode wordsResult = result.path("words_result");
|
||||
if (!wordsResult.isObject()) {
|
||||
return words;
|
||||
}
|
||||
wordsResult.fields().forEachRemaining(entry -> {
|
||||
JsonNode value = entry.getValue();
|
||||
if (value == null || value.isNull()) {
|
||||
return;
|
||||
}
|
||||
// 可能是 {"words": "..."},也可能直接是字符串
|
||||
String text = value.isObject() ? value.path(WORDS).asText("") : value.asText("");
|
||||
if (Func.isNotEmpty(text)) {
|
||||
words.put(entry.getKey(), text.trim());
|
||||
}
|
||||
});
|
||||
return words;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取第一个命中的别名值
|
||||
*
|
||||
* @param words 识别结果
|
||||
* @param aliases 候选键名,按优先级排列
|
||||
* @return 命中值;都没命中返回 null
|
||||
*/
|
||||
private static String firstMatch(Map<String, String> words, List<String> aliases) {
|
||||
for (String alias : aliases) {
|
||||
String value = words.get(alias);
|
||||
if (Func.isNotEmpty(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
// 精确键名没命中时按「包含」兜底:百度偶有「核定载质量(kg)」这类带后缀的键
|
||||
for (String alias : aliases) {
|
||||
for (Map.Entry<String, String> entry : words.entrySet()) {
|
||||
if (entry.getKey().contains(alias) && Func.isNotEmpty(entry.getValue())) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入一个字段,按字段语义做值归一化
|
||||
* <p>
|
||||
* 数字字段转数字、日期字段转 {@code yyyy-MM-dd};「长期」不产出日期,
|
||||
* 而是把对应的长期有效标志置 1——否则「长期」会被当成一个非法日期塞进日期框。
|
||||
*
|
||||
* @param fields 结果集
|
||||
* @param key 业务字段名
|
||||
* @param raw 识别到的原始文本
|
||||
*/
|
||||
private static void putField(Map<String, Object> fields, String key, String raw) {
|
||||
String text = raw.trim();
|
||||
if (isDateField(key)) {
|
||||
if (text.contains(LONG_TERM_TEXT)) {
|
||||
String longTermKey = longTermKeyOf(key);
|
||||
if (longTermKey != null) {
|
||||
fields.put(longTermKey, 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
String date = normalizeDate(text, key);
|
||||
if (date != null) {
|
||||
fields.put(key, date);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isNumberField(key)) {
|
||||
Integer number = normalizeNumber(text);
|
||||
if (number != null) {
|
||||
fields.put(key, number);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fields.put(key, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 「外廓尺寸」一项拆成外廓长/宽/高三个字段
|
||||
* <p>
|
||||
* 形如 {@code 5990×2300×3100},分隔符有 ×、x、*、空格等多种写法。
|
||||
*
|
||||
* @param words 识别结果
|
||||
* @param fields 结果集
|
||||
*/
|
||||
private static void splitOuterSize(Map<String, String> words, Map<String, Object> fields) {
|
||||
String size = firstMatch(words, List.of("外廓尺寸"));
|
||||
if (Func.isEmpty(size)) {
|
||||
return;
|
||||
}
|
||||
List<String> parts = new ArrayList<>();
|
||||
for (String part : size.split("[^0-9]+")) {
|
||||
if (Func.isNotEmpty(part)) {
|
||||
parts.add(part);
|
||||
}
|
||||
}
|
||||
// 必须正好三段才写回:只识别到两段时多半是漏识别,
|
||||
// 按顺序硬填会把宽度填成长度、高度填成宽度
|
||||
if (parts.size() != 3) {
|
||||
return;
|
||||
}
|
||||
String[] keys = {"outerLength", "outerWidth", "outerHeight"};
|
||||
for (int index = 0; index < keys.length; index++) {
|
||||
Integer number = normalizeNumber(parts.get(index));
|
||||
if (number != null) {
|
||||
fields.put(keys[index], number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 各证件类型的字段别名表
|
||||
*
|
||||
* @param type 证件类型
|
||||
* @return 「业务字段名 → 百度键名候选」
|
||||
*/
|
||||
private static Map<String, List<String>> aliasTable(BaiduOcrType type) {
|
||||
if (type == null) {
|
||||
return Map.of();
|
||||
}
|
||||
String name = type.name();
|
||||
if (BaiduOcrType.ID_CARD.name().equals(name)) {
|
||||
return idCardAliases();
|
||||
}
|
||||
if (BaiduOcrType.DRIVING_LICENSE.name().equals(name)) {
|
||||
return drivingLicenseAliases();
|
||||
}
|
||||
if (BaiduOcrType.VEHICLE_LICENSE.name().equals(name)) {
|
||||
return vehicleLicenseAliases();
|
||||
}
|
||||
if (BaiduOcrType.ROAD_TRANSPORT_CERTIFICATE.name().equals(name)) {
|
||||
return roadTransportAliases();
|
||||
}
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> idCardAliases() {
|
||||
Map<String, List<String>> aliases = new LinkedHashMap<>();
|
||||
aliases.put("realName", List.of("姓名"));
|
||||
aliases.put("idCardNo", List.of("公民身份号码", "公民身份证号码", "身份证号", "身份证号码"));
|
||||
aliases.put("idCardValidStart", List.of("签发日期", "有效起始日期", "有效期限起"));
|
||||
aliases.put("idCardValidEnd", List.of("失效日期", "有效期限", "有效期限止"));
|
||||
return aliases;
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> drivingLicenseAliases() {
|
||||
Map<String, List<String>> aliases = new LinkedHashMap<>();
|
||||
aliases.put("driverFileNo", List.of("档案编号", "证号"));
|
||||
aliases.put("driverClass", List.of("准驾车型"));
|
||||
aliases.put("driverValidStart", List.of("有效起始日期", "有效期起", "初次领证日期"));
|
||||
aliases.put("driverValidEnd", List.of("有效期限", "有效期止", "有效期限止"));
|
||||
return aliases;
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> vehicleLicenseAliases() {
|
||||
Map<String, List<String>> aliases = new LinkedHashMap<>();
|
||||
aliases.put("plateNo", List.of("号牌号码", "车牌号码", "号牌"));
|
||||
aliases.put("vehicleType", List.of("车辆类型"));
|
||||
aliases.put("ownerName", List.of("所有人"));
|
||||
aliases.put("vin", List.of("车辆识别代码", "车辆识别代号", "车架号", "VIN"));
|
||||
aliases.put("drivingLicenseNo", List.of("档案编号", "行驶证档案编号"));
|
||||
aliases.put("plateColor", List.of("号牌颜色", "车牌颜色"));
|
||||
aliases.put("energyType", List.of("能源种类", "能源类型"));
|
||||
aliases.put("approvedLoadKg", List.of("核定载质量"));
|
||||
aliases.put("tractionMassKg", List.of("准牵引总质量"));
|
||||
aliases.put("compulsoryScrapDate", List.of("强制报废期止", "强制报废日期"));
|
||||
aliases.put("drivingLicenseStartDate", List.of("注册日期", "发证日期"));
|
||||
aliases.put("drivingLicenseEndDate", List.of("检验有效期止", "有效期止"));
|
||||
return aliases;
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> roadTransportAliases() {
|
||||
Map<String, List<String>> aliases = new LinkedHashMap<>();
|
||||
aliases.put("roadTransportCertNo", List.of("道路运输证号", "运输证号"));
|
||||
aliases.put("ownerName", List.of("业户名称", "所有人"));
|
||||
aliases.put("roadTransportCertEndDate", List.of("有效期", "有效期止", "有效期至"));
|
||||
return aliases;
|
||||
}
|
||||
|
||||
private static boolean isDateField(String key) {
|
||||
return key.endsWith("Date") || key.endsWith("ValidStart") || key.endsWith("ValidEnd");
|
||||
}
|
||||
|
||||
private static boolean isNumberField(String key) {
|
||||
return "approvedLoadKg".equals(key) || "tractionMassKg".equals(key);
|
||||
}
|
||||
|
||||
private static String longTermKeyOf(String dateKey) {
|
||||
if ("compulsoryScrapDate".equals(dateKey)) {
|
||||
return "compulsoryScrapLongTerm";
|
||||
}
|
||||
if ("drivingLicenseEndDate".equals(dateKey)) {
|
||||
return "drivingLicenseLongTerm";
|
||||
}
|
||||
if ("roadTransportCertEndDate".equals(dateKey)) {
|
||||
return "roadTransportCertLongTerm";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字归一化:去掉「kg」「毫米」等单位与千分位后取整
|
||||
*
|
||||
* @param text 识别文本
|
||||
* @return 整数;取不到合理数字返回 null
|
||||
*/
|
||||
private static Integer normalizeNumber(String text) {
|
||||
String digits = text.replaceAll("[^0-9]", "");
|
||||
// 载质量、外廓尺寸最多 6 位;过长说明识别串了字段,宁可不填也不填错
|
||||
if (digits.isEmpty() || digits.length() > 6) {
|
||||
return null;
|
||||
}
|
||||
return Integer.valueOf(digits);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期归一化
|
||||
* <p>
|
||||
* 覆盖 {@code 2015年03月12日} / {@code 2015.3.9} / {@code 2015-03-12} / {@code 20150312}
|
||||
* 这几种写法,统一成 {@code yyyy-MM-dd}。
|
||||
* <p>
|
||||
* 区间(如「2015.03.12-2035.03.12」「2020-01-01 至 2030-01-01」)**按字段语义取一端**:
|
||||
* 以 Start 结尾的取第一段,其余(止期类)取最后一段。取错一端不会报错,
|
||||
* 只会把有效期起填成止——比不填更难发现。
|
||||
*
|
||||
* @param text 识别文本
|
||||
* @param key 业务字段名,用于判断该取区间的哪一端
|
||||
* @return 归一化日期;无法识别返回 null
|
||||
*/
|
||||
private static String normalizeDate(String text, String key) {
|
||||
List<String> dates = matchDates(text);
|
||||
if (!dates.isEmpty()) {
|
||||
return key.endsWith("Start") ? dates.get(0) : dates.get(dates.size() - 1);
|
||||
}
|
||||
String compact = text.replaceAll("[^0-9]", "");
|
||||
if (compact.length() == 8) {
|
||||
return formatDate(compact);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取出文本里所有日期(按出现顺序)
|
||||
*
|
||||
* @param text 识别文本
|
||||
* @return 归一化后的日期列表
|
||||
*/
|
||||
private static List<String> matchDates(String text) {
|
||||
List<String> dates = new ArrayList<>();
|
||||
Matcher matcher = DATE_PATTERN.matcher(text);
|
||||
while (matcher.find()) {
|
||||
dates.add(String.format("%s-%02d-%02d", matcher.group(1),
|
||||
Integer.parseInt(matcher.group(2)), Integer.parseInt(matcher.group(3))));
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
private static String formatDate(String digits) {
|
||||
return digits.substring(0, 4) + "-" + digits.substring(4, 6) + "-" + digits.substring(6, 8);
|
||||
}
|
||||
|
||||
}
|
||||
+53
-1
@@ -15,6 +15,7 @@ import org.springblade.transport.service.ITransportCertRecordService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
@@ -75,6 +76,41 @@ class CertAuditServiceImplTest {
|
||||
"通过后指针应指向该记录");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("通过车辆认证:把快照的证件内容写回车辆档案,让「档案 = 生效值」成立")
|
||||
void approveWritesSnapshotBackToVehicle() {
|
||||
TransportCertRecord record = vehicleRecord(TransportCertRecord.STATUS_PENDING);
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
service.audit(RECORD_ID, true, null);
|
||||
|
||||
ArgumentCaptor<TransportVehicle> captor = ArgumentCaptor.forClass(TransportVehicle.class);
|
||||
verify(transportVehicleService).updateById(captor.capture());
|
||||
TransportVehicle patched = captor.getValue();
|
||||
assertEquals("桂物物流集团", patched.getOwnerName(), "所有人要跟着生效值更新");
|
||||
assertEquals("管理挂靠", patched.getBusinessRelation());
|
||||
assertEquals(18000, patched.getApprovedLoadKg());
|
||||
assertEquals(RECORD_ID, patched.getEffectiveCertRecordId(), "内容与指针必须是同一次 UPDATE");
|
||||
assertNull(patched.getPlateNo(), "车牌是车辆身份,不是本次提交能改的内容");
|
||||
assertNull(patched.getOrganizationName(), "所属组织由管理端维护,不该被提交内容覆盖");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("通过车辆认证:快照是改造前的旧格式时明确失败,不能「通过但什么也没写」")
|
||||
void approveRejectsLegacySnapshot() {
|
||||
// 旧快照的键名与当前字段定义不同,会解析成「全是 null」而不报错——最危险的一种情况
|
||||
TransportCertRecord record = vehicleRecord(TransportCertRecord.STATUS_PENDING,
|
||||
"{\"licenseNo\":\"450100123456\",\"licenseFrontUrl\":\"http://oss/old.jpg\"}");
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.audit(RECORD_ID, true, null));
|
||||
assertTrue(exception.getMessage().contains("字段定义不匹配"), exception.getMessage());
|
||||
verify(transportVehicleService, never()).updateById(any(TransportVehicle.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("通过:切换指针而不是追加,同对象仍只有一条生效快照")
|
||||
void approveReplacesPointerInsteadOfAppending() {
|
||||
@@ -252,16 +288,32 @@ class CertAuditServiceImplTest {
|
||||
// ------------------------------------------------------------ 辅助
|
||||
|
||||
private TransportCertRecord vehicleRecord(Integer status) {
|
||||
return vehicleRecord(status, completeVehicleSnapshot());
|
||||
}
|
||||
|
||||
private TransportCertRecord vehicleRecord(Integer status, String snapshotJson) {
|
||||
TransportCertRecord record = new TransportCertRecord();
|
||||
record.setId(RECORD_ID);
|
||||
record.setBizType(TransportCertRecord.BIZ_TYPE_VEHICLE);
|
||||
record.setBizId(VEHICLE_ID);
|
||||
record.setBizKey("桂A32523");
|
||||
record.setStatus(status);
|
||||
record.setSnapshotJson("{}");
|
||||
record.setSnapshotJson(snapshotJson);
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一份字段齐全的车辆认证快照
|
||||
* <p>
|
||||
* 审核通过时会把快照内容写回车辆档案,故用例不能再拿空快照走通过流程;
|
||||
* 关键字段(档案编号、核定载质量)是「这份快照是否为当前格式」的判据。
|
||||
*/
|
||||
static String completeVehicleSnapshot() {
|
||||
return "{\"drivingLicenseNo\":\"450100123456\",\"approvedLoadKg\":18000,"
|
||||
+ "\"ownerName\":\"桂物物流集团\",\"businessRelation\":\"管理挂靠\","
|
||||
+ "\"vehicleType\":\"重型货车\",\"driveLicenseImage\":\"http://oss/xsz.jpg\"}";
|
||||
}
|
||||
|
||||
private TransportCertRecord driverRecord(Integer status) {
|
||||
TransportCertRecord record = new TransportCertRecord();
|
||||
record.setId(RECORD_ID);
|
||||
|
||||
+75
-3
@@ -17,9 +17,11 @@ import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.ITransportCertRecordService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
@@ -410,9 +412,7 @@ class DriverCertServiceImplTest {
|
||||
when(transportVehicleService.list(any(LambdaQueryWrapper.class))).thenReturn(List.of());
|
||||
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
|
||||
|
||||
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
|
||||
request.setPlateNo("桂A·32523"); // 带间隔符,应被规范化
|
||||
request.setVehicleType("重型货车");
|
||||
VehicleCertSubmitRequest request = completeVehicleRequest("桂A·32523");
|
||||
|
||||
service.submitVehicleCert(request);
|
||||
|
||||
@@ -423,6 +423,14 @@ class DriverCertServiceImplTest {
|
||||
assertEquals("重型货车", vehicleCaptor.getValue().getVehicleType());
|
||||
assertNull(vehicleCaptor.getValue().getEffectiveCertRecordId(),
|
||||
"新建的车不得自带生效快照——是否已认证只能由审核通过决定");
|
||||
assertEquals("桂物物流集团", vehicleCaptor.getValue().getOrganizationName(),
|
||||
"所属组织必须从司机档案带出:该列 NOT NULL 且无默认值,空着这条 INSERT 直接报错");
|
||||
assertEquals("管理挂靠", vehicleCaptor.getValue().getBusinessRelation(),
|
||||
"业务关系由司机在表单里选,服务端照实落库、不替用户猜");
|
||||
assertNotNull(vehicleCaptor.getValue().getRegistrationNo(),
|
||||
"机动车登记编号该列 NOT NULL:司机端选填,缺值要写空串而不是 null");
|
||||
assertEquals("桂物物流集团", vehicleCaptor.getValue().getOwnerName(),
|
||||
"所有人是行驶证上的登记主体,与所属组织是两个字段");
|
||||
|
||||
// 绑定:司机的 driving_vehicle 被追加该车牌
|
||||
ArgumentCaptor<Driver> driverCaptor = ArgumentCaptor.forClass(Driver.class);
|
||||
@@ -480,12 +488,76 @@ class DriverCertServiceImplTest {
|
||||
|
||||
// ---------------------------------------------------------------- 辅助
|
||||
|
||||
@Test
|
||||
@DisplayName("司机建车:字段不齐时给可读提示,而不是让数据库报 NOT NULL")
|
||||
void submitVehicleRejectsIncompleteRequest() {
|
||||
Driver driver = driver(null);
|
||||
when(driverAppService.currentDriverEntity()).thenReturn(driver);
|
||||
when(transportVehicleService.list(any(LambdaQueryWrapper.class))).thenReturn(List.of());
|
||||
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
|
||||
|
||||
VehicleCertSubmitRequest request = completeVehicleRequest("桂A32523");
|
||||
request.setApprovedLoadKg(null);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.submitVehicleCert(request));
|
||||
assertEquals("核定载质量不能为空", exception.getMessage());
|
||||
verify(transportVehicleService, never()).save(any(TransportVehicle.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("司机建车:没有所属组织时明确拒绝,不落一辆查不到归属的车")
|
||||
void submitVehicleRejectsDriverWithoutOrganization() {
|
||||
Driver driver = driver(null);
|
||||
driver.setOrganizationName(null);
|
||||
when(driverAppService.currentDriverEntity()).thenReturn(driver);
|
||||
when(transportVehicleService.list(any(LambdaQueryWrapper.class))).thenReturn(List.of());
|
||||
when(certRecordService.selectPendingRecord(any(), anyLong())).thenReturn(null);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.submitVehicleCert(completeVehicleRequest("桂A32523")));
|
||||
assertTrue(exception.getMessage().contains("所属组织"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 一份字段齐全的车辆认证提交内容
|
||||
* <p>
|
||||
* 车辆表有十个 NOT NULL 且无默认值的列,少一个就会触发数据库错误而不是业务提示,
|
||||
* 故用例统一从这里取请求,避免每个用例各自拼一半字段。
|
||||
*/
|
||||
private VehicleCertSubmitRequest completeVehicleRequest(String plateNo) {
|
||||
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
|
||||
request.setPlateNo(plateNo);
|
||||
request.setVehicleType("重型货车");
|
||||
request.setOwnerName("桂物物流集团");
|
||||
request.setPlateColor("黄牌");
|
||||
request.setEnergyType("柴油");
|
||||
request.setBusinessRelation("管理挂靠");
|
||||
request.setApprovedLoadKg(18000);
|
||||
request.setTractionMassKg(35000);
|
||||
request.setOuterLength(5990);
|
||||
request.setOuterWidth(2300);
|
||||
request.setOuterHeight(3100);
|
||||
request.setDrivingLicenseNo("450100123456");
|
||||
request.setDrivingLicenseImage("http://oss.example.com/xsz-front.jpg");
|
||||
request.setDrivingLicenseStartDate(LocalDate.of(2018, 3, 9));
|
||||
request.setDrivingLicenseEndDate(LocalDate.of(2028, 3, 9));
|
||||
request.setRoadTransportCertNo("桂交运管0001");
|
||||
request.setRoadTransportCertStartDate(LocalDate.of(2020, 1, 1));
|
||||
request.setRoadTransportCertEndDate(LocalDate.of(2030, 1, 1));
|
||||
request.setCompulsoryScrapDate(LocalDate.of(2035, 1, 1));
|
||||
return request;
|
||||
}
|
||||
|
||||
private Driver driver(String drivingVehicle) {
|
||||
Driver driver = new Driver();
|
||||
driver.setId(DRIVER_ID);
|
||||
// 司机档案ID与用户ID是两个不同的ID:提交人字段要的是后者
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDriverName("张三");
|
||||
// 车辆表的 organization_name 是 NOT NULL 且无默认值:司机缺组织时建车必然失败,
|
||||
// 故建车链路要求它非空,fixture 也必须给
|
||||
driver.setOrganizationName("桂物物流集团");
|
||||
driver.setDrivingVehicle(drivingVehicle);
|
||||
return driver;
|
||||
}
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package org.springblade.transport.support;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 百度 OCR 结果归一化测试。
|
||||
* <p>
|
||||
* 这一层挡的是「识别成功但什么都没填」——百度返回的是中文键,写死键名的代码在键名
|
||||
* 对不上的那天会静默失效:不抛异常、不报错,用户只看到识别完表单还是空的。
|
||||
* 故这里用真实形状的返回把别名容错与值归一化都钉住。
|
||||
*/
|
||||
class OcrFieldSupportTest {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@DisplayName("行驶证主页:官方字段名逐个归一化,外廓尺寸拆成三个")
|
||||
void vehicleLicenseFront() throws Exception {
|
||||
JsonNode result = MAPPER.readTree("""
|
||||
{
|
||||
"words_result_num": 12,
|
||||
"words_result": {
|
||||
"号牌号码": {"words": "桂A32523"},
|
||||
"车辆类型": {"words": "重型厢式货车"},
|
||||
"所有人": {"words": "桂物物流集团"},
|
||||
"车辆识别代码": {"words": "LGAX2A130J1001234"},
|
||||
"档案编号": {"words": "450100123456"},
|
||||
"核定载质量": {"words": "18000kg"},
|
||||
"准牵引总质量": {"words": "35000"},
|
||||
"外廓尺寸": {"words": "5990×2300×3100"},
|
||||
"强制报废期止": {"words": "2030-05-20"},
|
||||
"能源种类": {"words": "柴油"}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
Map<String, Object> fields = OcrFieldSupport.extract(BaiduOcrType.VEHICLE_LICENSE, "front", result);
|
||||
|
||||
assertEquals("桂A32523", fields.get("plateNo"));
|
||||
assertEquals("重型厢式货车", fields.get("vehicleType"));
|
||||
assertEquals("桂物物流集团", fields.get("ownerName"));
|
||||
assertEquals("LGAX2A130J1001234", fields.get("vin"));
|
||||
assertEquals("450100123456", fields.get("drivingLicenseNo"));
|
||||
assertEquals(18000, fields.get("approvedLoadKg"), "带 kg 单位也要能取到数字");
|
||||
assertEquals(35000, fields.get("tractionMassKg"));
|
||||
assertEquals(5990, fields.get("outerLength"));
|
||||
assertEquals(2300, fields.get("outerWidth"));
|
||||
assertEquals(3100, fields.get("outerHeight"));
|
||||
assertEquals("2030-05-20", fields.get("compulsoryScrapDate"));
|
||||
assertEquals("柴油", fields.get("energyType"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("键名写法差异:车牌号码 / 车辆识别代号 / 带单位后缀的键都要认")
|
||||
void aliasTolerance() throws Exception {
|
||||
JsonNode result = MAPPER.readTree("""
|
||||
{
|
||||
"words_result": {
|
||||
"车牌号码": {"words": "桂B11111"},
|
||||
"车辆识别代号": {"words": "LVSHCAMB7HF123456"},
|
||||
"核定载质量(kg)": {"words": "12000"}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
Map<String, Object> fields = OcrFieldSupport.extract(BaiduOcrType.VEHICLE_LICENSE, "front", result);
|
||||
|
||||
assertEquals("桂B11111", fields.get("plateNo"));
|
||||
assertEquals("LVSHCAMB7HF123456", fields.get("vin"));
|
||||
assertEquals(12000, fields.get("approvedLoadKg"), "带后缀的键名要靠「包含」兜底命中");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("日期写法:年月日 / 点分 / 纯数字都归一化成 yyyy-MM-dd")
|
||||
void dateNormalization() throws Exception {
|
||||
JsonNode result = MAPPER.readTree("""
|
||||
{
|
||||
"words_result": {
|
||||
"强制报废期止": {"words": "2030年05月20日"},
|
||||
"注册日期": {"words": "2018.3.9"}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
Map<String, Object> fields = OcrFieldSupport.extract(BaiduOcrType.VEHICLE_LICENSE, "front", result);
|
||||
|
||||
assertEquals("2030-05-20", fields.get("compulsoryScrapDate"));
|
||||
assertEquals("2018-03-09", fields.get("drivingLicenseStartDate"), "月日个位数要补零");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("「长期」不产出日期,而是把对应的长期有效标志置 1")
|
||||
void longTermBecomesFlag() throws Exception {
|
||||
JsonNode result = MAPPER.readTree("""
|
||||
{
|
||||
"words_result": {
|
||||
"强制报废期止": {"words": "长期"},
|
||||
"检验有效期止": {"words": "长期有效"}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
Map<String, Object> fields = OcrFieldSupport.extract(BaiduOcrType.VEHICLE_LICENSE, "front", result);
|
||||
|
||||
assertFalse(fields.containsKey("compulsoryScrapDate"), "长期不能当成日期塞进日期框");
|
||||
assertEquals(1, fields.get("compulsoryScrapLongTerm"));
|
||||
assertEquals(1, fields.get("drivingLicenseLongTerm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("身份证与驾驶证:键名归一化到个人认证的字段名")
|
||||
void idCardAndDrivingLicense() throws Exception {
|
||||
JsonNode idCard = MAPPER.readTree("""
|
||||
{
|
||||
"words_result": {
|
||||
"姓名": {"words": "黄宇富"},
|
||||
"公民身份号码": {"words": "452730197911172318"},
|
||||
"签发日期": {"words": "2015-03-12"},
|
||||
"失效日期": {"words": "2035-03-12"}
|
||||
}
|
||||
}
|
||||
""");
|
||||
Map<String, Object> person = OcrFieldSupport.extract(BaiduOcrType.ID_CARD, "front", idCard);
|
||||
assertEquals("黄宇富", person.get("realName"));
|
||||
assertEquals("452730197911172318", person.get("idCardNo"));
|
||||
assertEquals("2015-03-12", person.get("idCardValidStart"));
|
||||
assertEquals("2035-03-12", person.get("idCardValidEnd"));
|
||||
|
||||
JsonNode driving = MAPPER.readTree("""
|
||||
{
|
||||
"words_result": {
|
||||
"档案编号": {"words": "451200976793"},
|
||||
"准驾车型": {"words": "B2D"},
|
||||
"有效起始日期": {"words": "2016-08-01"},
|
||||
"有效期限": {"words": "2026-08-01"}
|
||||
}
|
||||
}
|
||||
""");
|
||||
Map<String, Object> license = OcrFieldSupport.extract(BaiduOcrType.DRIVING_LICENSE, "front", driving);
|
||||
assertEquals("451200976793", license.get("driverFileNo"));
|
||||
assertEquals("B2D", license.get("driverClass"));
|
||||
assertEquals("2016-08-01", license.get("driverValidStart"));
|
||||
assertEquals("2026-08-01", license.get("driverValidEnd"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("道路运输证:证号与业户名称")
|
||||
void roadTransport() throws Exception {
|
||||
JsonNode result = MAPPER.readTree("""
|
||||
{
|
||||
"words_result": {
|
||||
"道路运输证号": {"words": "桂交运管桂字450100123456号"},
|
||||
"业户名称": {"words": "桂物物流集团"},
|
||||
"有效期": {"words": "2020-01-01 至 2030-01-01"}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
Map<String, Object> fields = OcrFieldSupport.extract(BaiduOcrType.ROAD_TRANSPORT_CERTIFICATE, null, result);
|
||||
|
||||
assertEquals("桂交运管桂字450100123456号", fields.get("roadTransportCertNo"));
|
||||
assertEquals("桂物物流集团", fields.get("ownerName"));
|
||||
assertEquals("2030-01-01", fields.get("roadTransportCertEndDate"), "止期字段取区间的最后一段,取第一段会把有效期起填成止");
|
||||
assertEquals("2020-01-01", fields.get("roadTransportCertStartDate"), "区间要同时带出起止,否则用户得照图再敲一遍");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("识别不到任何字段时返回空 Map,而不是一堆空串")
|
||||
void emptyResult() throws Exception {
|
||||
JsonNode result = MAPPER.readTree("""
|
||||
{"words_result_num": 0, "words_result": {}}
|
||||
""");
|
||||
|
||||
Map<String, Object> fields = OcrFieldSupport.extract(BaiduOcrType.VEHICLE_LICENSE, "front", result);
|
||||
|
||||
assertTrue(fields.isEmpty(), "空结果必须是空 Map:给空串会把用户已填内容覆盖掉");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("数值荒谬时不填:位数过多的串不可能是载质量,宁可留空也不填错")
|
||||
void absurdNumberIsSkipped() throws Exception {
|
||||
JsonNode result = MAPPER.readTree("""
|
||||
{
|
||||
"words_result": {
|
||||
"核定载质量": {"words": "识别串行123456789012"}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
Map<String, Object> fields = OcrFieldSupport.extract(BaiduOcrType.VEHICLE_LICENSE, "front", result);
|
||||
|
||||
assertFalse(fields.containsKey("approvedLoadKg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("外廓尺寸只识别到两段时不写回:硬填会让宽高错位")
|
||||
void outerSizeNeedsThreeParts() throws Exception {
|
||||
JsonNode result = MAPPER.readTree("""
|
||||
{
|
||||
"words_result": {
|
||||
"外廓尺寸": {"words": "5990×2300"}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
Map<String, Object> fields = OcrFieldSupport.extract(BaiduOcrType.VEHICLE_LICENSE, "front", result);
|
||||
|
||||
assertFalse(fields.containsKey("outerLength"));
|
||||
assertFalse(fields.containsKey("outerWidth"));
|
||||
assertFalse(fields.containsKey("outerHeight"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,8 +8,10 @@ CREATE TABLE `blade_transport_vehicle` (
|
||||
`organization_name` varchar(50) NOT NULL COMMENT '所属组织',
|
||||
`use_department` varchar(50) DEFAULT NULL COMMENT '使用部门',
|
||||
`plate_no` varchar(16) NOT NULL COMMENT '车牌号',
|
||||
`owner_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '所有人(行驶证登记的单位名称,不是司机)',
|
||||
`plate_color` varchar(20) DEFAULT NULL COMMENT '车牌颜色',
|
||||
`vehicle_type` varchar(50) NOT NULL COMMENT '车辆类型',
|
||||
`vin` varchar(17) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '车辆识别代号(车架号)',
|
||||
`outer_length` int(11) DEFAULT NULL COMMENT '外廓长度,毫米',
|
||||
`outer_width` int(11) DEFAULT NULL COMMENT '外廓宽度,毫米',
|
||||
`outer_height` int(11) DEFAULT NULL COMMENT '外廓高度,毫米',
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- ----------------------------
|
||||
-- 车辆档案补两列:所有人、车辆识别代号(车架号)
|
||||
-- ----------------------------
|
||||
-- 所有人:行驶证上登记的单位名称(如「桂物物流集团」),**不是司机**,
|
||||
-- 与「所属组织」也是两个字段——组织是内部管理归属,所有人是证件上的登记主体,
|
||||
-- 两者可能不同(挂靠车的所有人不是本组织)。
|
||||
-- VIN:司机端车辆认证一直在收这个值并写进认证快照,但车辆表此前没有对应列,
|
||||
-- 于是它只存在于快照里、车辆档案上查不到。补列后档案与快照才一致。
|
||||
--
|
||||
-- 两列都允许为空:存量车辆(含订正补的那些)没有这两个值,
|
||||
-- 且 VIN 在原型里是**选填**项,不设默认值也不设 NOT NULL。
|
||||
ALTER TABLE `blade_transport_vehicle`
|
||||
ADD COLUMN `owner_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '所有人(行驶证登记的单位名称,不是司机)' AFTER `plate_no`,
|
||||
ADD COLUMN `vin` varchar(17) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '车辆识别代号(车架号)' AFTER `vehicle_type`;
|
||||
Reference in New Issue
Block a user