ially if it merges an updated upstream into a topic branch.
This commit is contained in:
quyixiao 2026-06-13 01:11:32 +08:00
commit 965d206f5a
3 changed files with 354 additions and 79 deletions

View File

@ -0,0 +1,168 @@
package com.heyu.api.controller;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
/**
* 百度 OCR 平台全部错误码枚举
* <p>
* 数据来源<a href="https://cloud.baidu.com/doc/OCR/s/dk3h7y5vr">百度 OCR 错误码文档</a>
* </p>
* <p>
* 每个枚举项包含百度官方的 {@link #code}错误码{@link #errorMsg}英文错误信息
* {@link #description}中文描述方便本地校验场景仿照百度格式统一返回
* </p>
*
* <h3>使用示例</h3>
* <pre>
* BaiduOcrErrorCode errorCode = BaiduOcrErrorCode.EMPTY_IMAGE;
* result.setCode(errorCode.getCode());
* result.setMsg(errorCode.getErrorMsg());
*
* // 根据百度返回的错误码查找
* BaiduOcrErrorCode fromPlatform = BaiduOcrErrorCode.fromCode("216200");
* </pre>
*
* @author zhengli
* @since 20260610_zl
*/
public enum BaiduOcrErrorCode {
// ===================== 系统级错误1~19 =====================
UNKNOWN("1", "Unknown error", "未知错误,请再次请求,如果持续出现此类错误,请在控制台提交工单联系技术支持团队"),
SERVICE_UNAVAILABLE("2", "Service temporarily unavailable", "服务暂不可用,请再次请求,如果持续出现此类错误,请在控制台提交工单联系技术支持团队"),
UNSUPPORTED_METHOD("3", "Unsupported openapi method", "调用的API不存在请检查请求URL后重新尝试"),
CLUSTER_LIMIT("4", "Open api request limit reached", "集群超限额,请再次请求,如果持续出现此类错误,请在控制台提交工单联系技术支持团队"),
NO_PERMISSION("6", "No permission to access data", "无接口调用权限,创建应用时未勾选相关文字识别接口"),
IAM_CERT_FAILED("14", "IAM Certification failed", "IAM鉴权失败建议用户参照文档自查生成sign的方式是否正确"),
DAILY_LIMIT("17", "Open api daily request limit reached", "免费测试资源使用完毕,每天请求量超限额"),
QPS_LIMIT("18", "Open api qps request limit reached", "QPS超限额"),
TOTAL_LIMIT("19", "Open api total request limit reached", "请求总量超限额"),
// ===================== 鉴权错误100~111 =====================
INVALID_PARAMETER("100", "Invalid parameter", "无效的access_token参数token拉取失败"),
ACCESS_TOKEN_INVALID("110", "Access token invalid or no longer valid", "access_token无效"),
ACCESS_TOKEN_EXPIRED("111", "Access token expired", "access token过期"),
// ===================== 业务参数错误216xxx =====================
INVALID_PARAM("216100", "invalid param", "请求中包含非法参数,请检查后重新尝试"),
NOT_ENOUGH_PARAM("216101", "not enough param", "缺少必须的参数,请检查参数是否有遗漏"),
SERVICE_NOT_SUPPORT("216102", "service not support", "请求了不支持的服务请检查调用的url"),
PARAM_TOO_LONG("216103", "param too long", "请求中某些参数过长,请检查后重新尝试"),
APPID_NOT_EXIST("216110", "appid not exist", "appid不存在请重新核对信息"),
// ===================== 影像相关错误2162xx =====================
EMPTY_IMAGE("216200", "empty image", "图片为空,请检查后重新尝试"),
IMAGE_FORMAT_ERROR("216201", "image format error", "上传的图片格式错误,现阶段支持 PNG、JPG、JPEG、BMP"),
IMAGE_SIZE_ERROR("216202", "image size error", "上传的图片大小错误"),
INPUT_OVERSIZE("216205", "input oversize", "传入的请求体大小错误"),
// ===================== 文件/上传错误2163xx =====================
UPLOAD_FILE_ERROR("216306", "Upload file error", "上传文件失败,请检查提交请求接口的请求参数"),
IMAGE_ANALYSIS_ERROR("216307", "image analysis error", "请求数据解析异常,图片数据解析加载失败"),
PDF_FILE_NUM_EXCEED("216308", "Pdf_file_num exceeds the number of pdf pages", "参数pdf_file_num大于PDF文件实际页数"),
// ===================== 任务错误2164xx =====================
CREATE_TASK_FAILED("216401", "Create task failed", "提交请求失败"),
QUERY_TASK_FAILED("216402", "Query task failed", "获取结果失败"),
// ===================== PDF/配额错误2166xx =====================
CHECK_PDF_FAILED("216603", "Check pdf page num failed", "获取PDF文件页数失败"),
INSUFFICIENT_QUOTA("216604", "Insufficient available quota", "请求总量超限额"),
RECOGNIZE_ERROR("216630", "recognize error", "识别错误,请再次请求"),
RECOGNIZE_BANK_CARD_ERROR("216631", "recognize bank card error", "识别银行卡错误"),
RECOGNIZE_IDCARD_ERROR("216633", "recognize idcard error", "识别身份证错误"),
DETECT_ERROR("216634", "detect error", "检测错误,请再次请求"),
// ===================== 企业核验错误216600~216602 =====================
BUSINESS_VERIFY_FAILED("216600", "business verify failed", "企业核验相关服务请求失败"),
BUSINESS_VERIFY_EMPTY("216601", "business verify result empty", "企业核验相关服务查询成功,但无查询结果"),
BUSINESS_VERIFY_TIMEOUT("216602", "business verify timeout", "企业核验相关服务接口超时"),
// ===================== 引擎内部错误282xxx =====================
INTERNAL_ERROR("282000", "internal error", "服务器内部错误"),
MISSING_PARAMETERS("282003", "missing parameters", "请求参数缺失"),
BATCH_PROCESSING_ERROR("282005", "batch processing error", "处理批量任务时发生部分或全部错误"),
BATCH_TASK_LIMIT("282006", "batch task limit reached", "批量任务处理数量超出限制"),
IMAGE_TRANSCODE_ERROR("282100", "image transcode error", "图片压缩转码错误"),
TARGET_DETECT_ERROR("282102", "target detect error", "未检测到图片中识别目标"),
TEMPLATE_MATCH_ERROR("282103", "recognize error, failed to match the template", "图片目标识别错误"),
// ===================== URL 相关错误2821xx =====================
URLS_NOT_EXIT("282110", "urls not exit", "URL参数不存在请核对URL后再次提交"),
URL_FORMAT_ILLEGAL("282111", "url format illegal", "URL格式非法"),
URL_DOWNLOAD_TIMEOUT("282112", "url download timeout", "url下载超时"),
URL_RESPONSE_INVALID("282113", "url response invalid", "URL返回无效参数"),
URL_SIZE_ERROR("282114", "url size error", "URL长度超过1024字节或为0"),
IMAGE_FETCH_FAILED("282115", "image fetch failed", "通过URL获取图片失败"),
// ===================== 增值税发票验真错误282134 =====================
OFFICIAL_WEB_EXCEPTION("282134", "officialWeb service exception", "国税局端网络超时"),
// ===================== 异步任务错误2828xx =====================
REQUEST_ID_NOT_EXIST("282808", "request id not exist", "request id 不存在"),
RESULT_TYPE_ERROR("282809", "result type error", "返回结果请求错误"),
IMAGE_RECOGNIZE_ERROR("282810", "image recognize error", "图像识别错误"),
// ===================== 行驶证核验错误2821xx =====================
DRIVING_LICENSE_RESOURCE_OVERRUN("282160", "driving license backend resource overrun", "后端资源超限"),
DRIVING_LICENSE_TOO_FREQUENT("282161", "driving license requests too frequently", "请求过于频繁"),
;
private final String code;
private final String errorMsg;
private final String description;
private static final Map<String, BaiduOcrErrorCode> BY_CODE;
static {
Map<String, BaiduOcrErrorCode> m = new HashMap<>();
for (BaiduOcrErrorCode e : EnumSet.allOf(BaiduOcrErrorCode.class)) {
m.put(e.code, e);
}
BY_CODE = Collections.unmodifiableMap(m);
}
BaiduOcrErrorCode(String code, String errorMsg, String description) {
this.code = code;
this.errorMsg = errorMsg;
this.description = description;
}
public String getCode() {
return code;
}
public String getErrorMsg() {
return errorMsg;
}
public String getDescription() {
return description;
}
/**
* 根据百度错误码查找枚举值
*
* @param code 百度错误码
* @return 对应的枚举值未匹配时返回 null
*/
public static BaiduOcrErrorCode fromCode(String code) {
return BY_CODE.get(code);
}
}

