✨ 小程序证件 OCR 薄适配层,含类型映射
小程序按既有契约 POST {imageUrl, type, side?} 到 /driver/auth/ocr,
后端转调已有的百度 OCR 能力(recognizeUrl)。
映射的必要性在于两侧命名对「行驶证」和「驾驶证」的用词正好相反:
驾驶证 —— 小程序传 driver,后端 OCR 用 driving_license
行驶证 —— 小程序传 vehicle,后端 OCR 用 vehicle_license
把入参直接透传下游,调用方不会收到任何错误,只会拿到另一种证件的字段,
问题要到人工核对时才发现。故用显式白名单逐一对应。
采用白名单而非「透传 + 兜底为通用识别」,是因为从业资格证没有对应的百度接口:
静默降级成通用文字识别会让前端以为识别成功,实际拿到一堆通用文本。
该类型明确报错并提示手动填写。
正反面:小程序目前不传 side,按 front 处理(正页是常用场景)。
10 个映射用例覆盖五个小程序类型、后端既有命名、大小写/分隔符变体、
未知类型、空值、不支持类型与 side 归一化。
全模块 58 个测试通过:mvn -o test -pl blade-service/blade-transport -am
This commit is contained in:
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 证件 OCR 识别请求(小程序)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "证件OCR识别请求")
|
||||
public class OcrRecognizeRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "已上传图片的地址,必须是百度可访问的HTTP或HTTPS地址")
|
||||
private String imageUrl;
|
||||
@Schema(description = "证件类型:idcard-身份证,driver-驾驶证,vehicle-行驶证,road_transport-道路运输证")
|
||||
private String type;
|
||||
@Schema(description = "正副面:front或back,默认front")
|
||||
private String side;
|
||||
|
||||
}
|
||||
+20
@@ -31,10 +31,15 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||
import org.springblade.transport.ocr.service.IBaiduOcrService;
|
||||
import org.springblade.transport.pojo.dto.OcrRecognizeRequest;
|
||||
import org.springblade.transport.pojo.dto.PersonCertSubmitRequest;
|
||||
import org.springblade.transport.pojo.dto.VehicleCertSubmitRequest;
|
||||
import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
|
||||
import org.springblade.transport.pojo.vo.TransportCertRecordVO;
|
||||
import org.springblade.transport.service.IDriverCertService;
|
||||
import org.springblade.transport.support.OcrTypeMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
@@ -61,6 +66,7 @@ import java.util.List;
|
||||
public class DriverAuthController extends BladeController {
|
||||
|
||||
private final IDriverCertService driverCertService;
|
||||
private final IBaiduOcrService baiduOcrService;
|
||||
|
||||
// ------------------------------------------------------------ 个人认证
|
||||
|
||||
@@ -118,4 +124,18 @@ public class DriverAuthController extends BladeController {
|
||||
return R.data(driverCertService.getRecord(id));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ OCR
|
||||
|
||||
@PostMapping("/ocr")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "证件OCR识别",
|
||||
description = "按小程序既有契约收 {imageUrl, type, side?},转调百度OCR。"
|
||||
+ "type 用小程序命名:idcard-身份证,driver-驾驶证,vehicle-行驶证,road_transport-道路运输证")
|
||||
public R<BaiduOcrResultVO> ocr(@RequestBody OcrRecognizeRequest request) {
|
||||
// 两侧对「驾驶证 / 行驶证」的用词正好相反,故必须经显式映射,不能把入参直接透传下游
|
||||
BaiduOcrType ocrType = OcrTypeMapping.toBaiduType(request.getType());
|
||||
String side = OcrTypeMapping.normalizeSide(request.getSide());
|
||||
return R.data(baiduOcrService.recognizeUrl(ocrType, side, request.getImageUrl()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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 org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 证件类型映射:小程序命名 → 百度 OCR 证件类型
|
||||
* <p>
|
||||
* ⚠️ 两侧命名对「行驶证」和「驾驶证」的用词正好相反,是本类存在的主要理由:
|
||||
* <table border="1">
|
||||
* <caption>命名对照</caption>
|
||||
* <tr><th>证件</th><th>小程序传</th><th>后端 OCR 用</th></tr>
|
||||
* <tr><td>驾驶证</td><td>{@code driver}</td><td>{@code driving_license}</td></tr>
|
||||
* <tr><td>行驶证</td><td>{@code vehicle}</td><td>{@code vehicle_license}</td></tr>
|
||||
* </table>
|
||||
* 若把 {@code vehicle} 直接透传给百度按 {@code driving_license} 处理,或反之,
|
||||
* 调用方不会收到任何错误——拿到的是另一种证件的字段,问题只在人工核对时才发现。
|
||||
* 故此处用显式白名单逐一对应,而不是把请求参数直接丢给下游。
|
||||
* <p>
|
||||
* 采用白名单而非「透传 + 兜底为通用识别」还有一个原因:
|
||||
* 从业资格证没有对应的百度接口,静默降级成通用文字识别会让前端以为识别成功。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class OcrTypeMapping {
|
||||
|
||||
/**
|
||||
* 允许的证件类型:小程序命名与后端既有命名都收,其余一律拒绝
|
||||
*/
|
||||
private static final Map<String, BaiduOcrType> TYPE_ALIASES = Map.ofEntries(
|
||||
// 身份证
|
||||
Map.entry("IDCARD", BaiduOcrType.ID_CARD),
|
||||
Map.entry("ID_CARD", BaiduOcrType.ID_CARD),
|
||||
// 驾驶证:小程序叫 driver
|
||||
Map.entry("DRIVER", BaiduOcrType.DRIVING_LICENSE),
|
||||
Map.entry("DRIVING_LICENSE", BaiduOcrType.DRIVING_LICENSE),
|
||||
// 行驶证:小程序叫 vehicle
|
||||
Map.entry("VEHICLE", BaiduOcrType.VEHICLE_LICENSE),
|
||||
Map.entry("VEHICLE_LICENSE", BaiduOcrType.VEHICLE_LICENSE),
|
||||
// 道路运输证
|
||||
Map.entry("ROAD_TRANSPORT", BaiduOcrType.ROAD_TRANSPORT_CERTIFICATE),
|
||||
Map.entry("ROAD_TRANSPORT_CERTIFICATE", BaiduOcrType.ROAD_TRANSPORT_CERTIFICATE)
|
||||
);
|
||||
|
||||
/**
|
||||
* 明确知道「没有对应 OCR 能力」的类型,单独给出可读提示
|
||||
*/
|
||||
private static final Map<String, String> KNOWN_UNSUPPORTED = Map.of(
|
||||
"QUALIFICATION", "从业资格证"
|
||||
);
|
||||
|
||||
private static final String SIDE_FRONT = "front";
|
||||
private static final String SIDE_BACK = "back";
|
||||
|
||||
private OcrTypeMapping() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 把调用方传来的证件类型转成百度 OCR 证件类型
|
||||
*
|
||||
* @param type 证件类型:小程序传 idcard / driver / vehicle / road_transport / qualification,
|
||||
* 也接受后端既有命名 id_card / driving_license / vehicle_license / road_transport_certificate
|
||||
* @return 百度 OCR 证件类型
|
||||
* @throws IllegalArgumentException 类型为空、未知,或该证件不支持 OCR
|
||||
*/
|
||||
public static BaiduOcrType toBaiduType(String type) {
|
||||
String normalized = normalize(type);
|
||||
if (normalized.isEmpty()) {
|
||||
throw new IllegalArgumentException("OCR证件类型不能为空");
|
||||
}
|
||||
BaiduOcrType baiduOcrType = TYPE_ALIASES.get(normalized);
|
||||
if (baiduOcrType != null) {
|
||||
return baiduOcrType;
|
||||
}
|
||||
String unsupportedName = KNOWN_UNSUPPORTED.get(normalized);
|
||||
if (unsupportedName != null) {
|
||||
// 不降级为通用识别:否则前端会拿到一堆通用文本却以为识别成功
|
||||
throw new IllegalArgumentException(unsupportedName + "暂不支持自动识别,请手动填写");
|
||||
}
|
||||
throw new IllegalArgumentException("不支持的OCR证件类型:" + type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化正反面参数
|
||||
* <p>
|
||||
* 小程序目前不传该参数,按正面处理(身份证、行驶证、驾驶证的正页是常用场景)。
|
||||
*
|
||||
* @param side 正反面:front / back
|
||||
* @return 规范化结果,恒为 front 或 back
|
||||
*/
|
||||
public static String normalizeSide(String side) {
|
||||
// 归一化结果是大写,故此处比对 "BACK"
|
||||
return "BACK".equals(normalize(side)) ? SIDE_BACK : SIDE_FRONT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化:去首尾空白、去分隔符差异、统一大写
|
||||
*
|
||||
* @param value 原始值
|
||||
* @return 归一化结果;空值返回空串
|
||||
*/
|
||||
private static String normalize(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return value.trim()
|
||||
.replace('-', '_')
|
||||
.replace(' ', '_')
|
||||
.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package org.springblade.transport.support;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 小程序证件类型 → 百度 OCR 证件类型的映射测试。
|
||||
* <p>
|
||||
* 两侧用的是两套命名,且**都不太直观**:
|
||||
* 小程序把「行驶证」叫 {@code vehicle}、把「驾驶证」叫 {@code driver}
|
||||
* (按业务直觉命名),而后端 OCR 沿用百度接口的 {@code vehicle_license}
|
||||
* (行驶证)与 {@code driving_license}(驾驶证)。
|
||||
* 这两个词一一对应关系相反,映射写错会让「行驶证」被当成「驾驶证」识别,
|
||||
* 结果是司机传了行驶证却拿到一堆驾驶证字段——而且不会报错。
|
||||
* 故此处逐一锁住。
|
||||
*/
|
||||
class OcrTypeMappingTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("身份证:小程序 idcard → 后端 ID_CARD")
|
||||
void idCard() {
|
||||
assertEquals(BaiduOcrType.ID_CARD, OcrTypeMapping.toBaiduType("idcard"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("驾驶证:小程序 driver → 后端 DRIVING_LICENSE(不是 vehicle_license)")
|
||||
void drivingLicense() {
|
||||
assertEquals(BaiduOcrType.DRIVING_LICENSE, OcrTypeMapping.toBaiduType("driver"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("行驶证:小程序 vehicle → 后端 VEHICLE_LICENSE(不是 driving_license)")
|
||||
void vehicleLicense() {
|
||||
assertEquals(BaiduOcrType.VEHICLE_LICENSE, OcrTypeMapping.toBaiduType("vehicle"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("道路运输证:小程序 road_transport → 后端 ROAD_TRANSPORT_CERTIFICATE")
|
||||
void roadTransport() {
|
||||
assertEquals(BaiduOcrType.ROAD_TRANSPORT_CERTIFICATE,
|
||||
OcrTypeMapping.toBaiduType("road_transport"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("从业资格证:后端无对应 OCR 能力,必须明确报错而不是退化成通用识别")
|
||||
void qualificationIsNotSilentlyDowngraded() {
|
||||
// 百度没有「从业资格证」专用接口。若这里悄悄退化成 GENERAL,
|
||||
// 前端会拿到一堆通用文本却以为识别成功了,故要求明确失败。
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||
() -> OcrTypeMapping.toBaiduType("qualification"));
|
||||
assertTrue(ex.getMessage().contains("从业资格证"),
|
||||
"错误信息要指明是哪个证件类型不支持,便于前端提示用户手动填写");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("后端既有命名也接受(前端或其它调用方可能直接传后端名)")
|
||||
void acceptsBackendNames() {
|
||||
assertEquals(BaiduOcrType.ID_CARD, OcrTypeMapping.toBaiduType("id_card"));
|
||||
assertEquals(BaiduOcrType.DRIVING_LICENSE, OcrTypeMapping.toBaiduType("driving_license"));
|
||||
assertEquals(BaiduOcrType.VEHICLE_LICENSE, OcrTypeMapping.toBaiduType("vehicle_license"));
|
||||
assertEquals(BaiduOcrType.ROAD_TRANSPORT_CERTIFICATE,
|
||||
OcrTypeMapping.toBaiduType("road_transport_certificate"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("大小写与分隔符不敏感")
|
||||
void caseAndSeparatorInsensitive() {
|
||||
assertEquals(BaiduOcrType.ID_CARD, OcrTypeMapping.toBaiduType("IDCard"));
|
||||
assertEquals(BaiduOcrType.ID_CARD, OcrTypeMapping.toBaiduType(" ID_CARD "));
|
||||
assertEquals(BaiduOcrType.ID_CARD, OcrTypeMapping.toBaiduType("id-card"));
|
||||
assertEquals(BaiduOcrType.ROAD_TRANSPORT_CERTIFICATE,
|
||||
OcrTypeMapping.toBaiduType("Road-Transport"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未知类型:明确报错,不静默降级为通用识别")
|
||||
void unknownTypeFails() {
|
||||
assertThrows(IllegalArgumentException.class, () -> OcrTypeMapping.toBaiduType("truck"));
|
||||
assertThrows(IllegalArgumentException.class, () -> OcrTypeMapping.toBaiduType("general"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空值:明确报错")
|
||||
void emptyTypeFails() {
|
||||
assertThrows(IllegalArgumentException.class, () -> OcrTypeMapping.toBaiduType(null));
|
||||
assertThrows(IllegalArgumentException.class, () -> OcrTypeMapping.toBaiduType(""));
|
||||
assertThrows(IllegalArgumentException.class, () -> OcrTypeMapping.toBaiduType(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("正反面:小程序不传 side 时按正面处理")
|
||||
void defaultSide() {
|
||||
assertEquals("front", OcrTypeMapping.normalizeSide(null));
|
||||
assertEquals("front", OcrTypeMapping.normalizeSide(""));
|
||||
assertEquals("front", OcrTypeMapping.normalizeSide("front"));
|
||||
assertEquals("back", OcrTypeMapping.normalizeSide("back"));
|
||||
assertEquals("back", OcrTypeMapping.normalizeSide("BACK"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user