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 3d21719..366c6bb 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 @@ -11,6 +11,7 @@ import com.heyu.api.resp.car.RecognizeDriverLicenseBackResp; import com.heyu.api.resp.car.RecognizeDriverLicenseFaceResp; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -72,10 +73,11 @@ public class RecognizeDriverLicenseController extends BaseController { */ @EbAuthentication(tencent = ApiConstants.TENCENT_AUTH) @PostMapping("/recognize") - public R recognize(DriverLicenseRecognizeRequest request) { + public R recognize(@RequestBody DriverLicenseRecognizeRequest request) { long start = System.currentTimeMillis(); RecognizeContext ctx = null; try { + // ---------- 步骤①:参数校验(不调百度、不扣费) ---------- String validateError = validateRequest(request); if (validateError != null) { log.info("驾驶证识别:参数检查没通过,直接返回错误(还没调识别、不扣费)。{} 返回给客户:{}", @@ -83,11 +85,13 @@ public class RecognizeDriverLicenseController extends BaseController { return R.error(validateError); } + // ---------- 步骤②:构建上下文(side、图片模式、入参摘要) ---------- ctx = new RecognizeContext( resolveDrivingLicenseSide(request), ImageMode.of(request), buildInputLogContext(request)); + // ---------- 步骤③:组装百度 API 请求体 ---------- String content = buildRequestContent(request, ctx.side); if (isBlank(content)) { log.error("驾驶证识别:组装请求失败,请求里没带有效图片。识别{},{}。{}", @@ -101,6 +105,7 @@ public class RecognizeDriverLicenseController extends BaseController { ), null); } + // ---------- 步骤④:调用百度平台识别 ---------- Map platformResult = callPlatform(content, ctx); if (platformResult == null) { return okResult(ctx.side, formatHint( @@ -111,11 +116,13 @@ public class RecognizeDriverLicenseController extends BaseController { ), null); } + // ---------- 步骤⑤:解析平台结果 → 根据 side 构建正页/副页响应 ---------- Object data = ApiConstants.back.equals(ctx.side) ? buildBackResp(platformResult) : buildFaceResp(platformResult); String hint = resolvePlatformHint(platformResult, ctx, data); + // ---------- 步骤⑥:日志记录 & 返回 ---------- logRecognizeResult(ctx, platformResult, data, hint, start); return okResult(ctx.side, hint, data); @@ -141,6 +148,14 @@ public class RecognizeDriverLicenseController extends BaseController { // ===================== 流程拆分方法 ===================== + /** + * 调用百度云驾驶证识别接口,记录调用日志并返回平台原始响应。 + * + * @param content 已拼装好的 POST 请求体 + * @param ctx 上下文(side、图片模式、入参摘要) + * @return 平台返回的 JSON 解析后的 Map;超时/鉴权/解析失败时返回 null + * @see #requestBaidu(String, String) + */ private Map callPlatform(String content, RecognizeContext ctx) { int len = content.length(); log.info("驾驶证识别:开始调用平台识别。识别{},{},请求大小约 {} 字节。{}", @@ -154,6 +169,21 @@ public class RecognizeDriverLicenseController extends BaseController { return result; } + /** + * 解析平台返回结果,判断是否需要向客户返回提示信息。 + *

判断顺序:

+ *
    + *
  1. error_code 存在 → 平台业务拒绝(配额/鉴权/影像不合规)
  2. + *
  3. words_result 为空 → 平台未识别出内容
  4. + *
  5. 响应对象所有字段均为空 → 平台有返回但字段映射后全为空
  6. + *
  7. 以上均不命中 → 返回 null(识别正常,无需额外提示)
  8. + *
+ * + * @param platformResult 百度平台原始返回 + * @param ctx 识别上下文 + * @param data 映射后的响应对象 + * @return 提示文案,正常识别返回 null + */ private String resolvePlatformHint(Map platformResult, RecognizeContext ctx, Object data) { Object errorCodeObj = platformResult.get("error_code"); if (errorCodeObj != null) { @@ -201,6 +231,16 @@ public class RecognizeDriverLicenseController extends BaseController { return null; } + /** + * 记录本次识别的最终结果日志(含耗时、识别字段数量)。 + * 区分"带提示"与"完全成功"两种日志级别,方便后续检索。 + * + * @param ctx 识别上下文 + * @param platformResult 平台原始返回(用于提取回执摘要) + * @param data 映射后的响应对象 + * @param hint 提示文案(可为 null) + * @param start 起始时间戳 ms,用于计算总耗时 + */ private void logRecognizeResult(RecognizeContext ctx, Map platformResult, Object data, String hint, long start) { long cost = System.currentTimeMillis() - start; @@ -259,6 +299,18 @@ public class RecognizeDriverLicenseController extends BaseController { return null; } + /** + * 解析 side 参数为标准化的正页/副页标识。 + *
    + *
  • 空/未传 → 默认正页 ({@link ApiConstants#front})
  • + *
  • face 或 front → 正页
  • + *
  • back → 副页
  • + *
  • 其他 → {@code null}(表示无效)
  • + *
+ * + * @param request 入参对象 + * @return {@link ApiConstants#front}、{@link ApiConstants#back} 或 {@code null} + */ private String resolveDrivingLicenseSide(DriverLicenseRecognizeRequest request) { if (request == null || isBlank(request.getSide())) { return ApiConstants.front; @@ -273,16 +325,39 @@ public class RecognizeDriverLicenseController extends BaseController { return null; } + /** + * 获取 side 对应的用户可读标签(带取值说明),用于日志与提示文案。 + * + * @param side 标准化后的 side(front / back) + * @return 如 "副页(back)" 或 "正页(front/face)" + */ private String sideLabel(String side) { return ApiConstants.back.equals(side) ? "副页(back)" : "正页(front/face)"; } + /** + * 获取 side 对应的中文描述,用于日志输出。 + * + * @param side 标准化后的 side(front / back) + * @return 如 "驾驶证副页" 或 "驾驶证正页" + */ private String sideDesc(String side) { return ApiConstants.back.equals(side) ? "驾驶证副页" : "驾驶证正页"; } // ===================== 请求/响应构造 ===================== + /** + * 构造调用百度驾驶证的 POST 请求体(application/x-www-form-urlencoded 格式)。 + *
    + *
  • 优先使用 Base64(image 参数),备选 URL(url 参数)
  • + *
  • 始终携带固定的百度参数:detect_direction、driving_license_side、unified_valid_period 等
  • + *
+ * + * @param request 客户入参 + * @param drivingLicenseSide 标准化后的 side(front / back) + * @return 请求体字符串;若既无 Base64 也无 URL 则返回空字符串 + */ private String buildRequestContent(DriverLicenseRecognizeRequest request, String drivingLicenseSide) { StringBuilder sb = new StringBuilder(); if (StringUtils.isNotBlank(request.getImageBase64())) { @@ -298,6 +373,18 @@ public class RecognizeDriverLicenseController extends BaseController { return sb.toString(); } + /** + * 统一构造接口返回体。 + *
    + *
  • 当 data 为 null 时,根据 side 自动创建对应类型的空响应对象(保证字段不为 null)
  • + *
  • hint 非空时追加到 R.ok() 的 msg 中,为空则使用默认成功消息
  • + *
+ * + * @param drivingLicenseSide 标准化 side,决定空响应类型 + * @param hint 提示文案(成功时可为 null) + * @param data 识别对象,可为 null + * @return 统一响应体 + */ private R okResult(String drivingLicenseSide, String hint, Object data) { if (data == null) { data = ApiConstants.back.equals(drivingLicenseSide) @@ -308,6 +395,14 @@ public class RecognizeDriverLicenseController extends BaseController { return result.setData(data); } + /** + * 将平台返回的 words_result 映射为驾驶证正页响应对象 {@link RecognizeDriverLicenseFaceResp}。 + * 逐字段从嵌套 Map 中提取:words_result.{字段名}.words + * + * @param data 百度平台返回的原始结果 + * @return 正页响应对象 + * @see #getWords(Map, String) + */ private RecognizeDriverLicenseFaceResp buildFaceResp(Map data) { RecognizeDriverLicenseFaceResp resp = new RecognizeDriverLicenseFaceResp(); resp.setLicenseNumber(getWords(data, "证号")); @@ -330,6 +425,13 @@ public class RecognizeDriverLicenseController extends BaseController { return resp; } + /** + * 将平台返回的 words_result 映射为驾驶证副页响应对象 {@link RecognizeDriverLicenseBackResp}。 + * 副页仅包含 姓名、记录、证号、档案编号 四个字段。 + * + * @param data 百度平台返回的原始结果 + * @return 副页响应对象 + */ private RecognizeDriverLicenseBackResp buildBackResp(Map data) { RecognizeDriverLicenseBackResp resp = new RecognizeDriverLicenseBackResp(); resp.setName(getWords(data, "姓名")); @@ -339,10 +441,26 @@ public class RecognizeDriverLicenseController extends BaseController { return resp; } + /** + * 从 Baidu 返回结果的 words_result 中提取指定字段的文本。 + *

访问路径:words_result.{field}.words

+ * + * @param data Baidu 返回的原始 Map + * @param field 字段中文名(如 "证号"、"姓名") + * @return 识别的文字内容,未命中或为空时返回 null + */ private String getWords(Map data, String field) { return MapUtils.getByExpr(data, "words_result." + field + ".words"); } + /** + * 取第一个非空字符串,均空时返回 null。 + * 用于处理百度返回中字段名不一致的场景(如有效期限可能叫"有效起始日期"也可能叫"有效期限")。 + * + * @param first 优先值 + * @param second 备选值 + * @return 非空字符串或 null + */ private String firstNonBlank(String first, String second) { return StringUtils.isNotBlank(first) ? first : second; } @@ -384,10 +502,31 @@ public class RecognizeDriverLicenseController extends BaseController { return count; } + /** + * 判断响应对象的所有 String 字段是否均为空。 + * 快速检查是否完全没有识出任何字段,用于决定是否追加"字段映射为空"提示。 + * + * @param data 正页或副页响应对象 + * @return 所有 String 字段均空则返回 true + * @see #countNonBlankStringFields(Object) + */ private static boolean isAllStringFieldsBlank(Object data) { return countNonBlankStringFields(data) == 0; } + /** + * 格式化为统一的提示文案,由四部分拼接而成: + *
+     * 【驾驶证识别·{category}】{reason}|定位信息:{detail}|处置指引:{suggestion}
+     * 
+ * detail 可空,空时跳过"定位信息"部分。 + * + * @param category 问题分类(如"影像不合规"、"鉴权失效") + * @param reason 问题原因描述 + * @param suggestion 处置建议 + * @param detail 定位辅助信息(可为空) + * @return 格式化的提示字符串 + */ private String formatHint(String category, String reason, String suggestion, String detail) { StringBuilder sb = new StringBuilder(); sb.append("【驾驶证识别·").append(category).append("】").append(reason); @@ -398,6 +537,14 @@ public class RecognizeDriverLicenseController extends BaseController { return sb.toString(); } + /** + * 截断字符串至指定长度,超出部分以 "..." 替代。 + * 防止日志或提示文案中出现超长文本(如 Base64 片段、详细堆栈)。 + * + * @param text 原始字符串 + * @param maxLen 最大保留字符数 + * @return 截断后的字符串,null 或未超长则返回原值 + */ private String abbreviate(String text, int maxLen) { if (text == null || text.length() <= maxLen) { return text; @@ -442,6 +589,13 @@ public class RecognizeDriverLicenseController extends BaseController { logId != null ? logId : "无"); } + /** + * 获取字符串长度,null 安全。 + * 用于 buildInputLogContext 中简述客户传入的 Base64 / URL 大小,避免打印全文。 + * + * @param value 字符串 + * @return 长度;null 返回 0 + */ private int textLength(String value) { return value == null ? 0 : value.length(); }