提交修改
This commit is contained in:
parent
86d66f6d6b
commit
e081df0fbc
@ -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 类命名(建议,可放宽)
|
### 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 枚举
|
## 七、错误码 — 使用顶级枚举 BaiduOcrError(v2.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 新建接口 SOP(5 步)
|
||||||
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`(影像入参工具)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user