diff --git a/api-mapper/src/main/java/com/heyu/api/data/utils/ImageInputUtils.java b/api-mapper/src/main/java/com/heyu/api/data/utils/ImageInputUtils.java new file mode 100644 index 0000000..a453c13 --- /dev/null +++ b/api-mapper/src/main/java/com/heyu/api/data/utils/ImageInputUtils.java @@ -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) 图片链接。 + *
规则:以 {@code http://} 或 {@code https://} 开头(含已 urlencode 的形式)视为链接,否则视为 Base64。
+ * + * @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; + } + } +} diff --git a/api-web/api-interface/src/main/java/com/heyu/api/controller/car/RecognizeDriverLicenseController.java b/api-web/api-interface/src/main/java/com/heyu/api/controller/car/RecognizeDriverLicenseController.java index 366c6bb..59e7d9a 100644 --- a/api-web/api-interface/src/main/java/com/heyu/api/controller/car/RecognizeDriverLicenseController.java +++ b/api-web/api-interface/src/main/java/com/heyu/api/controller/car/RecognizeDriverLicenseController.java @@ -3,6 +3,8 @@ package com.heyu.api.controller.car; import com.heyu.api.controller.BaseController; import com.heyu.api.data.annotation.EbAuthentication; 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.R; import com.heyu.api.data.utils.StringUtils; @@ -81,27 +83,29 @@ public class RecognizeDriverLicenseController extends BaseController { String validateError = validateRequest(request); if (validateError != null) { log.info("驾驶证识别:参数检查没通过,直接返回错误(还没调识别、不扣费)。{} 返回给客户:{}", - buildInputLogContext(request), abbreviate(validateError, 120)); + buildInputLogContext(request, ImageInputUtils.resolve(request.getImageUrlOrBase64())), + abbreviate(validateError, 120)); return R.error(validateError); } - // ---------- 步骤②:构建上下文(side、图片模式、入参摘要) ---------- + // ---------- 步骤②:解析影像入参 & 构建上下文 ---------- + ResolvedImageInput imageInput = ImageInputUtils.resolve(request.getImageUrlOrBase64()); ctx = new RecognizeContext( resolveDrivingLicenseSide(request), - ImageMode.of(request), - buildInputLogContext(request)); + imageInput, + buildInputLogContext(request, imageInput)); // ---------- 步骤③:组装百度 API 请求体 ---------- - String content = buildRequestContent(request, ctx.side); + String content = buildRequestContent(imageInput, ctx.side); if (isBlank(content)) { log.error("驾驶证识别:组装请求失败,请求里没带有效图片。识别{},{}。{}", - sideDesc(ctx.side), ctx.imageMode.desc, ctx.inputLog); + sideDesc(ctx.side), ctx.imageInput.getType().getDesc(), ctx.inputLog); return okResult(ctx.side, formatHint( "报文组装异常", "识别请求体在序列化后未包含任何影像载荷,平台侧无法受理本次识别", - "请核对 imageBase64 是否已 urlencode、imageUrl 是否非空字符串;" + "请核对 imageUrlOrBase64 是否非空且为有效 Base64 或 HTTP(S) 链接;" + "请求头须为 application/x-www-form-urlencoded", - "目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageMode.name() + "目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageInput.getType().name() ), null); } @@ -112,7 +116,7 @@ public class RecognizeDriverLicenseController extends BaseController { "服务无回执", "识别指令已下发,但在约定时间内未收到平台可解析的 JSON 回执", "建议间隔 3~5 秒重试;若连续失败,请记录 traceId、调用时刻与 side 并联系技术支持排查链路", - "目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageMode.name() + "目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageInput.getType().name() ), null); } @@ -130,10 +134,12 @@ public class RecognizeDriverLicenseController extends BaseController { long cost = System.currentTimeMillis() - start; String side = ctx != null ? ctx.side : (request != null ? resolveDrivingLicenseSide(request) : ApiConstants.front); - String mode = ctx != null ? ctx.imageMode.desc - : (request != null ? ImageMode.of(request).desc : "未知"); + ResolvedImageInput fallbackImage = request != null + ? ImageInputUtils.resolve(request.getImageUrlOrBase64()) : null; + String mode = ctx != null ? ctx.imageInput.getType().getDesc() + : (fallbackImage != null ? fallbackImage.getType().getDesc() : "未知"); log.error("驾驶证识别:程序运行出错,耗时 {} ms。识别{},{}。客户传的:{}。异常:{} - {}", - cost, sideDesc(side), mode, buildInputLogContext(request), + cost, sideDesc(side), mode, buildInputLogContext(request, fallbackImage), e.getClass().getSimpleName(), e.getMessage() != null ? e.getMessage() : "无具体说明", e); return okResult(side, formatHint( @@ -159,12 +165,12 @@ public class RecognizeDriverLicenseController extends BaseController { private Mapdiff --git a/api-web/api-interface/src/main/java/com/heyu/api/request/car/DriverLicenseRecognizeRequest.java b/api-web/api-interface/src/main/java/com/heyu/api/request/car/DriverLicenseRecognizeRequest.java index a857e42..7f73b4e 100644 --- a/api-web/api-interface/src/main/java/com/heyu/api/request/car/DriverLicenseRecognizeRequest.java +++ b/api-web/api-interface/src/main/java/com/heyu/api/request/car/DriverLicenseRecognizeRequest.java @@ -11,17 +11,15 @@ import lombok.Data; public class DriverLicenseRecognizeRequest { /** - * 图像数据,base64编码后进行urlencode,要求base64编码和urlencode后大小不超过4M - * 支持jpg/jpeg/png/bmp格式 - * 和url二选一 + * 影像入参(二选一能力已合并为单一字段): + *