From c272282d550a67c27ac913bbe96bbcb86617b3e7 Mon Sep 17 00:00:00 2001 From: quyixiao <2621048238@qq.com> Date: Thu, 4 Jun 2026 22:23:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../car/RecognizeDriverLicenseController.java | 766 +++++++++--------- 1 file changed, 371 insertions(+), 395 deletions(-) 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 3374ee9..3d21719 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 @@ -14,6 +14,9 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; @@ -37,59 +40,18 @@ import java.util.Map; *
  • 鉴权:{@link EbAuthentication}(Tencent 鉴权头,见项目网关配置)
  • * * - *

    请求参数({@link DriverLicenseRecognizeRequest})

    - * - * - * - * - * - * - * - * - *
    字段必填说明
    imageBase64与 imageUrl 二选一图片 Base64,需 urlencode;编码后≤4M;支持 jpg/jpeg/png/bmp;优先级高于 url
    imageUrl与 imageBase64 二选一图片完整 URL,≤1024 字节;需公网可访问并关闭防盗链
    side否,默认 face{@code face}/{@code front}:正页/电子证正页;{@code back}:副页。 - * 映射百度参数 {@code driving_license_side=front|back}
    - * - *

    返回结构(统一包装 {@link R})

    - *
    - * {
    - *   "code": "200",           // 成功为 200;仅入参非法时为 400
    - *   "msg": "...",            // 成功默认文案;业务异常时写入详细提示(仍可能 code=200)
    - *   "traceId": "...",
    - *   "data": { ... }          // 见下方 data 结构,失败时可能为空对象
    - * }
    - * 
    - *

    返回约定(重要):

    + *

    返回约定(重要)

    * * - *

    data 结构(按 side 区分)

    - *

    正页 / 电子证正页({@link RecognizeDriverLicenseFaceResp},side=face 或 front):

    - * - *

    副页({@link RecognizeDriverLicenseBackResp},side=back):

    - * - * - *

    字段与百度 {@code words_result} 中中文 key 的对应关系见 - * 官方返回示例

    - * * @author heyu * @since 1.0.0 * @see DriverLicenseRecognizeRequest * @see RecognizeDriverLicenseFaceResp * @see RecognizeDriverLicenseBackResp - * - * - * 开发者: 瞿贻晓 */ @Slf4j @RestController @@ -99,49 +61,11 @@ public class RecognizeDriverLicenseController extends BaseController { /** 百度驾驶证识别 API 路径,完整地址见类注释 */ private static final String DRIVING_LICENSE_URI = "/rest/2.0/ocr/v1/driving_license"; - /** 百度 error_code → 对外 msg 原因说明(日志与返回 msg 使用「平台」表述,注释可写百度) */ - private static final Map PLATFORM_ERROR_HINTS = new HashMap<>(); - - static { - PLATFORM_ERROR_HINTS.put("17", "本接口当日累计调用次数已触达平台日配额上限,本次识别请求被拒绝"); - PLATFORM_ERROR_HINTS.put("18", "单位时间请求过于密集,已触发平台 QPS 限流,请拉长调用间隔后重试"); - PLATFORM_ERROR_HINTS.put("19", "平台侧总调用配额已耗尽,需运营侧扩容或次日再试"); - PLATFORM_ERROR_HINTS.put("100", "提交的表单字段组合不符合驾驶证识别接口契约,请对照文档核对字段名与取值"); - PLATFORM_ERROR_HINTS.put("110", "平台鉴权凭证校验未通过,属服务端配置异常,需运维刷新令牌"); - PLATFORM_ERROR_HINTS.put("111", "平台鉴权凭证已超过有效期,属服务端配置异常,需运维重新签发"); - PLATFORM_ERROR_HINTS.put("216100", "存在非法或与接口不匹配的参数项"); - PLATFORM_ERROR_HINTS.put("216101", "影像入参缺失:未提供可用的 image 或 url 字段"); - PLATFORM_ERROR_HINTS.put("216103", "单个参数字段长度超限(常见于 url 过长或 base64 体积过大)"); - PLATFORM_ERROR_HINTS.put("216110", "当前应用未开通驾驶证识别能力或授权范围不包含本接口"); - PLATFORM_ERROR_HINTS.put("216200", "解码后的图片数据为空,上传环节可能未完成或内容损坏"); - PLATFORM_ERROR_HINTS.put("216201", "图片编码格式不在允许列表,仅支持 jpg、jpeg、png、bmp"); - PLATFORM_ERROR_HINTS.put("216202", "像素尺寸或编码后体积不满足规范(边长 15~4096px,编码后≤4M)"); - PLATFORM_ERROR_HINTS.put("216203", "引擎无法从当前画面中稳定提取驾驶证文本,多见于模糊、过曝或非证件照"); - PLATFORM_ERROR_HINTS.put("216630", "证件版面识别失败,建议重拍并保证四角完整、无遮挡"); - PLATFORM_ERROR_HINTS.put("282000", "平台识别引擎内部异常,属短暂性故障,可间隔数秒后重试"); - } + /** 入参绑定失败/page side 无法解析等错误中的"未传"展示文案 */ + private static final String SIDE_DEFAULT_HINT = "未传(将按正页处理)"; /** * 驾驶证识别 - *

    - * 请求示例(form 表单): - *

    - *
    -     * POST /driver/license/recognize
    -     * Content-Type: application/x-www-form-urlencoded
    -     *
    -     * imageUrl=https://example.com/license.jpg&side=face
    -     * // 或 imageBase64=xxx&side=back
    -     * 
    - *

    - * 成功示例(正页,data 为 {@link RecognizeDriverLicenseFaceResp}): - *

    - *
    -     * { "code":"200", "msg":"成功", "data": { "licenseNumber":"...", "name":"...", "vehicleType":"C1", ... } }
    -     * 
    - *

    - * 已调用百度但识别异常时仍返回 code=200,msg 中带处置指引,data 可能为空对象。 - *

    * * @param request 驾驶证识别请求,字段见类注释「请求参数」 * @return 正页/电子证正页返回 {@link RecognizeDriverLicenseFaceResp};副页返回 {@link RecognizeDriverLicenseBackResp} @@ -150,6 +74,7 @@ public class RecognizeDriverLicenseController extends BaseController { @PostMapping("/recognize") public R recognize(DriverLicenseRecognizeRequest request) { long start = System.currentTimeMillis(); + RecognizeContext ctx = null; try { String validateError = validateRequest(request); if (validateError != null) { @@ -158,80 +83,50 @@ public class RecognizeDriverLicenseController extends BaseController { return R.error(validateError); } - String drivingLicenseSide = resolveDrivingLicenseSide(request); - String imageMode = resolveImageMode(request); - String inputContext = buildInputLogContext(request); + ctx = new RecognizeContext( + resolveDrivingLicenseSide(request), + ImageMode.of(request), + buildInputLogContext(request)); - String content = buildRequestContent(request, drivingLicenseSide); + String content = buildRequestContent(request, ctx.side); if (isBlank(content)) { log.error("驾驶证识别:组装请求失败,请求里没带有效图片。识别{},{}。{}", - sideDesc(drivingLicenseSide), imageModeDesc(imageMode), inputContext); - return okResult(drivingLicenseSide, formatHint( + sideDesc(ctx.side), ctx.imageMode.desc, ctx.inputLog); + return okResult(ctx.side, formatHint( "报文组装异常", "识别请求体在序列化后未包含任何影像载荷,平台侧无法受理本次识别", "请核对 imageBase64 是否已 urlencode、imageUrl 是否非空字符串;" + "请求头须为 application/x-www-form-urlencoded", - "目标页面=" + sideLabel(drivingLicenseSide) + ",影像模式=" + imageMode + "目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageMode.name() ), null); } - int requestBodyLength = content.length(); - log.info("驾驶证识别:开始调用平台识别。识别{},{},请求大小约 {} 字节。{}", - sideDesc(drivingLicenseSide), imageModeDesc(imageMode), requestBodyLength, inputContext); - Map platformResult = requestBaidu(DRIVING_LICENSE_URI, content); + Map platformResult = callPlatform(content, ctx); if (platformResult == null) { - log.error("驾驶证识别:平台没有返回结果(可能网络超时、鉴权失败或响应无法解析)。识别{},{}," - + "请求约 {} 字节。{}", - sideDesc(drivingLicenseSide), imageModeDesc(imageMode), requestBodyLength, inputContext); - return okResult(drivingLicenseSide, formatHint( + return okResult(ctx.side, formatHint( "服务无回执", "识别指令已下发,但在约定时间内未收到平台可解析的 JSON 回执", "建议间隔 3~5 秒重试;若连续失败,请记录 traceId、调用时刻与 side 并联系技术支持排查链路", - "目标页面=" + sideLabel(drivingLicenseSide) + ",影像模式=" + imageMode + "目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageMode.name() ), null); } - String hint = resolvePlatformHint(platformResult, drivingLicenseSide, inputContext, imageMode); - Object data = ApiConstants.back.equals(drivingLicenseSide) + Object data = ApiConstants.back.equals(ctx.side) ? buildBackResp(platformResult) : buildFaceResp(platformResult); + String hint = resolvePlatformHint(platformResult, ctx, data); - if (hint == null && isRecognizeDataEmpty(data)) { - log.info("驾驶证识别:平台有返回,但证号、姓名等字段一个都没识别出来(可能传错正/副页或图片不清晰)。" - + "识别{},{}。客户传的:{}。平台回执:{}", - sideDesc(drivingLicenseSide), imageModeDesc(imageMode), - inputContext, buildPlatformReceiptSummary(platformResult)); - hint = formatHint( - "字段映射为空", - "平台回执已通过结构校验,但证号、姓名、准驾车型等结构化字段均未命中", - ApiConstants.back.equals(drivingLicenseSide) - ? "副页识别请确认 side=back,且画面包含「记录」「档案编号」等区域;避免裁剪或反光" - : "正页识别请确认 side=face/front,画面须包含完整证面;电子驾驶证需保证截图清晰", - "目标页面=" + sideLabel(drivingLicenseSide) + ",已映射字段数=0" - ); - } - - long cost = System.currentTimeMillis() - start; - int mappedFieldCount = countMappedFields(data); - if (StringUtils.isNotBlank(hint)) { - log.info("驾驶证识别:处理结束(接口仍返回成功,但带了提示信息)。耗时 {} ms,识别出 {} 个字段。" - + "识别{},{}。客户传的:{}。平台回执:{}。给客户的提示:{}", - cost, mappedFieldCount, sideDesc(drivingLicenseSide), imageModeDesc(imageMode), - inputContext, buildPlatformReceiptSummary(platformResult), abbreviate(hint, 120)); - } else { - log.info("驾驶证识别:识别成功。耗时 {} ms,共识别出 {} 个字段。识别{},{}。客户传的:{}。平台回执:{}", - cost, mappedFieldCount, sideDesc(drivingLicenseSide), imageModeDesc(imageMode), - inputContext, buildPlatformReceiptSummary(platformResult)); - } - return okResult(drivingLicenseSide, hint, data); + logRecognizeResult(ctx, platformResult, data, hint, start); + return okResult(ctx.side, hint, data); } catch (Exception e) { long cost = System.currentTimeMillis() - start; - String side = request != null ? resolveDrivingLicenseSide(request) : ApiConstants.front; + 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 : "未知"); log.error("驾驶证识别:程序运行出错,耗时 {} ms。识别{},{}。客户传的:{}。异常:{} - {}", - cost, sideDesc(side), - request != null ? imageModeDesc(resolveImageMode(request)) : "未知", - buildInputLogContext(request), + cost, sideDesc(side), mode, buildInputLogContext(request), e.getClass().getSimpleName(), e.getMessage() != null ? e.getMessage() : "无具体说明", e); return okResult(side, formatHint( @@ -244,6 +139,86 @@ public class RecognizeDriverLicenseController extends BaseController { } } + // ===================== 流程拆分方法 ===================== + + private Map callPlatform(String content, RecognizeContext ctx) { + int len = content.length(); + log.info("驾驶证识别:开始调用平台识别。识别{},{},请求大小约 {} 字节。{}", + sideDesc(ctx.side), ctx.imageMode.desc, len, ctx.inputLog); + Map result = requestBaidu(DRIVING_LICENSE_URI, content); + if (result == null) { + log.error("驾驶证识别:平台没有返回结果(可能网络超时、鉴权失败或响应无法解析)。" + + "识别{},{},请求约 {} 字节。{}", + sideDesc(ctx.side), ctx.imageMode.desc, len, ctx.inputLog); + } + return result; + } + + private String resolvePlatformHint(Map platformResult, RecognizeContext ctx, Object data) { + Object errorCodeObj = platformResult.get("error_code"); + if (errorCodeObj != null) { + String errorCode = String.valueOf(errorCodeObj); + Object errorMsgObj = platformResult.get("error_msg"); + String errorMsg = errorMsgObj != null ? errorMsgObj.toString() : "平台未返回文字描述"; + String category = PlatformError.categoryOf(errorCode); + log.error("驾驶证识别:平台拒绝了本次识别。[{}] 错误码 {},原因:{}。识别{},{}。客户传的:{}。{}", + category, errorCode, errorMsg, + sideDesc(ctx.side), ctx.imageMode.desc, + ctx.inputLog, buildPlatformReceiptSummary(platformResult)); + return formatHint( + category, + PlatformError.reasonOf(errorCode), + PlatformError.suggestionOf(errorCode, sideLabel(ctx.side)), + "错误码=" + errorCode + ",错误描述=" + errorMsg + ",目标页面=" + sideLabel(ctx.side) + ); + } + if (isWordsResultEmpty(platformResult)) { + log.info("驾驶证识别:平台返回了,但没有识别结果数据(可能不是驾驶证或 side 传错)。识别{},{}。客户传的:{}。{}", + sideDesc(ctx.side), ctx.imageMode.desc, + ctx.inputLog, buildPlatformReceiptSummary(platformResult)); + return formatHint( + "结构化结果缺失", + "平台回执未包含可解析的识别结果集合,无法进入字段映射环节", + "优先排查:① 上传内容是否为驾驶证对应页面;② side 是否与实物一致;" + + "③ 使用 URL 时确保平台抓取节点可访问且无 403/302 拦截", + "目标页面=" + sideLabel(ctx.side) + ",解析状态=结果集为空" + ); + } + if (isAllStringFieldsBlank(data)) { + log.info("驾驶证识别:平台有返回,但证号、姓名等字段一个都没识别出来(可能传错正/副页或图片不清晰)。" + + "识别{},{}。客户传的:{}。平台回执:{}", + sideDesc(ctx.side), ctx.imageMode.desc, + ctx.inputLog, buildPlatformReceiptSummary(platformResult)); + return formatHint( + "字段映射为空", + "平台回执已通过结构校验,但证号、姓名、准驾车型等结构化字段均未命中", + ApiConstants.back.equals(ctx.side) + ? "副页识别请确认 side=back,且画面包含「记录」「档案编号」等区域;避免裁剪或反光" + : "正页识别请确认 side=face/front,画面须包含完整证面;电子驾驶证需保证截图清晰", + "目标页面=" + sideLabel(ctx.side) + ",已映射字段数=0" + ); + } + return null; + } + + private void logRecognizeResult(RecognizeContext ctx, Map platformResult, + Object data, String hint, long start) { + long cost = System.currentTimeMillis() - start; + int mapped = countNonBlankStringFields(data); + if (StringUtils.isNotBlank(hint)) { + log.info("驾驶证识别:处理结束(接口仍返回成功,但带了提示信息)。耗时 {} ms,识别出 {} 个字段。" + + "识别{},{}。客户传的:{}。平台回执:{}。给客户的提示:{}", + cost, mapped, sideDesc(ctx.side), ctx.imageMode.desc, + ctx.inputLog, buildPlatformReceiptSummary(platformResult), abbreviate(hint, 120)); + } else { + log.info("驾驶证识别:识别成功。耗时 {} ms,共识别出 {} 个字段。识别{},{}。客户传的:{}。平台回执:{}", + cost, mapped, sideDesc(ctx.side), ctx.imageMode.desc, + ctx.inputLog, buildPlatformReceiptSummary(platformResult)); + } + } + + // ===================== 校验与上下文 ===================== + private String validateRequest(DriverLicenseRecognizeRequest request) { if (request == null) { log.info("驾驶证识别:没收到任何请求参数(表单未绑定成功),直接拒绝,未调用识别、不扣费"); @@ -263,7 +238,7 @@ public class RecognizeDriverLicenseController extends BaseController { "imageBase64 与 imageUrl 均未提供,识别引擎没有可处理的图像输入", "任选其一:① imageBase64 传 jpg/jpeg/png/bmp 的 base64(urlencode 后≤4M);" + "② imageUrl 传可公网直连的 HTTPS/HTTP 地址(≤1024 字符,关闭防盗链)", - "side=" + (isBlank(request.getSide()) ? "未传(将按正页处理)" : request.getSide().trim()) + "side=" + (isBlank(request.getSide()) ? SIDE_DEFAULT_HINT : request.getSide().trim()) ); } if (StringUtils.isNotBlank(request.getImageBase64()) && StringUtils.isNotBlank(request.getImageUrl())) { @@ -284,19 +259,6 @@ public class RecognizeDriverLicenseController extends BaseController { return null; } - private String resolveImageMode(DriverLicenseRecognizeRequest request) { - if (request == null) { - return "未知"; - } - if (StringUtils.isNotBlank(request.getImageBase64())) { - return StringUtils.isNotBlank(request.getImageUrl()) ? "Base64优先(已忽略URL)" : "Base64"; - } - if (StringUtils.isNotBlank(request.getImageUrl())) { - return "URL"; - } - return "无"; - } - private String resolveDrivingLicenseSide(DriverLicenseRecognizeRequest request) { if (request == null || isBlank(request.getSide())) { return ApiConstants.front; @@ -311,31 +273,15 @@ public class RecognizeDriverLicenseController extends BaseController { return null; } - private String sideLabel(String drivingLicenseSide) { - return ApiConstants.back.equals(drivingLicenseSide) ? "副页(back)" : "正页(front/face)"; + private String sideLabel(String side) { + return ApiConstants.back.equals(side) ? "副页(back)" : "正页(front/face)"; } - /** 日志用:正页/副页中文说明 */ - private String sideDesc(String drivingLicenseSide) { - return ApiConstants.back.equals(drivingLicenseSide) ? "驾驶证副页" : "驾驶证正页"; + private String sideDesc(String side) { + return ApiConstants.back.equals(side) ? "驾驶证副页" : "驾驶证正页"; } - /** 日志用:图片提交方式中文说明 */ - private String imageModeDesc(String imageMode) { - if ("Base64".equals(imageMode)) { - return "使用 Base64 编码图片"; - } - if ("URL".equals(imageMode)) { - return "使用图片链接"; - } - if (imageMode != null && imageMode.contains("Base64")) { - return "同时传了 Base64 和链接,实际使用 Base64"; - } - if ("无".equals(imageMode)) { - return "未传图片"; - } - return "图片提交方式未知"; - } + // ===================== 请求/响应构造 ===================== private String buildRequestContent(DriverLicenseRecognizeRequest request, String drivingLicenseSide) { StringBuilder sb = new StringBuilder(); @@ -362,231 +308,6 @@ public class RecognizeDriverLicenseController extends BaseController { return result.setData(data); } - private String resolvePlatformHint(Map platformResult, String drivingLicenseSide, - String inputContext, String imageMode) { - Object errorCodeObj = platformResult.get("error_code"); - if (errorCodeObj != null) { - String errorCode = String.valueOf(errorCodeObj); - Object errorMsgObj = platformResult.get("error_msg"); - String errorMsg = errorMsgObj != null ? errorMsgObj.toString() : "平台未返回文字描述"; - String knownHint = PLATFORM_ERROR_HINTS.get(errorCode); - String category = resolveErrorCategory(errorCode); - log.error("驾驶证识别:平台拒绝了本次识别。[{}] 错误码 {},原因:{}。识别{},{}。客户传的:{}。{}", - category, errorCode, errorMsg, - sideDesc(drivingLicenseSide), imageModeDesc(imageMode), - inputContext, buildPlatformReceiptSummary(platformResult)); - return formatHint( - category, - knownHint != null ? knownHint : "平台返回了未在本地维护的错误码,需结合错误描述人工判读", - buildPlatformErrorSuggestion(errorCode, drivingLicenseSide), - "错误码=" + errorCode + ",错误描述=" + errorMsg + ",目标页面=" + sideLabel(drivingLicenseSide) - ); - } - if (isWordsResultEmpty(platformResult)) { - log.info("驾驶证识别:平台返回了,但没有识别结果数据(可能不是驾驶证或 side 传错)。识别{},{}。客户传的:{}。{}", - sideDesc(drivingLicenseSide), imageModeDesc(imageMode), - inputContext, buildPlatformReceiptSummary(platformResult)); - return formatHint( - "结构化结果缺失", - "平台回执未包含可解析的识别结果集合,无法进入字段映射环节", - "优先排查:① 上传内容是否为驾驶证对应页面;② side 是否与实物一致;" - + "③ 使用 URL 时确保平台抓取节点可访问且无 403/302 拦截", - "目标页面=" + sideLabel(drivingLicenseSide) + ",解析状态=结果集为空" - ); - } - return null; - } - - private String resolveErrorCategory(String errorCode) { - if ("17".equals(errorCode)) { - return "日配额耗尽"; - } - if ("18".equals(errorCode)) { - return "并发限流"; - } - if ("19".equals(errorCode)) { - return "总配额耗尽"; - } - if ("110".equals(errorCode) || "111".equals(errorCode)) { - return "鉴权失效"; - } - if (errorCode.startsWith("2162")) { - return "影像不合规"; - } - if ("282000".equals(errorCode)) { - return "引擎内部错误"; - } - return "平台业务拒绝"; - } - - private String buildPlatformErrorSuggestion(String errorCode, String drivingLicenseSide) { - if ("17".equals(errorCode)) { - return "请安排次日再试,或联系运营提升日调用配额"; - } - if ("18".equals(errorCode)) { - return "请在客户端增加限流/退避策略,避免瞬时并发过高"; - } - if ("19".equals(errorCode)) { - return "请联系运营确认平台总配额与续费状态"; - } - if ("110".equals(errorCode)) { - return "属服务端鉴权配置问题,需技术支持检查平台密钥与令牌缓存"; - } - if ("111".equals(errorCode)) { - return "属服务端令牌过期,需技术支持重新获取并刷新缓存"; - } - if ("216200".equals(errorCode)) { - return "请重新上传图片,确认 base64 未截断、未混入换行或非法字符"; - } - if ("216201".equals(errorCode)) { - return "请将图片转为 jpg/jpeg/png/bmp 之一后重新编码上传"; - } - if ("216202".equals(errorCode)) { - return "请压缩或裁剪图片,保证最长边≤4096px、最短边≥15px,且 urlencode 后≤4M"; - } - if ("216203".equals(errorCode)) { - return "请在自然光下重拍,确保证面占画面主体且文字可辨;side=" + sideLabel(drivingLicenseSide); - } - if ("216630".equals(errorCode)) { - return "请平铺证件拍摄,避免手指遮挡关键字段;副页识别务必传 side=back"; - } - if ("282000".equals(errorCode)) { - return "短暂性故障,建议 5~10 秒后单次重试,不宜连续轰炸接口"; - } - return "请依据错误描述调整入参或影像后重试;持续失败请附带 traceId 联系技术支持"; - } - - @SuppressWarnings("unchecked") - private boolean isWordsResultEmpty(Map platformResult) { - Object wordsResult = platformResult.get("words_result"); - if (wordsResult == null) { - return true; - } - if (wordsResult instanceof Map) { - return ((Map) wordsResult).isEmpty(); - } - return false; - } - - private boolean isRecognizeDataEmpty(Object data) { - if (data instanceof RecognizeDriverLicenseFaceResp) { - RecognizeDriverLicenseFaceResp r = (RecognizeDriverLicenseFaceResp) data; - return isBlank(r.getLicenseNumber()) && isBlank(r.getName()) && isBlank(r.getGender()) - && isBlank(r.getNationality()) && isBlank(r.getBirthDate()) && isBlank(r.getIssueDate()) - && isBlank(r.getVehicleType()) && isBlank(r.getAddress()) && isBlank(r.getIssueUnit()) - && isBlank(r.getStartDate()) && isBlank(r.getEndDate()) && isBlank(r.getAccumulatedPoints()) - && isBlank(r.getStatus()) && isBlank(r.getArchiveNumber()) && isBlank(r.getGenerateTime()) - && isBlank(r.getCurrentTime()) && isBlank(r.getBarcodeNumber()); - } - if (data instanceof RecognizeDriverLicenseBackResp) { - RecognizeDriverLicenseBackResp r = (RecognizeDriverLicenseBackResp) data; - return isBlank(r.getName()) && isBlank(r.getRecord()) && isBlank(r.getCardNumber()) - && isBlank(r.getArchiveNumber()); - } - return true; - } - - private String formatHint(String category, String reason, String suggestion, String detail) { - StringBuilder sb = new StringBuilder(); - sb.append("【驾驶证识别·").append(category).append("】").append(reason); - if (StringUtils.isNotBlank(detail)) { - sb.append("|定位信息:").append(detail); - } - sb.append("|处置指引:").append(suggestion); - return sb.toString(); - } - - private String abbreviate(String text, int maxLen) { - if (text == null || text.length() <= maxLen) { - return text; - } - return text.substring(0, maxLen) + "..."; - } - - /** - * 入参日志说明(不打印 base64 正文,只说长度) - */ - private String buildInputLogContext(DriverLicenseRecognizeRequest request) { - if (request == null) { - return "未收到请求体"; - } - String sideParam = isBlank(request.getSide()) ? "未传(默认按正页)" : request.getSide().trim(); - String resolvedSide = resolveDrivingLicenseSide(request); - String targetPage = resolvedSide == null ? "side 无法识别" : sideDesc(resolvedSide); - String imageMode = resolveImageMode(request); - int base64Len = textLength(request.getImageBase64()); - int urlLen = textLength(request.getImageUrl()); - return String.format("识别%s,side 参数=%s,%s,Base64 长度 %d 字符,图片链接长度 %d 字符", - targetPage, sideParam, imageModeDesc(imageMode), base64Len, urlLen); - } - - /** - * 平台回执日志说明(便于失败排查) - */ - private String buildPlatformReceiptSummary(Map platformResult) { - if (platformResult == null) { - return "平台无任何回执"; - } - Object errorCode = platformResult.get("error_code"); - Object errorMsg = platformResult.get("error_msg"); - Object logId = platformResult.get("log_id"); - if (errorCode != null) { - return String.format("平台返回失败,错误码 %s,说明:%s,流水号 %s", - errorCode, - errorMsg != null ? errorMsg : "无", - logId != null ? logId : "无"); - } - Object wordsResultNum = platformResult.get("words_result_num"); - Object wordsResult = platformResult.get("words_result"); - int fieldCount = 0; - if (wordsResult instanceof Map) { - fieldCount = ((Map) wordsResult).size(); - } - return String.format("平台返回成功,识别结果约 %s 项、结构化字段 %d 个,流水号 %s", - wordsResultNum != null ? wordsResultNum : "未知", - fieldCount, - logId != null ? logId : "无"); - } - - private int textLength(String value) { - return value == null ? 0 : value.length(); - } - - private int countMappedFields(Object data) { - if (data instanceof RecognizeDriverLicenseFaceResp) { - RecognizeDriverLicenseFaceResp r = (RecognizeDriverLicenseFaceResp) data; - int count = 0; - if (StringUtils.isNotBlank(r.getLicenseNumber())) count++; - if (StringUtils.isNotBlank(r.getName())) count++; - if (StringUtils.isNotBlank(r.getGender())) count++; - if (StringUtils.isNotBlank(r.getNationality())) count++; - if (StringUtils.isNotBlank(r.getBirthDate())) count++; - if (StringUtils.isNotBlank(r.getIssueDate())) count++; - if (StringUtils.isNotBlank(r.getVehicleType())) count++; - if (StringUtils.isNotBlank(r.getAddress())) count++; - if (StringUtils.isNotBlank(r.getIssueUnit())) count++; - if (StringUtils.isNotBlank(r.getStartDate())) count++; - if (StringUtils.isNotBlank(r.getEndDate())) count++; - if (StringUtils.isNotBlank(r.getAccumulatedPoints())) count++; - if (StringUtils.isNotBlank(r.getStatus())) count++; - if (StringUtils.isNotBlank(r.getArchiveNumber())) count++; - if (StringUtils.isNotBlank(r.getGenerateTime())) count++; - if (StringUtils.isNotBlank(r.getCurrentTime())) count++; - if (StringUtils.isNotBlank(r.getBarcodeNumber())) count++; - return count; - } - if (data instanceof RecognizeDriverLicenseBackResp) { - RecognizeDriverLicenseBackResp r = (RecognizeDriverLicenseBackResp) data; - int count = 0; - if (StringUtils.isNotBlank(r.getName())) count++; - if (StringUtils.isNotBlank(r.getRecord())) count++; - if (StringUtils.isNotBlank(r.getCardNumber())) count++; - if (StringUtils.isNotBlank(r.getArchiveNumber())) count++; - return count; - } - return 0; - } - private RecognizeDriverLicenseFaceResp buildFaceResp(Map data) { RecognizeDriverLicenseFaceResp resp = new RecognizeDriverLicenseFaceResp(); resp.setLicenseNumber(getWords(data, "证号")); @@ -623,9 +344,264 @@ public class RecognizeDriverLicenseController extends BaseController { } private String firstNonBlank(String first, String second) { - if (StringUtils.isNotBlank(first)) { - return first; + return StringUtils.isNotBlank(first) ? first : second; + } + + // ===================== 通用工具 ===================== + + @SuppressWarnings("unchecked") + private boolean isWordsResultEmpty(Map platformResult) { + Object wordsResult = platformResult.get("words_result"); + if (wordsResult == null) { + return true; + } + if (wordsResult instanceof Map) { + return ((Map) wordsResult).isEmpty(); + } + return false; + } + + /** 统计响应对象中非空 String 字段数量(基于反射,避免新增字段时遗漏维护) */ + private static int countNonBlankStringFields(Object data) { + if (data == null) { + return 0; + } + int count = 0; + for (Field f : data.getClass().getDeclaredFields()) { + if (!String.class.equals(f.getType())) { + continue; + } + try { + f.setAccessible(true); + Object v = f.get(data); + if (v instanceof String && StringUtils.isNotBlank((String) v)) { + count++; + } + } catch (IllegalAccessException ignored) { + // 反射失败视为该字段为空,不影响整体统计 + } + } + return count; + } + + private static boolean isAllStringFieldsBlank(Object data) { + return countNonBlankStringFields(data) == 0; + } + + private String formatHint(String category, String reason, String suggestion, String detail) { + StringBuilder sb = new StringBuilder(); + sb.append("【驾驶证识别·").append(category).append("】").append(reason); + if (StringUtils.isNotBlank(detail)) { + sb.append("|定位信息:").append(detail); + } + sb.append("|处置指引:").append(suggestion); + return sb.toString(); + } + + private String abbreviate(String text, int maxLen) { + if (text == null || text.length() <= maxLen) { + return text; + } + return text.substring(0, maxLen) + "..."; + } + + /** 入参日志说明(不打印 base64 正文,只说长度) */ + private String buildInputLogContext(DriverLicenseRecognizeRequest request) { + if (request == null) { + return "未收到请求体"; + } + String sideParam = isBlank(request.getSide()) ? "未传(默认按正页)" : request.getSide().trim(); + String resolvedSide = resolveDrivingLicenseSide(request); + String targetPage = resolvedSide == null ? "side 无法识别" : sideDesc(resolvedSide); + ImageMode mode = ImageMode.of(request); + return String.format("识别%s,side 参数=%s,%s,Base64 长度 %d 字符,图片链接长度 %d 字符", + targetPage, sideParam, mode.desc, + textLength(request.getImageBase64()), textLength(request.getImageUrl())); + } + + /** 平台回执日志说明(便于失败排查) */ + private String buildPlatformReceiptSummary(Map platformResult) { + if (platformResult == null) { + return "平台无任何回执"; + } + Object errorCode = platformResult.get("error_code"); + Object errorMsg = platformResult.get("error_msg"); + Object logId = platformResult.get("log_id"); + if (errorCode != null) { + return String.format("平台返回失败,错误码 %s,说明:%s,流水号 %s", + errorCode, + errorMsg != null ? errorMsg : "无", + logId != null ? logId : "无"); + } + Object wordsResultNum = platformResult.get("words_result_num"); + Object wordsResult = platformResult.get("words_result"); + int fieldCount = wordsResult instanceof Map ? ((Map) wordsResult).size() : 0; + return String.format("平台返回成功,识别结果约 %s 项、结构化字段 %d 个,流水号 %s", + wordsResultNum != null ? wordsResultNum : "未知", + fieldCount, + logId != null ? logId : "无"); + } + + private int textLength(String value) { + return value == null ? 0 : value.length(); + } + + // ===================== 内部类型 ===================== + + /** + * 单次识别请求的上下文,集中保存 side / imageMode / 入参日志摘要,避免在主流程中反复解析。 + */ + private static class RecognizeContext { + final String side; + final ImageMode imageMode; + final String inputLog; + + RecognizeContext(String side, ImageMode imageMode, String inputLog) { + this.side = side; + this.imageMode = imageMode; + 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; + } + } + + /** + * 平台错误码元数据。 + *

    + * 将原本散落在 PLATFORM_ERROR_HINTS / resolveErrorCategory / buildPlatformErrorSuggestion 三处 + * 的错误码定义合并为单一来源;新增错误码只需追加一项枚举值,避免分散维护造成的漏改。 + *

    + */ + private enum PlatformError { + QPS_DAILY_LIMIT("17", "日配额耗尽", + "本接口当日累计调用次数已触达平台日配额上限,本次识别请求被拒绝", + "请安排次日再试,或联系运营提升日调用配额"), + QPS_RATE_LIMIT("18", "并发限流", + "单位时间请求过于密集,已触发平台 QPS 限流,请拉长调用间隔后重试", + "请在客户端增加限流/退避策略,避免瞬时并发过高"), + QPS_TOTAL_EXHAUSTED("19", "总配额耗尽", + "平台侧总调用配额已耗尽,需运营侧扩容或次日再试", + "请联系运营确认平台总配额与续费状态"), + PARAM_FORMAT("100", "平台业务拒绝", + "提交的表单字段组合不符合驾驶证识别接口契约,请对照文档核对字段名与取值", + null), + AUTH_INVALID("110", "鉴权失效", + "平台鉴权凭证校验未通过,属服务端配置异常,需运维刷新令牌", + "属服务端鉴权配置问题,需技术支持检查平台密钥与令牌缓存"), + AUTH_EXPIRED("111", "鉴权失效", + "平台鉴权凭证已超过有效期,属服务端配置异常,需运维重新签发", + "属服务端令牌过期,需技术支持重新获取并刷新缓存"), + PARAM_ILLEGAL("216100", "影像不合规", + "存在非法或与接口不匹配的参数项", + null), + IMAGE_MISSING("216101", "影像不合规", + "影像入参缺失:未提供可用的 image 或 url 字段", + null), + PARAM_TOO_LONG("216103", "影像不合规", + "单个参数字段长度超限(常见于 url 过长或 base64 体积过大)", + null), + APP_NOT_ENABLED("216110", "影像不合规", + "当前应用未开通驾驶证识别能力或授权范围不包含本接口", + null), + IMAGE_EMPTY("216200", "影像不合规", + "解码后的图片数据为空,上传环节可能未完成或内容损坏", + "请重新上传图片,确认 base64 未截断、未混入换行或非法字符"), + IMAGE_FORMAT("216201", "影像不合规", + "图片编码格式不在允许列表,仅支持 jpg、jpeg、png、bmp", + "请将图片转为 jpg/jpeg/png/bmp 之一后重新编码上传"), + IMAGE_SIZE("216202", "影像不合规", + "像素尺寸或编码后体积不满足规范(边长 15~4096px,编码后≤4M)", + "请压缩或裁剪图片,保证最长边≤4096px、最短边≥15px,且 urlencode 后≤4M"), + IMAGE_BLUR("216203", "影像不合规", + "引擎无法从当前画面中稳定提取驾驶证文本,多见于模糊、过曝或非证件照", + null), // 建议带 sideLabel 动态拼接,见 suggestionOf + LAYOUT_FAIL("216630", "影像不合规", + "证件版面识别失败,建议重拍并保证四角完整、无遮挡", + "请平铺证件拍摄,避免手指遮挡关键字段;副页识别务必传 side=back"), + ENGINE_INTERNAL("282000", "引擎内部错误", + "平台识别引擎内部异常,属短暂性故障,可间隔数秒后重试", + "短暂性故障,建议 5~10 秒后单次重试,不宜连续轰炸接口"); + + private static final String IMAGE_PREFIX = "2162"; + private static final String DEFAULT_CATEGORY = "平台业务拒绝"; + private static final String IMAGE_CATEGORY = "影像不合规"; + private static final String DEFAULT_REASON = "平台返回了未在本地维护的错误码,需结合错误描述人工判读"; + private static final String DEFAULT_SUGGESTION = + "请依据错误描述调整入参或影像后重试;持续失败请附带 traceId 联系技术支持"; + + private static final Map BY_CODE; + static { + Map m = new HashMap<>(); + for (PlatformError e : EnumSet.allOf(PlatformError.class)) { + m.put(e.code, e); + } + BY_CODE = Collections.unmodifiableMap(m); + } + + final String code; + final String category; + final String reason; + final String suggestion; + + PlatformError(String code, String category, String reason, String suggestion) { + this.code = code; + this.category = category; + this.reason = reason; + this.suggestion = suggestion; + } + + static String categoryOf(String code) { + PlatformError e = BY_CODE.get(code); + if (e != null) { + return e.category; + } + if (code != null && code.startsWith(IMAGE_PREFIX)) { + return IMAGE_CATEGORY; + } + return DEFAULT_CATEGORY; + } + + static String reasonOf(String code) { + PlatformError e = BY_CODE.get(code); + return e != null ? e.reason : DEFAULT_REASON; + } + + static String suggestionOf(String code, String sideLabel) { + PlatformError e = BY_CODE.get(code); + if (e == IMAGE_BLUR) { + return "请在自然光下重拍,确保证面占画面主体且文字可辨;side=" + sideLabel; + } + if (e != null && e.suggestion != null) { + return e.suggestion; + } + return DEFAULT_SUGGESTION; } - return second; } }