提交修改

This commit is contained in:
quyixiao 2026-06-09 01:28:56 +08:00
parent 86d66f6d6b
commit e081df0fbc

View File

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