提交修改
This commit is contained in:
parent
b6e2a376e6
commit
4dd5a1cb18
@ -0,0 +1,195 @@
|
|||||||
|
package com.heyu.api.data.utils;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 影像入参解析工具:自动识别 Base64 与 HTTP(S) 图片链接,并输出可供表单提交使用的编码值。
|
||||||
|
*/
|
||||||
|
public final class ImageInputUtils {
|
||||||
|
|
||||||
|
private static final Pattern PERCENT_ENCODED = Pattern.compile("%[0-9A-Fa-f]{2}");
|
||||||
|
|
||||||
|
private ImageInputUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 影像入参类型
|
||||||
|
*/
|
||||||
|
public enum ImageInputType {
|
||||||
|
/** Base64 编码图片(可带 data:image 前缀) */
|
||||||
|
BASE64("使用 Base64 编码图片"),
|
||||||
|
/** HTTP/HTTPS 图片链接 */
|
||||||
|
URL("使用图片链接");
|
||||||
|
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
ImageInputType(String desc) {
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDesc() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析后的影像入参
|
||||||
|
*/
|
||||||
|
public static final class ResolvedImageInput {
|
||||||
|
private final ImageInputType type;
|
||||||
|
/** 去除 data: 前缀、URL 解码后的原始值(日志用,不含表单编码) */
|
||||||
|
private final String rawValue;
|
||||||
|
/** 可直接拼接到 application/x-www-form-urlencoded 请求体的值 */
|
||||||
|
private final String formValue;
|
||||||
|
|
||||||
|
ResolvedImageInput(ImageInputType type, String rawValue, String formValue) {
|
||||||
|
this.type = type;
|
||||||
|
this.rawValue = rawValue;
|
||||||
|
this.formValue = formValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageInputType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRawValue() {
|
||||||
|
return rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFormValue() {
|
||||||
|
return formValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断字符串是 Base64 影像还是 HTTP(S) 图片链接。
|
||||||
|
* <p>规则:以 {@code http://} 或 {@code https://} 开头(含已 urlencode 的形式)视为链接,否则视为 Base64。</p>
|
||||||
|
*
|
||||||
|
* @param input 用户传入的影像字符串
|
||||||
|
* @return 入参类型;空串返回 {@code null}
|
||||||
|
*/
|
||||||
|
public static ImageInputType detectType(String input) {
|
||||||
|
if (StringUtils.isBlank(input)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String trimmed = input.trim();
|
||||||
|
if (trimmed.regionMatches(true, 0, "data:", 0, 5)) {
|
||||||
|
return ImageInputType.BASE64;
|
||||||
|
}
|
||||||
|
if (isHttpUrl(trimmed) || isHttpUrl(tryUrlDecode(trimmed))) {
|
||||||
|
return ImageInputType.URL;
|
||||||
|
}
|
||||||
|
return ImageInputType.BASE64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析影像入参:识别类型、剥离 data: 前缀,并在未 urlencode 时自动编码为表单可用值。
|
||||||
|
*
|
||||||
|
* @param input 用户传入的影像字符串
|
||||||
|
* @return 解析结果;入参为空时返回 {@code null}
|
||||||
|
*/
|
||||||
|
public static ResolvedImageInput resolve(String input) {
|
||||||
|
if (StringUtils.isBlank(input)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String trimmed = input.trim();
|
||||||
|
ImageInputType type = detectType(trimmed);
|
||||||
|
if (type == ImageInputType.URL) {
|
||||||
|
String rawUrl = normalizeHttpUrl(trimmed);
|
||||||
|
return new ResolvedImageInput(type, rawUrl, encodeForFormBody(trimmed, rawUrl));
|
||||||
|
}
|
||||||
|
String rawBase64 = stripDataUriPrefix(trimmed);
|
||||||
|
return new ResolvedImageInput(type, rawBase64, encodeForFormBody(trimmed, rawBase64));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeHttpUrl(String input) {
|
||||||
|
if (isHttpUrl(input)) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
String decoded = tryUrlDecode(input);
|
||||||
|
return isHttpUrl(decoded) ? decoded : input;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isHttpUrl(String value) {
|
||||||
|
if (StringUtils.isBlank(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return value.regionMatches(true, 0, "http://", 0, 7)
|
||||||
|
|| value.regionMatches(true, 0, "https://", 0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stripDataUriPrefix(String input) {
|
||||||
|
if (!input.regionMatches(true, 0, "data:", 0, 5)) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
int base64Idx = input.indexOf("base64,");
|
||||||
|
if (base64Idx >= 0) {
|
||||||
|
return input.substring(base64Idx + "base64,".length());
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 若入参尚未做表单 urlencode,则进行编码;已编码则原样返回。
|
||||||
|
*/
|
||||||
|
private static String encodeForFormBody(String original, String rawValue) {
|
||||||
|
if (appearsFormUrlEncoded(original, rawValue)) {
|
||||||
|
return original.trim();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return URLEncoder.encode(rawValue, StandardCharsets.UTF_8.name());
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
return rawValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否已做过表单 urlencode(避免二次编码)。
|
||||||
|
*/
|
||||||
|
private static boolean appearsFormUrlEncoded(String original, String rawValue) {
|
||||||
|
String trimmed = original.trim();
|
||||||
|
if (!PERCENT_ENCODED.matcher(trimmed).find()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (trimmed.equals(rawValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String decoded = URLDecoder.decode(trimmed, StandardCharsets.UTF_8.name());
|
||||||
|
return decoded.equals(rawValue);
|
||||||
|
} catch (IllegalArgumentException | UnsupportedEncodingException ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String tryUrlDecode(String value) {
|
||||||
|
if (StringUtils.isBlank(value) || !PERCENT_ENCODED.matcher(value).find()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
|
||||||
|
} catch (IllegalArgumentException | UnsupportedEncodingException ex) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 粗略校验 Base64 字符串是否可解码(仅用于可选校验,非强制)。
|
||||||
|
*/
|
||||||
|
public static boolean isValidBase64(String base64) {
|
||||||
|
if (StringUtils.isBlank(base64)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Base64.getDecoder().decode(base64);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@ package com.heyu.api.controller.car;
|
|||||||
import com.heyu.api.controller.BaseController;
|
import com.heyu.api.controller.BaseController;
|
||||||
import com.heyu.api.data.annotation.EbAuthentication;
|
import com.heyu.api.data.annotation.EbAuthentication;
|
||||||
import com.heyu.api.data.constants.ApiConstants;
|
import com.heyu.api.data.constants.ApiConstants;
|
||||||
|
import com.heyu.api.data.utils.ImageInputUtils;
|
||||||
|
import com.heyu.api.data.utils.ImageInputUtils.ResolvedImageInput;
|
||||||
import com.heyu.api.data.utils.MapUtils;
|
import com.heyu.api.data.utils.MapUtils;
|
||||||
import com.heyu.api.data.utils.R;
|
import com.heyu.api.data.utils.R;
|
||||||
import com.heyu.api.data.utils.StringUtils;
|
import com.heyu.api.data.utils.StringUtils;
|
||||||
@ -81,27 +83,29 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
String validateError = validateRequest(request);
|
String validateError = validateRequest(request);
|
||||||
if (validateError != null) {
|
if (validateError != null) {
|
||||||
log.info("驾驶证识别:参数检查没通过,直接返回错误(还没调识别、不扣费)。{} 返回给客户:{}",
|
log.info("驾驶证识别:参数检查没通过,直接返回错误(还没调识别、不扣费)。{} 返回给客户:{}",
|
||||||
buildInputLogContext(request), abbreviate(validateError, 120));
|
buildInputLogContext(request, ImageInputUtils.resolve(request.getImageUrlOrBase64())),
|
||||||
|
abbreviate(validateError, 120));
|
||||||
return R.error(validateError);
|
return R.error(validateError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 步骤②:构建上下文(side、图片模式、入参摘要) ----------
|
// ---------- 步骤②:解析影像入参 & 构建上下文 ----------
|
||||||
|
ResolvedImageInput imageInput = ImageInputUtils.resolve(request.getImageUrlOrBase64());
|
||||||
ctx = new RecognizeContext(
|
ctx = new RecognizeContext(
|
||||||
resolveDrivingLicenseSide(request),
|
resolveDrivingLicenseSide(request),
|
||||||
ImageMode.of(request),
|
imageInput,
|
||||||
buildInputLogContext(request));
|
buildInputLogContext(request, imageInput));
|
||||||
|
|
||||||
// ---------- 步骤③:组装百度 API 请求体 ----------
|
// ---------- 步骤③:组装百度 API 请求体 ----------
|
||||||
String content = buildRequestContent(request, ctx.side);
|
String content = buildRequestContent(imageInput, ctx.side);
|
||||||
if (isBlank(content)) {
|
if (isBlank(content)) {
|
||||||
log.error("驾驶证识别:组装请求失败,请求里没带有效图片。识别{},{}。{}",
|
log.error("驾驶证识别:组装请求失败,请求里没带有效图片。识别{},{}。{}",
|
||||||
sideDesc(ctx.side), ctx.imageMode.desc, ctx.inputLog);
|
sideDesc(ctx.side), ctx.imageInput.getType().getDesc(), ctx.inputLog);
|
||||||
return okResult(ctx.side, formatHint(
|
return okResult(ctx.side, formatHint(
|
||||||
"报文组装异常",
|
"报文组装异常",
|
||||||
"识别请求体在序列化后未包含任何影像载荷,平台侧无法受理本次识别",
|
"识别请求体在序列化后未包含任何影像载荷,平台侧无法受理本次识别",
|
||||||
"请核对 imageBase64 是否已 urlencode、imageUrl 是否非空字符串;"
|
"请核对 imageUrlOrBase64 是否非空且为有效 Base64 或 HTTP(S) 链接;"
|
||||||
+ "请求头须为 application/x-www-form-urlencoded",
|
+ "请求头须为 application/x-www-form-urlencoded",
|
||||||
"目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageMode.name()
|
"目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageInput.getType().name()
|
||||||
), null);
|
), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +116,7 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
"服务无回执",
|
"服务无回执",
|
||||||
"识别指令已下发,但在约定时间内未收到平台可解析的 JSON 回执",
|
"识别指令已下发,但在约定时间内未收到平台可解析的 JSON 回执",
|
||||||
"建议间隔 3~5 秒重试;若连续失败,请记录 traceId、调用时刻与 side 并联系技术支持排查链路",
|
"建议间隔 3~5 秒重试;若连续失败,请记录 traceId、调用时刻与 side 并联系技术支持排查链路",
|
||||||
"目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageMode.name()
|
"目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageInput.getType().name()
|
||||||
), null);
|
), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,10 +134,12 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
long cost = System.currentTimeMillis() - start;
|
long cost = System.currentTimeMillis() - start;
|
||||||
String side = ctx != null ? ctx.side
|
String side = ctx != null ? ctx.side
|
||||||
: (request != null ? resolveDrivingLicenseSide(request) : ApiConstants.front);
|
: (request != null ? resolveDrivingLicenseSide(request) : ApiConstants.front);
|
||||||
String mode = ctx != null ? ctx.imageMode.desc
|
ResolvedImageInput fallbackImage = request != null
|
||||||
: (request != null ? ImageMode.of(request).desc : "未知");
|
? ImageInputUtils.resolve(request.getImageUrlOrBase64()) : null;
|
||||||
|
String mode = ctx != null ? ctx.imageInput.getType().getDesc()
|
||||||
|
: (fallbackImage != null ? fallbackImage.getType().getDesc() : "未知");
|
||||||
log.error("驾驶证识别:程序运行出错,耗时 {} ms。识别{},{}。客户传的:{}。异常:{} - {}",
|
log.error("驾驶证识别:程序运行出错,耗时 {} ms。识别{},{}。客户传的:{}。异常:{} - {}",
|
||||||
cost, sideDesc(side), mode, buildInputLogContext(request),
|
cost, sideDesc(side), mode, buildInputLogContext(request, fallbackImage),
|
||||||
e.getClass().getSimpleName(),
|
e.getClass().getSimpleName(),
|
||||||
e.getMessage() != null ? e.getMessage() : "无具体说明", e);
|
e.getMessage() != null ? e.getMessage() : "无具体说明", e);
|
||||||
return okResult(side, formatHint(
|
return okResult(side, formatHint(
|
||||||
@ -159,12 +165,12 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
private Map<String, Object> callPlatform(String content, RecognizeContext ctx) {
|
private Map<String, Object> callPlatform(String content, RecognizeContext ctx) {
|
||||||
int len = content.length();
|
int len = content.length();
|
||||||
log.info("驾驶证识别:开始调用平台识别。识别{},{},请求大小约 {} 字节。{}",
|
log.info("驾驶证识别:开始调用平台识别。识别{},{},请求大小约 {} 字节。{}",
|
||||||
sideDesc(ctx.side), ctx.imageMode.desc, len, ctx.inputLog);
|
sideDesc(ctx.side), ctx.imageInput.getType().getDesc(), len, ctx.inputLog);
|
||||||
Map<String, Object> result = requestBaidu(DRIVING_LICENSE_URI, content);
|
Map<String, Object> result = requestBaidu(DRIVING_LICENSE_URI, content);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
log.error("驾驶证识别:平台没有返回结果(可能网络超时、鉴权失败或响应无法解析)。"
|
log.error("驾驶证识别:平台没有返回结果(可能网络超时、鉴权失败或响应无法解析)。"
|
||||||
+ "识别{},{},请求约 {} 字节。{}",
|
+ "识别{},{},请求约 {} 字节。{}",
|
||||||
sideDesc(ctx.side), ctx.imageMode.desc, len, ctx.inputLog);
|
sideDesc(ctx.side), ctx.imageInput.getType().getDesc(), len, ctx.inputLog);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -193,7 +199,7 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
String category = PlatformError.categoryOf(errorCode);
|
String category = PlatformError.categoryOf(errorCode);
|
||||||
log.error("驾驶证识别:平台拒绝了本次识别。[{}] 错误码 {},原因:{}。识别{},{}。客户传的:{}。{}",
|
log.error("驾驶证识别:平台拒绝了本次识别。[{}] 错误码 {},原因:{}。识别{},{}。客户传的:{}。{}",
|
||||||
category, errorCode, errorMsg,
|
category, errorCode, errorMsg,
|
||||||
sideDesc(ctx.side), ctx.imageMode.desc,
|
sideDesc(ctx.side), ctx.imageInput.getType().getDesc(),
|
||||||
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
||||||
return formatHint(
|
return formatHint(
|
||||||
category,
|
category,
|
||||||
@ -204,7 +210,7 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
}
|
}
|
||||||
if (isWordsResultEmpty(platformResult)) {
|
if (isWordsResultEmpty(platformResult)) {
|
||||||
log.info("驾驶证识别:平台返回了,但没有识别结果数据(可能不是驾驶证或 side 传错)。识别{},{}。客户传的:{}。{}",
|
log.info("驾驶证识别:平台返回了,但没有识别结果数据(可能不是驾驶证或 side 传错)。识别{},{}。客户传的:{}。{}",
|
||||||
sideDesc(ctx.side), ctx.imageMode.desc,
|
sideDesc(ctx.side), ctx.imageInput.getType().getDesc(),
|
||||||
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
||||||
return formatHint(
|
return formatHint(
|
||||||
"结构化结果缺失",
|
"结构化结果缺失",
|
||||||
@ -217,7 +223,7 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
if (isAllStringFieldsBlank(data)) {
|
if (isAllStringFieldsBlank(data)) {
|
||||||
log.info("驾驶证识别:平台有返回,但证号、姓名等字段一个都没识别出来(可能传错正/副页或图片不清晰)。"
|
log.info("驾驶证识别:平台有返回,但证号、姓名等字段一个都没识别出来(可能传错正/副页或图片不清晰)。"
|
||||||
+ "识别{},{}。客户传的:{}。平台回执:{}",
|
+ "识别{},{}。客户传的:{}。平台回执:{}",
|
||||||
sideDesc(ctx.side), ctx.imageMode.desc,
|
sideDesc(ctx.side), ctx.imageInput.getType().getDesc(),
|
||||||
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
||||||
return formatHint(
|
return formatHint(
|
||||||
"字段映射为空",
|
"字段映射为空",
|
||||||
@ -248,11 +254,11 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
if (StringUtils.isNotBlank(hint)) {
|
if (StringUtils.isNotBlank(hint)) {
|
||||||
log.info("驾驶证识别:处理结束(接口仍返回成功,但带了提示信息)。耗时 {} ms,识别出 {} 个字段。"
|
log.info("驾驶证识别:处理结束(接口仍返回成功,但带了提示信息)。耗时 {} ms,识别出 {} 个字段。"
|
||||||
+ "识别{},{}。客户传的:{}。平台回执:{}。给客户的提示:{}",
|
+ "识别{},{}。客户传的:{}。平台回执:{}。给客户的提示:{}",
|
||||||
cost, mapped, sideDesc(ctx.side), ctx.imageMode.desc,
|
cost, mapped, sideDesc(ctx.side), ctx.imageInput.getType().getDesc(),
|
||||||
ctx.inputLog, buildPlatformReceiptSummary(platformResult), abbreviate(hint, 120));
|
ctx.inputLog, buildPlatformReceiptSummary(platformResult), abbreviate(hint, 120));
|
||||||
} else {
|
} else {
|
||||||
log.info("驾驶证识别:识别成功。耗时 {} ms,共识别出 {} 个字段。识别{},{}。客户传的:{}。平台回执:{}",
|
log.info("驾驶证识别:识别成功。耗时 {} ms,共识别出 {} 个字段。识别{},{}。客户传的:{}。平台回执:{}",
|
||||||
cost, mapped, sideDesc(ctx.side), ctx.imageMode.desc,
|
cost, mapped, sideDesc(ctx.side), ctx.imageInput.getType().getDesc(),
|
||||||
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -265,29 +271,35 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
return formatHint(
|
return formatHint(
|
||||||
"入参绑定失败",
|
"入参绑定失败",
|
||||||
"控制器未接收到可绑定的表单对象,所有业务字段均为空",
|
"控制器未接收到可绑定的表单对象,所有业务字段均为空",
|
||||||
"请确认使用 POST 提交;Content-Type 为 application/x-www-form-urlencoded;"
|
"请确认使用 POST 提交;Content-Type 为 application/json 或 application/x-www-form-urlencoded;"
|
||||||
+ "字段名与接口文档一致(imageBase64 / imageUrl / side)",
|
+ "字段名与接口文档一致(imageUrlOrBase64 / side)",
|
||||||
"绑定结果=DriverLicenseRecognizeRequest 为 null"
|
"绑定结果=DriverLicenseRecognizeRequest 为 null"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isBlank(request.getImageBase64()) && isBlank(request.getImageUrl())) {
|
if (isBlank(request.getImageUrlOrBase64())) {
|
||||||
log.info("驾驶证识别:没传图片(imageBase64 和 imageUrl 都为空),直接拒绝,未调用识别、不扣费。{}",
|
log.info("驾驶证识别:没传图片(imageUrlOrBase64 为空),直接拒绝,未调用识别、不扣费。{}",
|
||||||
buildInputLogContext(request));
|
buildInputLogContext(request, null));
|
||||||
return formatHint(
|
return formatHint(
|
||||||
"影像源缺失",
|
"影像源缺失",
|
||||||
"imageBase64 与 imageUrl 均未提供,识别引擎没有可处理的图像输入",
|
"imageUrlOrBase64 未提供,识别引擎没有可处理的图像输入",
|
||||||
"任选其一:① imageBase64 传 jpg/jpeg/png/bmp 的 base64(urlencode 后≤4M);"
|
"请传 HTTP/HTTPS 图片链接,或 jpg/jpeg/png/bmp 的 Base64 字符串(可带 data:image 前缀);"
|
||||||
+ "② imageUrl 传可公网直连的 HTTPS/HTTP 地址(≤1024 字符,关闭防盗链)",
|
+ "未 urlencode 时由服务端自动编码",
|
||||||
"side=" + (isBlank(request.getSide()) ? SIDE_DEFAULT_HINT : request.getSide().trim())
|
"side=" + (isBlank(request.getSide()) ? SIDE_DEFAULT_HINT : request.getSide().trim())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (StringUtils.isNotBlank(request.getImageBase64()) && StringUtils.isNotBlank(request.getImageUrl())) {
|
if (ImageInputUtils.resolve(request.getImageUrlOrBase64()) == null) {
|
||||||
log.info("驾驶证识别:客户同时传了 Base64 和图片链接,按规则只用 Base64,忽略链接。{}",
|
log.info("驾驶证识别:imageUrlOrBase64 无法解析为有效影像入参,直接拒绝,未调用识别、不扣费。{}",
|
||||||
buildInputLogContext(request));
|
buildInputLogContext(request, null));
|
||||||
|
return formatHint(
|
||||||
|
"影像入参无效",
|
||||||
|
"imageUrlOrBase64 内容为空或无法识别为 Base64 / HTTP(S) 链接",
|
||||||
|
"链接须以 http:// 或 https:// 开头;Base64 须为有效编码字符串",
|
||||||
|
"imageUrlOrBase64 长度=" + textLength(request.getImageUrlOrBase64())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (resolveDrivingLicenseSide(request) == null) {
|
if (resolveDrivingLicenseSide(request) == null) {
|
||||||
log.info("驾驶证识别:side 参数写错了(只支持 face/front 正页、back 副页),直接拒绝,未调用识别、不扣费。{}",
|
log.info("驾驶证识别:side 参数写错了(只支持 face/front 正页、back 副页),直接拒绝,未调用识别、不扣费。{}",
|
||||||
buildInputLogContext(request));
|
buildInputLogContext(request, ImageInputUtils.resolve(request.getImageUrlOrBase64())));
|
||||||
return formatHint(
|
return formatHint(
|
||||||
"页面标识无效",
|
"页面标识无效",
|
||||||
"参数 side 的取值无法映射到驾驶证正页或副页识别模式",
|
"参数 side 的取值无法映射到驾驶证正页或副页识别模式",
|
||||||
@ -350,20 +362,23 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
/**
|
/**
|
||||||
* 构造调用百度驾驶证的 POST 请求体(application/x-www-form-urlencoded 格式)。
|
* 构造调用百度驾驶证的 POST 请求体(application/x-www-form-urlencoded 格式)。
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>优先使用 Base64(image 参数),备选 URL(url 参数)</li>
|
* <li>Base64 使用 image 参数,HTTP(S) 链接使用 url 参数(均已表单 urlencode)</li>
|
||||||
* <li>始终携带固定的百度参数:detect_direction、driving_license_side、unified_valid_period 等</li>
|
* <li>始终携带固定的百度参数:detect_direction、driving_license_side、unified_valid_period 等</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @param request 客户入参
|
* @param imageInput 解析后的影像入参
|
||||||
* @param drivingLicenseSide 标准化后的 side(front / back)
|
* @param drivingLicenseSide 标准化后的 side(front / back)
|
||||||
* @return 请求体字符串;若既无 Base64 也无 URL 则返回空字符串
|
* @return 请求体字符串;imageInput 为空时返回空字符串
|
||||||
*/
|
*/
|
||||||
private String buildRequestContent(DriverLicenseRecognizeRequest request, String drivingLicenseSide) {
|
private String buildRequestContent(ResolvedImageInput imageInput, String drivingLicenseSide) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
if (StringUtils.isNotBlank(request.getImageBase64())) {
|
if (imageInput == null || StringUtils.isBlank(imageInput.getFormValue())) {
|
||||||
sb.append("&image=").append(request.getImageBase64());
|
return sb.toString();
|
||||||
} else if (StringUtils.isNotBlank(request.getImageUrl())) {
|
}
|
||||||
sb.append("&url=").append(request.getImageUrl());
|
if (ImageInputUtils.ImageInputType.URL == imageInput.getType()) {
|
||||||
|
sb.append("&url=").append(imageInput.getFormValue());
|
||||||
|
} else {
|
||||||
|
sb.append("&image=").append(imageInput.getFormValue());
|
||||||
}
|
}
|
||||||
sb.append("&detect_direction=false");
|
sb.append("&detect_direction=false");
|
||||||
sb.append("&driving_license_side=").append(drivingLicenseSide);
|
sb.append("&driving_license_side=").append(drivingLicenseSide);
|
||||||
@ -552,18 +567,18 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
return text.substring(0, maxLen) + "...";
|
return text.substring(0, maxLen) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 入参日志说明(不打印 base64 正文,只说长度) */
|
/** 入参日志说明(不打印影像正文,只说类型与长度) */
|
||||||
private String buildInputLogContext(DriverLicenseRecognizeRequest request) {
|
private String buildInputLogContext(DriverLicenseRecognizeRequest request, ResolvedImageInput imageInput) {
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
return "未收到请求体";
|
return "未收到请求体";
|
||||||
}
|
}
|
||||||
String sideParam = isBlank(request.getSide()) ? "未传(默认按正页)" : request.getSide().trim();
|
String sideParam = isBlank(request.getSide()) ? "未传(默认按正页)" : request.getSide().trim();
|
||||||
String resolvedSide = resolveDrivingLicenseSide(request);
|
String resolvedSide = resolveDrivingLicenseSide(request);
|
||||||
String targetPage = resolvedSide == null ? "side 无法识别" : sideDesc(resolvedSide);
|
String targetPage = resolvedSide == null ? "side 无法识别" : sideDesc(resolvedSide);
|
||||||
ImageMode mode = ImageMode.of(request);
|
String modeDesc = imageInput != null ? imageInput.getType().getDesc() : "影像未解析";
|
||||||
return String.format("识别%s,side 参数=%s,%s,Base64 长度 %d 字符,图片链接长度 %d 字符",
|
int rawLen = imageInput != null ? textLength(imageInput.getRawValue()) : textLength(request.getImageUrlOrBase64());
|
||||||
targetPage, sideParam, mode.desc,
|
return String.format("识别%s,side 参数=%s,%s,影像原始长度 %d 字符",
|
||||||
textLength(request.getImageBase64()), textLength(request.getImageUrl()));
|
targetPage, sideParam, modeDesc, rawLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 平台回执日志说明(便于失败排查) */
|
/** 平台回执日志说明(便于失败排查) */
|
||||||
@ -603,49 +618,20 @@ public class RecognizeDriverLicenseController extends BaseController {
|
|||||||
// ===================== 内部类型 =====================
|
// ===================== 内部类型 =====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单次识别请求的上下文,集中保存 side / imageMode / 入参日志摘要,避免在主流程中反复解析。
|
* 单次识别请求的上下文,集中保存 side / 影像入参 / 入参日志摘要,避免在主流程中反复解析。
|
||||||
*/
|
*/
|
||||||
private static class RecognizeContext {
|
private static class RecognizeContext {
|
||||||
final String side;
|
final String side;
|
||||||
final ImageMode imageMode;
|
final ResolvedImageInput imageInput;
|
||||||
final String inputLog;
|
final String inputLog;
|
||||||
|
|
||||||
RecognizeContext(String side, ImageMode imageMode, String inputLog) {
|
RecognizeContext(String side, ResolvedImageInput imageInput, String inputLog) {
|
||||||
this.side = side;
|
this.side = side;
|
||||||
this.imageMode = imageMode;
|
this.imageInput = imageInput;
|
||||||
this.inputLog = inputLog;
|
this.inputLog = inputLog;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 图片提交方式枚举,拆分"逻辑标识"与"展示文案",避免字符串比较散落。
|
|
||||||
*/
|
|
||||||
private enum ImageMode {
|
|
||||||
BASE64("使用 Base64 编码图片"),
|
|
||||||
URL("使用图片链接"),
|
|
||||||
BASE64_WITH_URL_IGNORED("同时传了 Base64 和链接,实际使用 Base64"),
|
|
||||||
NONE("未传图片"),
|
|
||||||
UNKNOWN("图片提交方式未知");
|
|
||||||
|
|
||||||
final String desc;
|
|
||||||
|
|
||||||
ImageMode(String desc) {
|
|
||||||
this.desc = desc;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ImageMode of(DriverLicenseRecognizeRequest req) {
|
|
||||||
if (req == null) {
|
|
||||||
return UNKNOWN;
|
|
||||||
}
|
|
||||||
boolean hasBase64 = StringUtils.isNotBlank(req.getImageBase64());
|
|
||||||
boolean hasUrl = StringUtils.isNotBlank(req.getImageUrl());
|
|
||||||
if (hasBase64 && hasUrl) return BASE64_WITH_URL_IGNORED;
|
|
||||||
if (hasBase64) return BASE64;
|
|
||||||
if (hasUrl) return URL;
|
|
||||||
return NONE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 平台错误码元数据。
|
* 平台错误码元数据。
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@ -11,17 +11,15 @@ import lombok.Data;
|
|||||||
public class DriverLicenseRecognizeRequest {
|
public class DriverLicenseRecognizeRequest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图像数据,base64编码后进行urlencode,要求base64编码和urlencode后大小不超过4M
|
* 影像入参(二选一能力已合并为单一字段):
|
||||||
* 支持jpg/jpeg/png/bmp格式
|
* <ul>
|
||||||
* 和url二选一
|
* <li>HTTP/HTTPS 图片链接(长度建议≤1024 字符,未 urlencode 时由服务端自动编码)</li>
|
||||||
|
* <li>图片 Base64 字符串(支持 jpg/jpeg/png/bmp;可带 data:image/...;base64, 前缀;
|
||||||
|
* 编码后 urlencode 前建议≤4M,未 urlencode 时由服务端自动编码)</li>
|
||||||
|
* </ul>
|
||||||
|
* 服务端根据内容自动识别为链接或 Base64。
|
||||||
*/
|
*/
|
||||||
private String imageBase64;
|
private String imageUrlOrBase64;
|
||||||
|
|
||||||
/**
|
|
||||||
* 图片完整URL,URL长度不超过1024字节
|
|
||||||
* 和imageBase64二选一
|
|
||||||
*/
|
|
||||||
private String imageUrl;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* face:识别驾驶证正页、电子驾驶证正页(默认)
|
* face:识别驾驶证正页、电子驾驶证正页(默认)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user