View File

@ -3,6 +3,7 @@ package com.heyu.api.controller.doc;
import com.heyu.api.controller.AbstractRecognizeController; import com.heyu.api.controller.AbstractRecognizeController;
import com.heyu.api.controller.BaiduOcrError; import com.heyu.api.controller.BaiduOcrError;
import com.heyu.api.controller.BaiduOcrErrorCode;
import com.heyu.api.data.annotation.CacheResult; import com.heyu.api.data.annotation.CacheResult;
import com.heyu.api.data.annotation.EbAuthentication; import com.heyu.api.data.annotation.EbAuthentication;
import com.heyu.api.data.annotation.NotIntercept; import com.heyu.api.data.annotation.NotIntercept;
@ -17,7 +18,6 @@ import com.heyu.api.resp.doc.DocClassifyLocationResp;
import com.heyu.api.resp.doc.DocClassifyResp; import com.heyu.api.resp.doc.DocClassifyResp;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -48,8 +48,10 @@ import java.util.Map;
* *
* <h3>返回约定</h3> * <h3>返回约定</h3>
* <ul> * <ul>
* <li>一律 {@code R.ok()}入参校验失败平台异常识别结果为空等情况将说明写入 {@code msg} * <li>百度平台返回 {@code error_code} {@code R.code} 按百度原始错误码返回
* {@code data} 可能为空或字段不全对外不暴露上游平台字样日志内可排查</li> * {@code R.msg} 按百度原始 {@code error_msg} 返回</li>
* <li>本地校验不通过时仿照百度错误码返回对应的 {@code code} {@code msg}详细诊断信息仅写日志</li>
* <li>识别成功但结果为空等软失败场景一律 {@code R.ok()}说明写入 {@code msg}</li>
* <li>入参校验失败时<strong>未调用平台不计费</strong></li> * <li>入参校验失败时<strong>未调用平台不计费</strong></li>
* </ul> * </ul>
* *
@ -76,18 +78,14 @@ public class DocClassifyController extends AbstractRecognizeController {
@EbAuthentication(tencent = ApiConstants.TENCENT_AUTH) @EbAuthentication(tencent = ApiConstants.TENCENT_AUTH)
@PostMapping("/classify") @PostMapping("/classify")
@CacheResult @CacheResult
public R<DocClassifyResp> recognize(@RequestBody DocClassifyRequest request) { public R<DocClassifyResp> recognize(DocClassifyRequest request) {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
RecognizeContext ctx = null; RecognizeContext ctx = null;
try { try {
// ---------- 步骤参数校验不调平台不扣费 ---------- // ---------- 步骤参数校验不调平台不扣费 ----------
String validateError = validateRequest(request); R<DocClassifyResp> validateError = validateRequest(request);
if (validateError != null) { if (validateError != null) {
ResolvedImageInput validateImage = request != null return validateError;
? ImageInputUtils.resolve(request.getImageUrlOrBase64()) : null;
log.info("文档分类:参数检查没通过,接口仍返回成功并附带提示(还没调识别、不扣费)。{} 返回给客户:{}",
buildInputLogContext(request, validateImage), abbreviate(validateError, 120));
return okResult(null, validateError, null);
} }
// ---------- 步骤解析影像入参 & 构建上下文 ---------- // ---------- 步骤解析影像入参 & 构建上下文 ----------
@ -97,32 +95,58 @@ public class DocClassifyController extends AbstractRecognizeController {
// ---------- 步骤组装请求体 ---------- // ---------- 步骤组装请求体 ----------
String content = buildRequestContent(imageInput); String content = buildRequestContent(imageInput);
if (isBlank(content)) { if (isBlank(content)) {
log.error("文档分类:组装请求失败,请求里没带有效图片。{}。{}", String hint = formatHint(
ctx.imageInput.getType().getDesc(), ctx.inputLog);
return okResult(null, formatHint(
"报文组装异常", "报文组装异常",
"识别请求体在序列化后未包含任何影像载荷,平台侧无法受理本次识别", "识别请求体在序列化后未包含任何影像载荷,平台侧无法受理本次识别",
"请核对 imageUrlOrBase64 是否非空且为有效 Base64 或 HTTP(S) 链接", "请核对 imageUrlOrBase64 是否非空且为有效 Base64 或 HTTP(S) 链接",
"影像模式=" + ctx.imageInput.getType().name() "影像模式=" + ctx.imageInput.getType().name()
), null); );
log.error("文档分类:组装请求失败,请求里没带有效图片。{}。{}。详细诊断:{}",
ctx.imageInput.getType().getDesc(), ctx.inputLog, abbreviate(hint, 200));
return baiduErrorR(BaiduOcrErrorCode.EMPTY_IMAGE);
} }
// ---------- 步骤调用平台识别 ---------- // ---------- 步骤调用平台识别 ----------
Map<String, Object> platformResult = callPlatform(content, ctx); Map<String, Object> platformResult = callPlatform(content, ctx);
if (platformResult == null) { if (platformResult == null) {
return okResult(null, formatHint( String hint = formatHint(
"服务无回执", "服务无回执",
"识别指令已下发,但在约定时间内未收到平台可解析的 JSON 回执", "识别指令已下发,但在约定时间内未收到平台可解析的 JSON 回执",
"建议间隔 3~5 秒重试;若连续失败,请记录 traceId、调用时刻并联系技术支持", "建议间隔 3~5 秒重试;若连续失败,请记录 traceId、调用时刻并联系技术支持",
"影像模式=" + ctx.imageInput.getType().name() "影像模式=" + ctx.imageInput.getType().name()
), null); );
log.error("文档分类:平台无回执。{}。详细诊断:{}", ctx.inputLog, abbreviate(hint, 200));
return baiduErrorR(BaiduOcrErrorCode.INTERNAL_ERROR);
} }
// ---------- 步骤解析平台结果 构造响应对象 ---------- // ---------- 步骤解析平台结果 构造响应对象 ----------
DocClassifyResp data = buildResp(platformResult); DocClassifyResp data = buildResp(platformResult);
// ---------- 步骤百度返回错误码时按百度原始 code/msg 返回 ----------
Object errorCodeObj = platformResult.get("error_code");
if (errorCodeObj != null) {
String errorCode = String.valueOf(errorCodeObj);
Object errorMsgObj = platformResult.get("error_msg");
String errorMsg;
if (errorMsgObj != null) {
errorMsg = errorMsgObj.toString();
} else {
BaiduOcrErrorCode baiduCode = BaiduOcrErrorCode.fromCode(errorCode);
errorMsg = baiduCode != null ? baiduCode.getErrorMsg() : "未知错误";
}
// 详细诊断信息写入日志不返回给客户端
String hint = resolvePlatformHint(platformResult, ctx, data);
logRecognizeResult(ctx, platformResult, data, hint, start);
R<DocClassifyResp> result = new R<>();
result.setCode(errorCode);
result.setMsg(errorMsg);
result.setData(data != null ? data : new DocClassifyResp());
return result;
}
String hint = resolvePlatformHint(platformResult, ctx, data); String hint = resolvePlatformHint(platformResult, ctx, data);
// ---------- 步骤日志记录 & 返回 ---------- // ---------- 步骤日志记录 & 返回 ----------
logRecognizeResult(ctx, platformResult, data, hint, start); logRecognizeResult(ctx, platformResult, data, hint, start);
return okResult(null, hint, data); return okResult(null, hint, data);
@ -132,17 +156,19 @@ public class DocClassifyController extends AbstractRecognizeController {
? ImageInputUtils.resolve(request.getImageUrlOrBase64()) : null; ? ImageInputUtils.resolve(request.getImageUrlOrBase64()) : null;
String mode = ctx != null ? ctx.imageInput.getType().getDesc() String mode = ctx != null ? ctx.imageInput.getType().getDesc()
: (fallbackImage != null ? fallbackImage.getType().getDesc() : "未知"); : (fallbackImage != null ? fallbackImage.getType().getDesc() : "未知");
log.error("文档分类:程序运行出错,耗时 {} ms。{}。客户传的:{}。异常:{} - {}", String hint = formatHint(
cost, mode, buildInputLogContext(request, fallbackImage),
e.getClass().getSimpleName(),
e.getMessage() != null ? e.getMessage() : "无具体说明", e);
return okResult(null, formatHint(
"运行时故障", "运行时故障",
"服务端在处理分类流程时抛出未预期异常,识别结果不可用", "服务端在处理分类流程时抛出未预期异常,识别结果不可用",
"请勿重复高频重试;请保存 traceId、异常发生时间由技术支持结合堆栈进一步定位", "请勿重复高频重试;请保存 traceId、异常发生时间由技术支持结合堆栈进一步定位",
"异常类型=" + e.getClass().getSimpleName() "异常类型=" + e.getClass().getSimpleName()
+ (e.getMessage() != null ? ",摘要=" + e.getMessage() : "") + (e.getMessage() != null ? ",摘要=" + e.getMessage() : "")
), null); );
log.error("文档分类:程序运行出错,耗时 {} ms。{}。客户传的:{}。异常:{} - {}。详细诊断:{}",
cost, mode, buildInputLogContext(request, fallbackImage),
e.getClass().getSimpleName(),
e.getMessage() != null ? e.getMessage() : "无具体说明",
abbreviate(hint, 200), e);
return baiduErrorR(BaiduOcrErrorCode.INTERNAL_ERROR);
} }
} }
@ -241,30 +267,32 @@ public class DocClassifyController extends AbstractRecognizeController {
// ===================== 校验与上下文 ===================== // ===================== 校验与上下文 =====================
private String validateRequest(DocClassifyRequest request) { private R<DocClassifyResp> validateRequest(DocClassifyRequest request) {
if (request == null) { if (request == null) {
log.info("文档分类:没收到任何请求参数,直接拒绝,未调用识别、不扣费"); String hint = formatHint(
return formatHint(
"入参绑定失败", "入参绑定失败",
"控制器未接收到可绑定的请求对象,所有业务字段均为空", "控制器未接收到可绑定的请求对象,所有业务字段均为空",
"请确认使用 POST 提交Content-Type 为 application/json" "请确认使用 POST 提交Content-Type 为 application/json"
+ "字段名与接口文档一致imageUrlOrBase64", + "字段名与接口文档一致imageUrlOrBase64",
"绑定结果=DocClassifyRequest 为 null" "绑定结果=DocClassifyRequest 为 null"
); );
log.info("文档分类:没收到任何请求参数,直接拒绝,未调用识别、不扣费。详细诊断:{}",
abbreviate(hint, 200));
return baiduErrorR(BaiduOcrErrorCode.NOT_ENOUGH_PARAM);
} }
ImageInputUtils.ValidationResult imageValidation = ImageInputUtils.ValidationResult imageValidation =
ImageInputUtils.validate(request.getImageUrlOrBase64()); ImageInputUtils.validate(request.getImageUrlOrBase64());
if (!imageValidation.isValid()) { if (!imageValidation.isValid()) {
log.info("文档分类imageUrlOrBase64 校验未通过({}),未调用识别、不扣费。原因:{}。{}", String hint = formatHint(
imageValidation.getCategory(),
imageValidation.getReason(),
buildInputLogContext(request, null));
return formatHint(
imageValidation.getCategory(), imageValidation.getCategory(),
imageValidation.getReason(), imageValidation.getReason(),
imageValidation.getSuggestion(), imageValidation.getSuggestion(),
imageValidation.getDetail() imageValidation.getDetail()
); );
log.info("文档分类imageUrlOrBase64 校验未通过({}),未调用识别、不扣费。详细诊断:{}",
imageValidation.getCategory(), abbreviate(hint, 200));
BaiduOcrErrorCode baiduError = mapValidationToBaiduError(imageValidation.getCategory());
return baiduErrorR(baiduError);
} }
return null; return null;
} }
@ -343,6 +371,52 @@ public class DocClassifyController extends AbstractRecognizeController {
// ===================== 私有工具 ===================== // ===================== 私有工具 =====================
/**
* 构造百度风格错误响应 R
*
* @param errorCode 百度错误码枚举
* @return 带百度错误码/信息的 R 对象data 为空 DocClassifyResp
*/
private R<DocClassifyResp> baiduErrorR(BaiduOcrErrorCode errorCode) {
R<DocClassifyResp> result = new R<>();
result.setCode(errorCode.getCode());
result.setMsg(errorCode.getErrorMsg());
result.setData(new DocClassifyResp());
return result;
}
/**
* 将本地影像校验类别映射为百度 OCR 错误码枚举
*
* @param category ImageInputUtils.ValidationResult 中的 category
* @return 对应的百度错误码枚举
*/
private static BaiduOcrErrorCode mapValidationToBaiduError(String category) {
if (category == null) {
return BaiduOcrErrorCode.INVALID_PARAM;
}
switch (category) {
case "影像源缺失":
case "Base64 内容过短":
return BaiduOcrErrorCode.EMPTY_IMAGE;
case "链接协议无效":
case "链接解析失败":
case "链接缺少协议头":
return BaiduOcrErrorCode.URL_FORMAT_ILLEGAL;
case "链接过长":
return BaiduOcrErrorCode.URL_SIZE_ERROR;
case "影像格式无效":
case "Base64 前缀无效":
case "Base64 字符非法":
case "Base64 解码失败":
return BaiduOcrErrorCode.IMAGE_FORMAT_ERROR;
case "Base64 体积过大":
return BaiduOcrErrorCode.INPUT_OVERSIZE;
default:
return BaiduOcrErrorCode.INVALID_PARAM;
}
}
/** /**
* 判断分类结果是否为空 * 判断分类结果是否为空
* <p>doc_classify words_result List OCR 识别的 Map 结构不同 * <p>doc_classify words_result List OCR 识别的 Map 结构不同

View File

@ -2,6 +2,7 @@ package com.heyu.api.controller.doc;
import com.ApiInterfaceApplicationTests; import com.ApiInterfaceApplicationTests;
import com.TestConstant; import com.TestConstant;
import com.heyu.api.controller.BaiduOcrErrorCode;
import com.heyu.api.data.utils.R; import com.heyu.api.data.utils.R;
import com.heyu.api.request.doc.DocClassifyRequest; import com.heyu.api.request.doc.DocClassifyRequest;
import com.heyu.api.resp.doc.DocClassifyResp; import com.heyu.api.resp.doc.DocClassifyResp;
@ -26,33 +27,32 @@ public class DocClassifyControllerTest extends ApiInterfaceApplicationTests {
@Autowired @Autowired
private DocClassifyController docClassifyController; private DocClassifyController docClassifyController;
// ===================== 参数校验场景 ===================== // ===================== 参数校验场景仿照百度错误码返回 =====================
/** /**
* 假设(Given): 不传任何请求参数 * 假设(Given): 不传任何请求参数
* (When): 调用 classify 接口 * (When): 调用 classify 接口
* (Then): 返回 code=200data 不为 nullmsg 文档分类·入参绑定失败 * (Then): 返回 code=216101msg=not enough paramdata 为空 DocClassifyResp
*/ */
@Test @Test
void classifyRequestNullTest() { void classifyRequestNullTest() {
// Act // Act
R result = docClassifyController.recognize(null); R<DocClassifyResp> result = docClassifyController.recognize(null);
// Assert // Assert
assertNotNull(result, "返回结果不能为空"); assertNotNull(result, "返回结果不能为空");
assertEquals("200", result.getCode(), "接口应返回成功"); assertEquals(BaiduOcrErrorCode.NOT_ENOUGH_PARAM.getCode(), result.getCode(),
assertNotNull(result.getData(), "data 不能为 nulldefaultEmptyResp 生效)"); "缺少必须参数时返回百度错误码 216101");
assertEquals(BaiduOcrErrorCode.NOT_ENOUGH_PARAM.getErrorMsg(), result.getMsg(),
"msg 应为百度官方 error_msg");
assertNotNull(result.getData(), "data 不能为 null");
assertTrue(result.getData() instanceof DocClassifyResp, "data 类型应为 DocClassifyResp"); assertTrue(result.getData() instanceof DocClassifyResp, "data 类型应为 DocClassifyResp");
String msg = result.getMsg();
assertNotNull(msg, "msg 不应为 null");
assertTrue(msg.contains("【文档分类·入参绑定失败】"), "msg 应含入参绑定失败前缀");
assertNoUpstreamPlatformName(msg, "msg");
} }
/** /**
* 假设(Given): imageUrlOrBase64 为空字符串 * 假设(Given): imageUrlOrBase64 为空字符串
* (When): 调用 classify 接口 * (When): 调用 classify 接口
* (Then): 返回 code=200msg 文档分类·影像源缺失 * (Then): 返回 code=216200msg=empty image
*/ */
@Test @Test
void classifyImageEmptyTest() { void classifyImageEmptyTest() {
@ -61,22 +61,21 @@ public class DocClassifyControllerTest extends ApiInterfaceApplicationTests {
request.setImageUrlOrBase64(""); request.setImageUrlOrBase64("");
// Act // Act
R result = docClassifyController.recognize(request); R<DocClassifyResp> result = docClassifyController.recognize(request);
// Assert // Assert
assertNotNull(result, "返回结果不能为空"); assertNotNull(result, "返回结果不能为空");
assertEquals("200", result.getCode(), "接口应返回成功"); assertEquals(BaiduOcrErrorCode.EMPTY_IMAGE.getCode(), result.getCode(),
"影像源缺失时返回百度错误码 216200");
assertEquals(BaiduOcrErrorCode.EMPTY_IMAGE.getErrorMsg(), result.getMsg(),
"msg 应为 empty image");
assertNotNull(result.getData(), "data 不能为 null"); assertNotNull(result.getData(), "data 不能为 null");
String msg = result.getMsg();
assertNotNull(msg, "msg 不应为 null");
assertTrue(msg.contains("【文档分类·影像源缺失】"), "msg 应含影像源缺失前缀");
assertNoUpstreamPlatformName(msg, "msg");
} }
/** /**
* 假设(Given): imageUrlOrBase64 为非法 Base64 * 假设(Given): imageUrlOrBase64 为非法 Base64含非法字符
* (When): 调用 classify 接口 * (When): 调用 classify 接口
* (Then): 返回 code=200msg 含分类错误信息 * (Then): 返回 code=216201msg=image format error
*/ */
@Test @Test
void classifyInvalidBase64Test() { void classifyInvalidBase64Test() {
@ -85,22 +84,21 @@ public class DocClassifyControllerTest extends ApiInterfaceApplicationTests {
request.setImageUrlOrBase64("not_a_valid_base64!!!"); request.setImageUrlOrBase64("not_a_valid_base64!!!");
// Act // Act
R result = docClassifyController.recognize(request); R<DocClassifyResp> result = docClassifyController.recognize(request);
// Assert // Assert
assertNotNull(result, "返回结果不能为空"); assertNotNull(result, "返回结果不能为空");
assertEquals("200", result.getCode(), "接口应返回成功"); assertEquals(BaiduOcrErrorCode.IMAGE_FORMAT_ERROR.getCode(), result.getCode(),
"Base64 字符非法时返回百度错误码 216201");
assertEquals(BaiduOcrErrorCode.IMAGE_FORMAT_ERROR.getErrorMsg(), result.getMsg(),
"msg 应为 image format error");
assertNotNull(result.getData(), "data 不能为 null"); assertNotNull(result.getData(), "data 不能为 null");
String msg = result.getMsg();
assertNotNull(msg, "msg 不应为 null");
assertTrue(msg.startsWith("【文档分类·"), "msg 应含文档分类前缀");
assertNoUpstreamPlatformName(msg, "msg");
} }
/** /**
* 假设(Given): imageUrlOrBase64 为非法 URLftp 协议 * 假设(Given): imageUrlOrBase64 为非法 URLftp 协议
* (When): 调用 classify 接口 * (When): 调用 classify 接口
* (Then): 返回 code=200msg 含链接协议无效 * (Then): 返回 code=282111msg=url format illegal
*/ */
@Test @Test
void classifyInvalidUrlProtocolTest() { void classifyInvalidUrlProtocolTest() {
@ -109,15 +107,66 @@ public class DocClassifyControllerTest extends ApiInterfaceApplicationTests {
request.setImageUrlOrBase64("ftp://example.com/image.png"); request.setImageUrlOrBase64("ftp://example.com/image.png");
// Act // Act
R result = docClassifyController.recognize(request); R<DocClassifyResp> result = docClassifyController.recognize(request);
// Assert // Assert
assertNotNull(result, "返回结果不能为空"); assertNotNull(result, "返回结果不能为空");
assertEquals("200", result.getCode(), "接口应返回成功"); assertEquals(BaiduOcrErrorCode.URL_FORMAT_ILLEGAL.getCode(), result.getCode(),
"链接协议无效时返回百度错误码 282111");
assertEquals(BaiduOcrErrorCode.URL_FORMAT_ILLEGAL.getErrorMsg(), result.getMsg(),
"msg 应为 url format illegal");
assertNotNull(result.getData(), "data 不能为 null");
}
/**
* 假设(Given): 通过URL获取图片失败
* (When): 调用 classify 接口
* (Then): 返回 code=282115msg=image fetch failed
*/
@Test
void classifyUrlTooLongTest() {
// Arrange 构造超过 1024 字符的 URL
StringBuilder longUrl = new StringBuilder("https://example.com/");
for (int i = 0; i < 60; i++) {
longUrl.append("path_segment_").append(i).append("/");
}
longUrl.append("image.png");
DocClassifyRequest request = new DocClassifyRequest();
request.setImageUrlOrBase64(longUrl.toString());
// Act
R<DocClassifyResp> result = docClassifyController.recognize(request);
// Assert
assertNotNull(result, "返回结果不能为空");
assertEquals(BaiduOcrErrorCode.IMAGE_FETCH_FAILED.getCode(), result.getCode(),
"URL 过长时返回百度错误码 282115");
assertEquals(BaiduOcrErrorCode.IMAGE_FETCH_FAILED.getErrorMsg(), result.getMsg(),
"msg 应为 image fetch failed");
assertNotNull(result.getData(), "data 不能为 null");
}
/**
* 假设(Given): imageUrlOrBase64 Base64 内容过短
* (When): 调用 classify 接口
* (Then): 返回 code=216200msg=empty image
*/
@Test
void classifyBase64TooShortTest() {
// Arrange
DocClassifyRequest request = new DocClassifyRequest();
request.setImageUrlOrBase64("abc");
// Act
R<DocClassifyResp> result = docClassifyController.recognize(request);
// Assert
assertNotNull(result, "返回结果不能为空");
assertEquals(BaiduOcrErrorCode.EMPTY_IMAGE.getCode(), result.getCode(),
"Base64 内容过短时返回百度错误码 216200");
assertEquals(BaiduOcrErrorCode.EMPTY_IMAGE.getErrorMsg(), result.getMsg(),
"msg 应为 empty image");
assertNotNull(result.getData(), "data 不能为 null"); assertNotNull(result.getData(), "data 不能为 null");
String msg = result.getMsg();
assertTrue(msg.contains("【文档分类·"), "msg 应含文档分类前缀");
assertNoUpstreamPlatformName(msg, "msg");
} }
// ===================== 识别成功场景 ===================== // ===================== 识别成功场景 =====================
@ -134,14 +183,13 @@ public class DocClassifyControllerTest extends ApiInterfaceApplicationTests {
request.setImageUrlOrBase64(TestConstant.TARGET_IMAGE_URL); request.setImageUrlOrBase64(TestConstant.TARGET_IMAGE_URL);
// Act // Act
R result = docClassifyController.recognize(request); R<DocClassifyResp> result = docClassifyController.recognize(request);
// Assert // Assert
assertNotNull(result, "返回结果不能为空"); assertNotNull(result, "返回结果不能为空");
assertEquals("200", result.getCode(), "接口应返回成功"); assertEquals("200", result.getCode(), "接口应返回成功");
assertNotNull(result.getData(), "返回数据不能为空"); assertNotNull(result.getData(), "返回数据不能为空");
assertTrue(result.getData() instanceof DocClassifyResp, "data 类型应为 DocClassifyResp"); assertTrue(result.getData() instanceof DocClassifyResp, "data 类型应为 DocClassifyResp");
assertNoUpstreamPlatformName(result.getMsg(), "msg");
} }
/** /**
@ -156,26 +204,11 @@ public class DocClassifyControllerTest extends ApiInterfaceApplicationTests {
request.setImageUrlOrBase64(TestConstant.TARGET_IMAGE_BASE64); request.setImageUrlOrBase64(TestConstant.TARGET_IMAGE_BASE64);
// Act // Act
R result = docClassifyController.recognize(request); R<DocClassifyResp> result = docClassifyController.recognize(request);
// Assert // Assert
assertNotNull(result, "返回结果不能为空"); assertNotNull(result, "返回结果不能为空");
assertEquals("200", result.getCode(), "base64 传图应返回成功"); assertEquals("200", result.getCode(), "base64 传图应返回成功");
assertNotNull(result.getData(), "返回数据不能为空"); assertNotNull(result.getData(), "返回数据不能为空");
assertNoUpstreamPlatformName(result.getMsg(), "msg");
}
// ===================== 断言工具方法 =====================
/**
* 断言文本中不包含上游平台字样百度baidu
*/
private void assertNoUpstreamPlatformName(String text, String fieldName) {
if (text == null) {
return;
}
String lower = text.toLowerCase();
assertTrue(!lower.contains("百度") && !lower.contains("baidu") && !lower.contains("aliyun") && !lower.contains("tencent"),
fieldName + " 不应包含上游平台字样,实际值:" + text);
} }
} }