diff --git a/api-web/api-interface/src/main/java/com/heyu/api/controller/car/RecognizeDriverLicense/CODE_SPECIFICATION.md b/api-web/api-interface/src/main/java/com/heyu/api/controller/car/RecognizeDriverLicense/CODE_SPECIFICATION.md index 33f1f9e..5f2dbf2 100644 --- a/api-web/api-interface/src/main/java/com/heyu/api/controller/car/RecognizeDriverLicense/CODE_SPECIFICATION.md +++ b/api-web/api-interface/src/main/java/com/heyu/api/controller/car/RecognizeDriverLicense/CODE_SPECIFICATION.md @@ -1,8 +1,41 @@ -# 驾驶证识别接口 — 代码规范 +# OCR 识别类接口 — 代码规范 -> 基于 `RecognizeDriverLicenseController.java` 沉淀的工程规范,适用于本仓库内所有同类"对接第三方 AI 平台 + 提供统一 RESTful 接口"的 Controller。 +> 基于 `RecognizeDriverLicenseController.java` + `RecognizeDrivingLicenseController.java` 沉淀的工程规范, +> 适用于本仓库内所有 **对接百度 OCR 平台 + 提供统一 RESTful 接口** 的 Controller。 > -> 新增同类接口(行驶证识别、银行卡识别、人脸识别 …)请严格参照本规范,确保整个 OCR/识别族接口在 **返回协议、日志结构、错误处理、字段映射** 上保持一致。 +> 新增同类接口(身份证、银行卡、护照、营业执照 …)请严格参照本规范, +> 确保整个识别族接口在 **继承结构、返回协议、日志结构、错误处理、字段映射** 上保持一致。 +> +> **文档版本**:v2.0(适配 `AbstractRecognizeController` + `BaiduOcrError` 父类抽取后的架构) + +--- + +## 〇、架构总览(v2.0 必读) + +``` +BaseController ← 通用:requestBaidu / 鉴权 / Redis + └─ AbstractRecognizeController ← OCR 识别公共父类(本次新增) + │ • okResult / defaultEmptyResp 钩子 + │ • getWords / isWordsResultEmpty / buildPlatformReceiptSummary + │ • countNonBlankStringFields / isAllStringFieldsBlank(反射) + │ • abbreviate / firstNonBlank / textLength + │ • RecognizeContext(protected static) + │ + ├─ RecognizeDriverLicenseController ← 参考标杆①(驾驶证) + ├─ RecognizeDrivingLicenseController ← 参考标杆②(行驶证) + └─ 新增的 XxxController ← 按本规范编写 + +通用资源: + • BaiduOcrError ← 百度 OCR 错误码字典(top-level public enum) + • ImageInputUtils ← 影像入参解析/校验(resolve / validate) + • R / ApiR ← 统一返回包装 +``` + +**关键变化(vs v1.0)**: +1. 所有 OCR Controller **必须** 继承 `AbstractRecognizeController`,不再直接 `extends BaseController`。 +2. 错误码 **不再** 在每个 Controller 内重复定义;统一使用顶级枚举 `BaiduOcrError`。 +3. 影像入参 **必须** 走 `ImageInputUtils`,禁止 Controller 内自己判断 `imageUrl` / `imageBase64`。 +4. 请求对象只暴露 **两个字段**:`imageUrlOrBase64` + `side`,由服务端自动识别 URL/Base64。 --- @@ -10,21 +43,21 @@ ### 1.1 类命名(建议,可放宽) - Controller 命名以 **能清晰表达业务含义** 为准,最低要求:以 `Controller` 结尾、首字母大写驼峰。 - - ✅ 推荐:`{业务对象}Controller`,如 `DriverLicenseController`、`IdCardController`、`BankCardController` + - ✅ 推荐:`{业务对象}Controller`,如 `IdCardController`、`BankCardController` - ✅ 也接受:`Recognize{业务对象}Controller`,如 `RecognizeDriverLicenseController`(旧代码兼容) - ❌ 避免:`DriverLicenseCtrl`、`driverlicenseController`、`DriverlicenseAPI` -- 请求对象:建议以 `Request` 结尾,如 `DriverLicenseRecognizeRequest`、`IdCardRequest`,统一放在 `com.heyu.api.request.{domain}` 包下。 +- 请求对象:以 `Request` 结尾,统一放在 `com.heyu.api.request.{domain}` 包下。 - 响应对象按业务页面/场景拆分,命名以 `Resp` 或 `Response` 结尾即可: - 单一响应:`{业务对象}Resp`,如 `BankCardResp` - - 多响应(如正/副页):`{业务对象}{Side}Resp`,如 `RecognizeDriverLicenseFaceResp`、`RecognizeDriverLicenseBackResp` -- 同一接口若存在多种响应结构,**不允许**用一个超大对象同时承载所有字段,应通过 `side`/`type` 参数路由到不同 Resp。 + - 多响应(如正/副页):`{业务对象}{Side}Resp` +- 同一接口若存在多种响应结构,**不允许**用一个超大对象承载所有字段,应通过 `side`/`type` 参数路由到不同 Resp。 ### 1.2 URI 命名 - 统一前缀 `/{domain}/{action}`,全小写、连字符可选,禁止驼峰。 - - ✅ `/driver/license/recognize` + - ✅ `/driver/license/recognize`、`/driving/license/recognize` - ❌ `/driverLicense/Recognize` - HTTP 方法固定为 `POST`,禁止用 GET 传递影像参数。 -- Content-Type 优先 `application/json`,需要兼容老调用方时同步支持 `application/x-www-form-urlencoded`。 +- Content-Type 推荐 `application/json` + `@RequestBody`。 ### 1.3 包目录约定 ``` @@ -39,88 +72,132 @@ controller/{domain}/{Object}/ ← 文档/测试报告/规范专 --- -## 二、Controller 必要结构 +## 二、请求对象规范(v2.0 强制) + +### 2.1 字段约束 +**只允许两个字段**: +```java +@Data +public class XxxRequest { + /** + * 影像入参(URL 或 Base64 二合一): + * - HTTP/HTTPS 图片链接(≤1024 字符) + * - 图片 Base64(可带 data:image/...;base64, 前缀) + * 服务端通过 ImageInputUtils 自动识别。 + */ + private String imageUrlOrBase64; + + /** + * 业务侧标识,如 face/front/back。建议设默认值。 + */ + private String side = "front"; +} +``` + +### 2.2 禁止 +- ❌ 不允许同时保留 `imageBase64` 与 `imageUrl` 两个字段(影像入参合并已沉淀) +- ❌ 不允许新增其他业务字段(如 `userId` / `merchantId`),如需上下文走鉴权/拦截层 + +--- + +## 三、Controller 必要结构 每个识别类 Controller **必须** 至少包含以下成员(顺序固定,便于阅读): -``` -class RecognizeXxxController extends BaseController { +```java +@Slf4j +@RestController +@RequestMapping("/xxx") +class XxxController extends AbstractRecognizeController { - // 1. 常量区(百度 URI、默认文案等) + // 1. 常量区 private static final String XXX_URI = "/rest/2.0/ocr/v1/xxx"; + private static final String SIDE_DEFAULT_HINT = "未传(将按正页处理)"; // 2. 主入口 recognize(...) + @EbAuthentication(tencent = ApiConstants.TENCENT_AUTH) @PostMapping("/recognize") public R recognize(@RequestBody XxxRequest request) { ... } - // 3. 流程拆分方法(按调用顺序排列) + // 3. 父类钩子(必须重写) + @Override + protected Object defaultEmptyResp(String side) { ... } + + // 4. 流程拆分方法(按调用顺序排列) private Map callPlatform(...) private String resolvePlatformHint(...) private void logRecognizeResult(...) - // 4. 校验与上下文 + // 5. 校验与上下文 private String validateRequest(...) private String resolveXxxSide(...) private String sideLabel(...) private String sideDesc(...) - // 5. 请求/响应构造 + // 6. 请求/响应构造 private String buildRequestContent(...) - private R okResult(...) private Xxx{Face|Back}Resp build{Face|Back}Resp(...) - private String getWords(...) - // 6. 通用工具(私有) - private boolean isWordsResultEmpty(...) - private static int countNonBlankStringFields(...) + // 7. 私有工具 private String formatHint(...) - private String abbreviate(...) private String buildInputLogContext(...) - private String buildPlatformReceiptSummary(...) - - // 7. 内部类型 - private static class RecognizeContext { ... } - private enum PlatformError { ... } } ``` -> **铁律**:`recognize()` 主方法必须保持在 80 行以内,所有复杂逻辑下沉到上述分区方法。 +> **铁律**:`recognize()` 主方法必须保持在 80 行以内;通用工具方法 **不要重复实现**,全部继承自父类。 + +### 3.1 父类提供的方法清单(不要在子类重复实现) + +| 方法 | 来源 | 用途 | +|---|---|---| +| `okResult(side, hint, data)` | `AbstractRecognizeController` | 统一返回构造,调用 `defaultEmptyResp` 钩子 | +| `getWords(data, field)` | 同上 | 百度 `words_result.{field}.words` 提取 | +| `isWordsResultEmpty(platformResult)` | 同上 | 判断百度结果是否空 | +| `buildPlatformReceiptSummary(platformResult)` | 同上 | 平台回执日志摘要 | +| `countNonBlankStringFields(data)` | 同上(static) | 反射统计非空 String 字段数 | +| `isAllStringFieldsBlank(data)` | 同上(static) | 判断响应对象是否全空 | +| `abbreviate(text, maxLen)` | 同上(static) | 字符串截断 | +| `firstNonBlank(first, second)` | 同上(static) | 二选一非空字符串 | +| `textLength(value)` | 同上(static) | null 安全长度 | +| `RecognizeContext` | 同上(inner class) | side / imageInput / inputLog 上下文 | +| `requestBaidu(uri, content)` | `BaseController` | 调用百度 API | +| `isBlank(value)` | `BaseController` | null 安全空串判断 | --- -## 三、返回协议(强制) +## 四、返回协议(强制) -### 3.1 统一约定 -- **入参绑定失败、参数校验失败、平台异常、识别结果为空** — 一律返回 `R.ok()`,错误信息写入 `msg`;`data` 不为 `null`,赋为空响应对象。 -- **仅在系统级不可恢复异常**(如反射失败、JVM OOM)时可以考虑 `R.error()`,但要保留 `traceId`。 +### 4.1 统一约定 +- **入参绑定失败、参数校验失败、平台异常、识别结果为空** — 一律返回 `R.ok()`,错误信息写入 `msg`;`data` 不为 `null`(由 `defaultEmptyResp` 钩子保证)。 +- **仅在系统级不可恢复异常**(如反射失败、JVM OOM)时可以考虑 `R.error()`。 - 返回结构: ```json { "code": "200", "msg": "...", "traceId": "...", "data": {...} } ``` -### 3.2 okResult() 模板 +### 4.2 defaultEmptyResp() 钩子(取代旧的 okResult 模板) +> v2.0 起,`okResult` 已下沉到父类;子类 **只需要** 重写空对象工厂: + ```java -private R okResult(String side, String hint, Object data) { - if (data == null) { - data = ApiConstants.back.equals(side) - ? new XxxBackResp() : new XxxFaceResp(); - } - R r = StringUtils.isNotBlank(hint) ? R.ok(hint) : R.ok(); - return r.setData(data); +@Override +protected Object defaultEmptyResp(String side) { + return ApiConstants.back.equals(side) + ? new XxxBackResp() : new XxxFaceResp(); } ``` -### 3.3 不暴露上游 +### 4.3 不暴露上游 - `msg`、`data`、`code` 中 **禁止** 出现"百度"、"baidu"、"aliyun"、"tencent" 等上游平台字样。 - 日志中(`log.info/error`)可以出现"平台"二字,便于内部排查,但绝不写入返回体。 --- -## 四、recognize() 主流程模板(6 个步骤) +## 五、recognize() 主流程模板(6 个步骤) -所有识别接口主入口必须严格按以下 6 步执行: +所有识别接口主入口必须严格按以下 6 步执行(与 `RecognizeDriverLicenseController` / `RecognizeDrivingLicenseController` 完全一致): ```java +@EbAuthentication(tencent = ApiConstants.TENCENT_AUTH) @PostMapping("/recognize") public R recognize(@RequestBody XxxRequest request) { long start = System.currentTimeMillis(); @@ -128,7 +205,15 @@ public R recognize(@RequestBody XxxRequest request) { try { // ① 参数校验(不调上游、不扣费) String validateError = validateRequest(request); - if (validateError != null) { return okResult(side, validateError, null); } + if (validateError != null) { + String side = request != null ? resolveXxxSide(request) : null; + if (side == null) side = ApiConstants.front; + ResolvedImageInput validateImage = request != null + ? ImageInputUtils.resolve(request.getImageUrlOrBase64()) : null; + log.info("Xxx识别:参数检查没通过,... {} 返回给客户:{}", + buildInputLogContext(request, validateImage), abbreviate(validateError, 120)); + return okResult(side, validateError, null); + } // ② 解析影像入参 & 构建上下文 ResolvedImageInput imageInput = ImageInputUtils.resolve(request.getImageUrlOrBase64()); @@ -153,6 +238,7 @@ public R recognize(@RequestBody XxxRequest request) { return okResult(ctx.side, hint, data); } catch (Exception e) { + // 统一兜底:side / imageMode 回退取值,返回运行时故障 hint return okResult(side, 运行时故障 hint, null); } } @@ -162,67 +248,64 @@ public R recognize(@RequestBody XxxRequest request) { --- -## 五、参数校验规范 +## 六、参数校验规范 -### 5.1 校验顺序 +### 6.1 校验顺序 1. `request == null` → "入参绑定失败" -2. 影像入参校验(委托给 `ImageInputUtils.validate(...)`) +2. 影像入参校验 **必须** 走 `ImageInputUtils.validate(request.getImageUrlOrBase64())` 3. `side` 校验(`resolveXxxSide(request) == null` → "页面标识无效") 4. 业务字段校验(按需追加) -### 5.2 校验失败处理 -- **必须** 返回 `formatHint(category, reason, suggestion, detail)` 的结构化文案;禁止裸返回 `"参数错误"` 之类的话。 +### 6.2 校验失败处理 +- **必须** 返回 `formatHint(category, reason, suggestion, detail)` 的结构化文案;禁止裸返回 `"参数错误"`。 +- **必须** 走 `okResult(side, hint, null)`,不要用 `R.error(...)`。 - **必须** `log.info` 记录"已拒绝、未调用识别、不扣费",便于运维区分软失败与硬失败。 -### 5.3 `formatHint` 四要素 +### 6.3 `formatHint` 四要素 ``` -【驾驶证识别·{category}】{reason}|定位信息:{detail}|处置指引:{suggestion} +【Xxx识别·{category}】{reason}|定位信息:{detail}|处置指引:{suggestion} ``` - `category`:问题分类(≤8 个汉字),如"影像不合规"、"鉴权失效" - `reason`:失败原因(一句话说清是什么) - `suggestion`:处置建议(告诉调用方下一步怎么办) - `detail`:定位信息(可空,附带 side、错误码、字段名等便于排查) +> `formatHint` 的前缀 `【Xxx识别·...】` 是子类业务名,留在子类自行实现。 + --- -## 六、错误码与 PlatformError 枚举 +## 七、错误码 — 使用顶级枚举 BaiduOcrError(v2.0 重大变化) -### 6.1 单一来源 -**禁止** 将错误码定义分散到 Map、switch、if-else 三处。**必须** 使用单一 `enum PlatformError` 集中维护: +### 7.1 单一来源 +**禁止** 在 Controller 内嵌入 `PlatformError` 内部枚举。**必须** 直接使用顶级枚举 `com.heyu.api.controller.BaiduOcrError`: ```java -private enum PlatformError { - CODE_XX("17", "日配额耗尽", "原因...", "建议..."), - ...; - final String code, category, reason, suggestion; - - static String categoryOf(String code) { ... } - static String reasonOf(String code) { ... } - static String suggestionOf(String code, String sideLabel) { ... } -} +String category = BaiduOcrError.categoryOf(errorCode); +String reason = BaiduOcrError.reasonOf(errorCode); +String suggestion = BaiduOcrError.suggestionOf(errorCode, sideLabel(ctx.side)); ``` -### 6.2 新增错误码 SOP -1. 在枚举中追加一行 -2. 如该错误码属于某个前缀族(如 `2162xxx` → 影像不合规),在 `categoryOf` 的前缀分支中自动归类 +### 7.2 新增错误码 SOP +1. 在 `BaiduOcrError` 枚举中追加一行(不要在子类 Controller 中扩展) +2. 如属于某个前缀族(如 `2162xxx` → 影像不合规),`categoryOf` 自动归类 3. 如建议文案需要动态拼接 `sideLabel`,在 `suggestionOf` 中特判(参考 `IMAGE_BLUR`) -4. **不需要** 再改任何其他地方 +4. 所有 OCR Controller **同时受益**,不需要修改其他地方 -### 6.3 未知错误码兜底 +### 7.3 未知错误码兜底 - `categoryOf` 未匹配 → `"平台业务拒绝"` - `reasonOf` 未匹配 → `DEFAULT_REASON` - `suggestionOf` 未匹配 → `DEFAULT_SUGGESTION`(含"联系技术支持") --- -## 七、日志规范 +## 八、日志规范 -### 7.1 日志格式 +### 8.1 日志格式 - 一律使用 `@Slf4j`,禁止 `System.out.println` 与 `LoggerFactory.getLogger`。 - 必须使用占位符 `{}`,禁止 `+` 拼接(除非全是常量)。 - 每条业务日志必须包含:**业务名 + 处置动作 + side + imageMode + inputContext**。 -### 7.2 日志级别 +### 8.2 日志级别 | 场景 | 级别 | |---|---| | 参数校验未通过、调用前流程 | `INFO` | @@ -232,35 +315,34 @@ private enum PlatformError { | 字段映射为空、平台返回空 words_result | `INFO`(属业务层"软失败") | | 运行时未捕获异常 | `ERROR` + 堆栈 | -### 7.3 敏感信息保护 +### 8.3 敏感信息保护 - **禁止** 将 Base64 全文打印到日志。仅打印长度,例如:`"影像原始长度 23456 字符"`。 - **禁止** 打印用户姓名、身份证号、电话等 PII。如必须,用 `***` 中段脱敏。 - URL 可以完整打印,便于复现。 -### 7.4 三个标准日志构造方法 -所有识别接口都应包含以下三个方法,签名与语义保持一致: -- `buildInputLogContext(request, imageInput)` — 入参摘要 -- `buildPlatformReceiptSummary(platformResult)` — 平台回执摘要 -- `logRecognizeResult(ctx, platformResult, data, hint, start)` — 最终结果日志 +### 8.4 三个标准日志构造方法 +- `buildInputLogContext(request, imageInput)` — **子类实现**(涉及业务文案 sideDesc) +- `buildPlatformReceiptSummary(platformResult)` — **父类提供** +- `logRecognizeResult(ctx, platformResult, data, hint, start)` — **子类实现**(业务名 + 字段统计调父类) --- -## 八、上下文与不变性 +## 九、上下文与不变性 -### 8.1 RecognizeContext -- **必须** 提供 `RecognizeContext` 私有静态内部类,集中保存 `side` / `imageInput` / `inputLog`,避免主流程反复调用解析方法。 -- 字段必须 `final`;只通过构造器赋值。 -- **不允许** Context 内含可变集合。 +### 9.1 RecognizeContext(已下沉到父类) +- 直接使用父类提供的 `RecognizeContext`:`public final` 字段 `side` / `imageInput` / `inputLog` +- 主流程中 **必须** 通过 `ctx` 访问,禁止反复调用 `resolveXxxSide` / `ImageInputUtils.resolve` -### 8.2 ResolvedImageInput -- 影像入参解析统一委托 `ImageInputUtils.resolve(...)`,禁止 Controller 自己判断 `imageUrl` / `imageBase64`。 -- 影像类型通过 `imageInput.getType()` 枚举判断,禁止字符串比较(如 `mode.contains("Base64")`)。 +### 9.2 ResolvedImageInput +- 影像入参解析统一委托 `ImageInputUtils.resolve(...)` / `validate(...)`,**禁止** Controller 自己判断 `imageUrl` / `imageBase64` +- 影像类型通过 `imageInput.getType()` 枚举判断,**禁止** 字符串比较 +- 表单值通过 `imageInput.getFormValue()` 获取(已做 urlencode) --- -## 九、响应对象映射 +## 十、响应对象映射 -### 9.1 字段映射 +### 10.1 字段映射 - 通过 `getWords(data, "中文字段名")` 单次调用完成,禁止内联 `MapUtils.getByExpr` 表达式。 - 同一字段存在多个候选名时,使用 `firstNonBlank(getWords(...), getWords(...))`: ```java @@ -268,22 +350,23 @@ private enum PlatformError { getWords(data, "有效期限"))); ``` -### 9.2 响应类字段统计 -- **禁止** 在 Controller 内手工列举每个字段名做空判断。**必须** 使用反射工具: +### 10.2 响应类字段统计(父类反射工具) +- **禁止** 在 Controller 内手工列举每个字段名做空判断 +- **必须** 使用父类提供的反射工具: ```java - private static int countNonBlankStringFields(Object data) - private static boolean isAllStringFieldsBlank(Object data) + countNonBlankStringFields(data) // 统计非空 String 字段数 + isAllStringFieldsBlank(data) // 判断是否全空 ``` -- 响应类新增字段时,统计逻辑自动覆盖,无需修改 Controller。 +- 响应类新增字段时,统计逻辑自动覆盖,无需修改 Controller -### 9.3 响应类规范 -- 响应对象只允许包含 `String` 类型字段(数字日期同样以 String 返回),保证反射统计能覆盖。 -- 使用 `@Data` 注解;字段建议 `private`(与 Lombok 配合),不强制。 -- 每个字段必须有 Javadoc 说明 + 示例值。 +### 10.3 响应类规范 +- 响应对象 **只允许** 包含 `String` 类型字段(数字、日期同样以 String 返回),保证反射统计覆盖 +- 使用 `@Data` 注解;字段建议 `private`(与 Lombok 配合),不强制 +- 每个字段必须有 Javadoc 说明 + 示例值 --- -## 十、鉴权与拦截 +## 十一、鉴权与拦截 - **必须** 在 `recognize` 方法上添加 `@EbAuthentication(tencent = ApiConstants.TENCENT_AUTH)`。 - 跳过 `LogAop` 等需求请通过 `@NotIntercept` 显式声明,禁止悄悄绕过。 @@ -291,56 +374,84 @@ private enum PlatformError { --- -## 十一、单元测试要求 +## 十二、单元测试要求 -### 11.1 必测场景(每个识别接口) +### 12.1 必测场景(每个识别接口) | 用例分类 | 必测点 | |---|---| -| 参数校验 | request=null / 影像入参为空 / side 非法 / side 多种合法值 | -| 上游异常 | 平台返回 error_code(每个 PlatformError 至少 1 例) | +| 参数校验 | request=null / imageUrlOrBase64 为空 / 非法 / side 非法 / side 多种合法值 | +| 上游异常 | 平台返回 error_code(覆盖 `BaiduOcrError` 主要分类各 1 例) | | 识别空结果 | words_result 为空 / 字段全空 | | 识别成功 | 正页 / 副页 / 电子证(如有)各一例 | | 兜底 | 反射异常、运行时异常 | -### 11.2 断言 -- 必须断言 `R.code == "200"` 且 `data` 不为 null。 -- 必须断言 `msg` 不含上游平台字样。 -- 错误用例必须断言 `msg` 含 `【驾驶证识别·{category}】` 前缀。 +### 12.2 断言 +- 必须断言 `R.code == "200"` 且 `data` 不为 null(验证 `defaultEmptyResp` 生效) +- 必须断言 `msg` 不含上游平台字样 +- 错误用例必须断言 `msg` 含 `【Xxx识别·{category}】` 前缀 --- -## 十二、Code Review Checklist +## 十三、Code Review Checklist 提 PR 前自查: -- [ ] `recognize()` 方法 ≤ 80 行 -- [ ] 6 个步骤注释完整且顺序正确 +- [ ] **继承 `AbstractRecognizeController`**,不是 `BaseController` +- [ ] **使用 `BaiduOcrError`**,未在 Controller 内嵌套 `PlatformError` +- [ ] **未重复实现** 父类已提供的方法(okResult / getWords / isWordsResultEmpty 等) +- [ ] **请求对象只有两个字段**(imageUrlOrBase64 / side) +- [ ] **影像入参** 走 `ImageInputUtils`,未直接判断 `imageBase64`/`imageUrl` +- [ ] **重写 `defaultEmptyResp(side)`** 钩子,保证 data 非空 +- [ ] `recognize()` 方法 ≤ 80 行,6 个步骤注释完整 - [ ] 所有错误返回走 `okResult()` + `formatHint()` - [ ] `msg` / `data` 不含上游平台字样 -- [ ] 错误码统一在 `PlatformError` 枚举中维护 - [ ] 字段空判断使用反射工具,未手工列举 - [ ] 日志含 `side` / `imageMode` / `inputContext` 三件套 - [ ] Base64 / PII 未泄露到日志 - [ ] `@EbAuthentication` 已添加 -- [ ] 响应对象有默认实例(不为 null) - [ ] 单元测试覆盖必测场景 --- -## 十三、参考实现 +## 十四、参考实现 & 新接口落地步骤 -✅ **参考标杆**:`RecognizeDriverLicenseController.java` +### 14.1 参考标杆 +- ✅ `RecognizeDriverLicenseController.java`(驾驶证,face/front/back 三态 side) +- ✅ `RecognizeDrivingLicenseController.java`(行驶证,front/back 两态 side) -新增同类接口时,建议直接复制本类做骨架: -1. 全文替换 `DriverLicense` → 业务对象名 -2. 调整 `DRIVING_LICENSE_URI` 为目标 API 路径 -3. 重写 `buildFaceResp` / `buildBackResp` 的字段映射 -4. 按上游实际错误码增删 `PlatformError` 枚举项 -5. 其余结构保持不变 +### 14.2 新建接口 SOP(5 步) +1. **建请求对象**:`{Object}Request` — 两字段 +2. **建响应对象**:`{Object}FaceResp` / `{Object}BackResp`(单态则一个 `{Object}Resp`) +3. **复制 `RecognizeDrivingLicenseController` 作骨架**: + - 全文替换 `DrivingLicense` → 新业务对象名 + - 替换 `VehicleLicenseRequest` → `{Object}Request` + - 调整 `VEHICLE_LICENSE_URI` 为目标 API 路径 +4. **改 4 处业务代码**: + - `buildRequestContent`:百度该接口需要的额外参数(如 `id_card_side=...`) + - `buildFaceResp` / `buildBackResp`:字段映射 + - `defaultEmptyResp`:返回新 Resp 类型 + - `formatHint` / 日志 中 "行驶证识别" → "{业务名}识别" +5. **必要时扩展 `BaiduOcrError`**:如该接口出现新错误码,追加枚举值(所有接口共享) + +### 14.3 重构既有接口的对照表 + +| 旧代码 | 新代码 | +|---|---| +| `extends BaseController` | `extends AbstractRecognizeController` | +| 内嵌 `private enum PlatformError {...}` | 删除,引用 `BaiduOcrError` | +| 内嵌 `private static class RecognizeContext {...}` | 删除,直接用父类 | +| 自写 `okResult` / `getWords` / `isWordsResultEmpty` / ... | 全部删除 | +| `request.getImageBase64()` / `getImageUrl()` 双字段 | 合并为 `request.getImageUrlOrBase64()` | +| 自写 `ImageMode` 枚举 | 删除,用 `ResolvedImageInput.getType()` | --- -> **文档版本**:v1.0 +> **文档版本**:v2.0 > **维护人**:heyu > **最后更新**:2026-06-09 -> **关联代码**:`com.heyu.api.controller.car.RecognizeDriverLicenseController` +> **关联代码**: +> - `com.heyu.api.controller.AbstractRecognizeController`(公共父类) +> - `com.heyu.api.controller.BaiduOcrError`(错误码字典) +> - `com.heyu.api.controller.car.RecognizeDriverLicenseController`(标杆①) +> - `com.heyu.api.controller.car.RecognizeDrivingLicenseController`(标杆②) +> - `com.heyu.api.data.utils.ImageInputUtils`(影像入参工具)