feat(facebody): 实现人脸识别搜索的重试机制

- 添加可重试和不可重试异常分类
- 集成百度云错误码分类器
- 实现搜索人脸接口的自动重试逻辑
- 支持根据错误码动态调整重试次数和延迟
- 添加详细的异常日志记录
- 保持与原有逻辑一致的空结果返回行为
This commit is contained in:
2025-10-31 15:01:06 +08:00
parent 631d5c175f
commit 7c42c5c462
5 changed files with 814 additions and 5 deletions

View File

@@ -5,6 +5,10 @@ import com.ycwl.basic.facebody.entity.AddFaceResp;
import com.ycwl.basic.facebody.entity.BceFaceBodyConfig; import com.ycwl.basic.facebody.entity.BceFaceBodyConfig;
import com.ycwl.basic.facebody.entity.SearchFaceResp; import com.ycwl.basic.facebody.entity.SearchFaceResp;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem; import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.facebody.exceptions.BceErrorCodeClassifier;
import com.ycwl.basic.facebody.exceptions.FaceBodyException;
import com.ycwl.basic.facebody.exceptions.NonRetryableFaceBodyException;
import com.ycwl.basic.facebody.exceptions.RetryableFaceBodyException;
import com.ycwl.basic.utils.ratelimiter.FixedRateLimiter; import com.ycwl.basic.utils.ratelimiter.FixedRateLimiter;
import com.ycwl.basic.utils.ratelimiter.IRateLimiter; import com.ycwl.basic.utils.ratelimiter.IRateLimiter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -273,18 +277,71 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
@Override @Override
public SearchFaceResp searchFace(String dbName, String faceUrl) { public SearchFaceResp searchFace(String dbName, String faceUrl) {
int retryCount = 0;
while (true) {
try {
return doSearchFace(dbName, faceUrl);
} catch (RetryableFaceBodyException e) {
// 获取建议的最大重试次数
Integer maxRetries = BceErrorCodeClassifier.getSuggestedMaxRetries(e.getErrorCode());
if (maxRetries == null) {
maxRetries = 1; // 默认重试1次
}
if (retryCount >= maxRetries) {
log.error("搜索人脸重试{}次后仍失败,错误码: {}, 错误信息: {}",
retryCount, e.getErrorCode(), e.getMessage());
// 返回空结果而不是抛出异常,保持与原有逻辑一致
return null;
}
// 计算延迟时间
Long delay = BceErrorCodeClassifier.getSuggestedRetryDelay(e.getErrorCode(), retryCount);
if (delay == null) {
delay = 500L; // 默认延迟500ms
}
log.warn("搜索人脸失败[错误码: {}],{}ms后进行第{}次重试,错误信息: {}",
e.getErrorCode(), delay, retryCount + 1, e.getMessage());
try {
if (delay > 0) {
Thread.sleep(delay);
}
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
retryCount++;
} catch (NonRetryableFaceBodyException e) {
// 不可重试错误,直接返回
log.error("搜索人脸失败(不可重试),错误码: {}, 类别: {}, 错误信息: {}",
e.getErrorCode(), e.getCategory(), e.getMessage());
// 返回空结果而不是抛出异常,保持与原有逻辑一致
return null;
}
}
}
private SearchFaceResp doSearchFace(String dbName, String faceUrl) {
IRateLimiter searchFaceLimiter = getLimiter(LOCK_TYPE.SEARCH_FACE); IRateLimiter searchFaceLimiter = getLimiter(LOCK_TYPE.SEARCH_FACE);
SearchFaceResp resp = new SearchFaceResp(); SearchFaceResp resp = new SearchFaceResp();
try { try {
AipFace client = getClient(); AipFace client = getClient();
HashMap<String, Object> options = new HashMap<>(); HashMap<String, Object> options = new HashMap<>();
options.put("max_user_num", "50"); options.put("max_user_num", "50");
try { try {
searchFaceLimiter.acquire(); searchFaceLimiter.acquire();
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
} }
JSONObject response = client.search(faceUrl, "URL", dbName, options); JSONObject response = client.search(faceUrl, "URL", dbName, options);
if (response.getInt("error_code") == 0) { int errorCode = response.getInt("error_code");
if (errorCode == 0) {
resp.setOriginalFaceScore(100f); resp.setOriginalFaceScore(100f);
JSONObject resultObj = response.getJSONObject("result"); JSONObject resultObj = response.getJSONObject("result");
if (resultObj == null) { if (resultObj == null) {
@@ -308,12 +365,18 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
} }
return resp; return resp;
} else { } else {
resp.setOriginalFaceScore(0f); // 使用错误码分类器创建相应的异常
return resp; String errorMsg = response.optString("error_msg", "未知错误");
throw BceErrorCodeClassifier.createException(errorCode,
"人脸搜索失败[" + errorCode + "]: " + errorMsg);
} }
} catch (FaceBodyException e) {
// 重新抛出 FaceBodyException
throw e;
} catch (Exception e) { } catch (Exception e) {
log.error("搜索人脸失败!", e); // 其他异常(如网络异常)包装为可重试异常
return null; log.error("搜索人脸网络异常", e);
throw new RetryableFaceBodyException("搜索人脸网络异常: " + e.getMessage(), e);
} }
} }

View File

@@ -0,0 +1,405 @@
package com.ycwl.basic.facebody.exceptions;
import java.util.Set;
/**
* 百度云人脸识别错误码分类器
*
* <p>根据百度云人脸识别 API 错误码文档,对错误进行分类:
* <ul>
* <li>可重试错误:网络问题、限流、服务端临时故障等</li>
* <li>不可重试错误:参数错误、认证失败、资源不存在、业务规则违反等</li>
* </ul>
*
* <p>参考文档: https://cloud.baidu.com/doc/FACE/s/5k37c1ujz
*
* @see RetryableFaceBodyException
* @see NonRetryableFaceBodyException
*/
public class BceErrorCodeClassifier {
/**
* 可重试的错误码集合
*/
private static final Set<Integer> RETRYABLE_ERROR_CODES = Set.of(
// ========== 接口流控及鉴权错误码 ==========
2, // Service temporarily unavailable - 服务暂不可用
4, // Open api request limit reached - 集群超限额
17, // Open api daily request limit reached - 每天流量超限额
18, // Open api qps request limit reached - QPS超限额
19, // Open api total request limit reached - 请求总量超限额
110, // Access token invalid or no longer valid - Access Token失效
111, // Access token expired - Access token过期
// ========== 网络及服务端临时故障 ==========
222201, // network not available - 服务端请求失败
222204, // image_url_download_fail - 从图片的url下载图片失败
222205, // network not available - 服务端请求失败
222206, // rtse service return fail - 服务端请求失败
222302, // system error - 服务端请求失败
222301, // get face fail - 获取人脸图片失败
222303, // get face fail - 获取人脸图片失败
222361, // network not available - 公安服务连接失败
// ========== 系统繁忙 ==========
222901, 222902, 222903, 222904, 222905, 222906,
222907, 222908, 222909, 222910, 222911, 222912,
222913, 222914, 222915, 222916, // system busy - 系统繁忙相关
// ========== H5活体检测接口临时错误 ==========
216430, // rtse/face service error - rtse/face 服务异常
216431, // voice service error - 语音识别服务异常
216432, // video service call fail - 视频解析服务调用失败
216433, // video service error - 视频解析服务发生错误
216505, // redis connect error - redis连接失败
216506, // redis operation error - redis操作失败
216612, // system busy - 系统繁忙
// ========== H5方案临时错误 ==========
283400, // 服务异常,请稍后再试
283460, // 视频文件过大,核验请求超时
283438, // 视频转码失败,请重试
283436, // Token生成失败,请重试
283502, // 视频文件上传 bos 失败
283468, // BOS文件上传失败
283447 // 验证失败,请稍后重新尝试
);
/**
* 明确不可重试的错误码集合(参数错误、认证失败、资源不存在、业务规则违反等)
*/
private static final Set<Integer> NON_RETRYABLE_ERROR_CODES = Set.of(
// ========== 接口权限错误(认证失败) ==========
6, // no permission to access data - 没有接口权限
100, // Invalid parameter - 无效的access_token参数
// ========== 参数格式错误 ==========
222001, 222002, 222003, 222004, 222005, 222006, 222007, 222008, 222009, 222010,
222011, 222012, 222013, 222014, 222015, 222016, 222017, 222018, 222019, 222020,
222021, 222022, 222023, 222024, 222025, 222026, 222027, 222028, 222029, 222030,
222039, 222046, 222101, 222102, 222041, 222042, 222038, // param format error 系列
// ========== 图片相关错误 ==========
222200, // request body should be json format - 格式错误
222202, // pic not has face - 图片中没有人脸
222203, // image check fail - 无法解析人脸
222208, // the number of image is incorrect - 图片的数量错误
222213, // face size is too small - 人脸尺寸过小
222214, // face are cartoon images - 卡通图像
222215, // face quality is not acceptable - 人脸属性编辑处理失败
222304, // image size is too large - 图片尺寸太大
222305, // pic storage not support - 当前版本不支持图片存储
222307, // image illegal, reason: porn - 图片非法 鉴黄未通过
222308, // image illegal, reason: sensitive person - 图片非法 含有政治敏感人物
222309, // image size is too small - 图片尺寸过小
// ========== 人脸库管理错误(资源不存在/已存在) ==========
223100, // group is not exist - 操作的用户组不存在
223101, // group is already exist - 该用户组已存在
223102, // user is already exist - 该用户已存在
223103, // user is not exist - 找不到该用户
223105, // face is already exist - 该人脸已存在
223106, // face is not exist - 该人脸不存在
223111, // dst group not exist - 目标用户组不存在
223136, // images exist in this group - 该组内存在关联图片
223128, // group was deleting - 正在清理该用户组的数据
// ========== 业务规则违反 ==========
222104, // group_list is too large - group_list包含组数量过多
222110, // uid_list is too large - uid_list包含数量过多
222117, // app_list is too large - app_list包含app数量过多
222207, // match user is not found - 未找到匹配的用户
222209, // face token not exist - face token不存在
222210, // the number of user's faces is beyond the limit - 人脸数目超过限制
223107, // scene_type not same - 源组与目标组的scene_type不同
223112, // quality_conf format error - quality_conf格式不正确
223118, // quality control error - 质量控制项错误
223119, // liveness control item error - 活体控制项错误
223201, // param[scene_type] format error - scene_type格式错误
223202, // scene_type does not match - scene_type不匹配
// ========== 质量检测未通过(业务规则违反) ==========
223113, // face is covered - 人脸有被遮挡
223114, // face is fuzzy - 人脸模糊
223115, // face light is not good - 人脸光照不好
223116, // incomplete face - 人脸不完整
223129, // face not forward - 人脸未面向正前方
223121, 223122, 223123, 223124, 223125, 223126, 223127, // 各部位遮挡检测未通过
// ========== 活体检测未通过 ==========
223120, // liveness check fail - 活体检测未通过
223130, // spoofing_control item error - spoofing_control参数格式错误
223131, // spoofing check fail - 合成图检测未通过
223133, // video extract image liveness check fail - 视频提取图片活体检测失败
223052, // action identify fail - 视频中的动作验证未通过
// ========== 人脸融合错误 ==========
222211, // template image quality reject - 模板图质量不合格
222212, // merge face fail - 人脸融合失败
222300, // add face fail - 人脸图片添加失败
222514, // face editattrpro operation fail - 人脸属性编辑v2调用服务失败
222152, // param[target] format error - target参数错误
// ========== 人脸实名认证错误 ==========
222350, // police picture is none or low quality - 公安网图片不存在或质量过低
222351, // id number and name not match - 身份证号与姓名不匹配
222354, // id number not exist - 公安库里不存在此身份证号
222355, // police picture not exist - 公安库里没有对应的照片
222356, // person picture is low quality - 人脸图片质量不符合要求
222357, // picture file format error - 图片格式解析失败
222358, // trigger risk interception - 触发数据源风险拦截
282105, // image decrypt error - 图片解密失败
216201, // image format error - 图片格式失败
216100, // invalid param - 参数格式失败
282003, // missing required parameter(s) - 缺少必要参数
282000, // internal error - 服务器内部错误
216600, // 身份证号码格式错误
216601, // 身份证号和名字不匹配
222360, // 身份核验未通过
// ========== H5活体检测错误 ==========
216500, // code digit error - 验证码位数错误
216501, // not found face - 没有找到人脸
216502, // session lapse - 当前会话已失效
216508, // not found video info - 没有找到视频信息
216509, // voice can not identify - 视频中的声音无法识别
216510, // video time is too long - 视频长度超过10s
216511, // voice file error - 语音文件不符合要求
216512, // action verify must post session_id - 必须使用会话id
216513, // detect_model param error - 检测模型参数错误
216908, // 视频中人脸质量较差
216909, // video all image detect over two face - 人脸数超过2
223050, // voice similarity low error - 语音与验证码相似度过低
// ========== H5方案错误 ==========
200, // unsupported operation - 不支持的操作
283456, // 图片为空或格式不正确
283458, // 当前链接已失效
283459, // 请从手机端扫描二维码访问
216434, // 活体检测未通过
223051, // 唇语验证未通过
283738, // 颜色验证未通过
283457, // 当前环境存在安全风险
283501, // 安全检验未通过
283421, // 应用不存在
283437, // Token无效或已过期
283439, // STS_Token 已经生成
283464, // 非法流程
283461, // 人脸和对比源不匹配
283462, // 比对源配置错误
283463, // 人脸图片质量检测未通过
283465, // 人脸图片活体检测未通过
283467, // 该PLAN_ID下未查询到图片文件
283469, // 用户请求的 body 是空
283435, // 方案不存在
283440, // 身份证照片不符合要求
283442, // 身份证信息不合法
283443, // 不可使用语音验证码
283444, // 语音验证码生成失败
283448, // 语音验证码不符合要求
283449, // 活体检测视频不符合要求
283450, // 认证尚未开始
283451, // 认证处理中
283453, // 不可使用照片活体
283454, // 不可使用视频活体
283455, // 超出查询有效期
283503, // 对比源信息未传入
283504, // 请上传正确的身份证照片
283505, // 请上传正确的身份证人像面
283506, // 请上传正确的身份证国徽面
283507, // 不可使用身份证识别
283601, // 重复推送错误信息
300201, // 您已拒绝授权摄像头
300001, // 受当前环境限制
300002, // 受当前环境限制
999999, // 请确保是本人操作且正脸采集
800001, // 采集超时
800002 // 炫瞳检测失败
);
/**
* 判断错误码是否为可重试错误
*
* @param errorCode 百度云返回的错误码
* @return true 如果是可重试错误
*/
public static boolean isRetryable(Integer errorCode) {
if (errorCode == null) {
return false;
}
return RETRYABLE_ERROR_CODES.contains(errorCode);
}
/**
* 判断错误码是否为不可重试错误
*
* @param errorCode 百度云返回的错误码
* @return true 如果是不可重试错误
*/
public static boolean isNonRetryable(Integer errorCode) {
if (errorCode == null) {
return false;
}
return NON_RETRYABLE_ERROR_CODES.contains(errorCode);
}
/**
* 根据错误码和错误消息创建合适的异常
*
* @param errorCode 百度云返回的错误码
* @param errorMessage 错误消息
* @return 对应的异常对象
*/
public static FaceBodyException createException(Integer errorCode, String errorMessage) {
if (isRetryable(errorCode)) {
return new RetryableFaceBodyException(errorMessage, errorCode);
} else if (isNonRetryable(errorCode)) {
return new NonRetryableFaceBodyException(
errorMessage,
errorCode,
categorizeNonRetryableError(errorCode)
);
} else {
// 未知错误码,默认为不可重试
return new NonRetryableFaceBodyException(
errorMessage,
errorCode,
NonRetryableFaceBodyException.ErrorCategory.OTHER
);
}
}
/**
* 对不可重试错误进行分类
*
* @param errorCode 错误码
* @return 错误类别
*/
private static NonRetryableFaceBodyException.ErrorCategory categorizeNonRetryableError(Integer errorCode) {
if (errorCode == null) {
return NonRetryableFaceBodyException.ErrorCategory.OTHER;
}
// 认证/权限错误
if (errorCode == 6 || errorCode == 100) {
return NonRetryableFaceBodyException.ErrorCategory.AUTHENTICATION_ERROR;
}
// 参数验证错误
if ((errorCode >= 222001 && errorCode <= 222046) ||
(errorCode >= 222101 && errorCode <= 222102) ||
(errorCode >= 222041 && errorCode <= 222042) ||
errorCode == 222038 || errorCode == 216100 || errorCode == 282003) {
return NonRetryableFaceBodyException.ErrorCategory.VALIDATION_ERROR;
}
// 资源不存在
if (errorCode == 223100 || errorCode == 223103 || errorCode == 223106 ||
errorCode == 223111 || errorCode == 222207 || errorCode == 222209 ||
errorCode == 222354 || errorCode == 222355 || errorCode == 283435 ||
errorCode == 283467 || errorCode == 283421) {
return NonRetryableFaceBodyException.ErrorCategory.RESOURCE_NOT_FOUND;
}
// 数据冲突
if (errorCode == 223101 || errorCode == 223102 || errorCode == 223105 ||
errorCode == 223136 || errorCode == 283439 || errorCode == 283601) {
return NonRetryableFaceBodyException.ErrorCategory.CONFLICT;
}
// 不支持的操作
if (errorCode == 200 || errorCode == 222305 || errorCode == 283443 ||
errorCode == 283453 || errorCode == 283454 || errorCode == 283507) {
return NonRetryableFaceBodyException.ErrorCategory.UNSUPPORTED_OPERATION;
}
// 业务规则违反(质量检测、活体检测、人脸融合等)
if ((errorCode >= 223113 && errorCode <= 223131) ||
errorCode == 223133 || errorCode == 223052 || errorCode == 223120 ||
(errorCode >= 222202 && errorCode <= 222215) ||
errorCode == 222304 || errorCode == 222307 || errorCode == 222308 ||
errorCode == 222309 || errorCode == 222210 || errorCode == 222211 ||
errorCode == 222212 || errorCode == 222300 || errorCode == 222350 ||
errorCode == 222351 || errorCode == 222356 || errorCode == 222358 ||
errorCode == 216434 || errorCode == 216500 || errorCode == 216501 ||
errorCode == 216508 || errorCode == 216509 || errorCode == 216510 ||
errorCode == 216511 || errorCode == 216908 || errorCode == 216909 ||
errorCode == 223050 || errorCode == 223051 || errorCode == 283738 ||
errorCode == 283456 || errorCode == 283457 || errorCode == 283461 ||
errorCode == 283463 || errorCode == 283465 || errorCode == 283440 ||
errorCode == 283442 || errorCode == 283449 || errorCode == 800001 ||
errorCode == 800002 || errorCode == 999999 || errorCode == 216600 ||
errorCode == 216601 || errorCode == 222360) {
return NonRetryableFaceBodyException.ErrorCategory.BUSINESS_RULE_VIOLATION;
}
return NonRetryableFaceBodyException.ErrorCategory.OTHER;
}
/**
* 根据错误码获取建议的重试次数
*
* @param errorCode 错误码
* @return 建议的重试次数,null 表示使用默认值
*/
public static Integer getSuggestedMaxRetries(Integer errorCode) {
if (errorCode == null || !isRetryable(errorCode)) {
return 0;
}
// QPS/流量限制,建议重试次数较多
if (errorCode == 18 || errorCode == 17 || errorCode == 19 || errorCode == 4) {
return 5;
}
// Token失效,只需重试1次(重新获取token后)
if (errorCode == 110 || errorCode == 111) {
return 1;
}
// 临时服务故障,建议重试3次
if (errorCode == 2 || errorCode == 222201 || errorCode == 222204 || errorCode == 222205 ||
errorCode == 222206 || errorCode == 222302) {
return 3;
}
// 默认重试次数
return 3;
}
/**
* 根据错误码获取建议的重试延迟时间(毫秒)
*
* @param errorCode 错误码
* @param retryCount 当前重试次数(从0开始)
* @return 建议的延迟时间(毫秒),null 表示使用默认指数退避策略
*/
public static Long getSuggestedRetryDelay(Integer errorCode, int retryCount) {
if (errorCode == null || !isRetryable(errorCode)) {
return null;
}
// QPS限制,建议较长的延迟(指数退避)
if (errorCode == 18) {
return (long) (Math.pow(2, retryCount) * 1000); // 1s, 2s, 4s, 8s...
}
// 每天流量超限,建议更长的延迟
if (errorCode == 17 || errorCode == 19) {
return (long) (Math.pow(2, retryCount) * 5000); // 5s, 10s, 20s...
}
// Token失效,立即重试(因为需要先刷新token)
if (errorCode == 110 || errorCode == 111) {
return 0L;
}
// 集群超限,建议短暂延迟
if (errorCode == 4) {
return (long) (Math.pow(1.5, retryCount) * 500); // 500ms, 750ms, 1125ms...
}
// 默认使用指数退避策略
return (long) (Math.pow(2, retryCount) * 500); // 500ms, 1s, 2s, 4s...
}
}

View File

@@ -1,7 +1,24 @@
package com.ycwl.basic.facebody.exceptions; package com.ycwl.basic.facebody.exceptions;
/**
* 人脸识别异常基类
*
* <p>所有 facebody 包相关的异常都应继承此类。
*
* @see RetryableFaceBodyException
* @see NonRetryableFaceBodyException
*/
public class FaceBodyException extends RuntimeException { public class FaceBodyException extends RuntimeException {
public FaceBodyException(String message) { public FaceBodyException(String message) {
super(message); super(message);
} }
public FaceBodyException(String message, Throwable cause) {
super(message, cause);
}
public FaceBodyException(Throwable cause) {
super(cause);
}
} }

View File

@@ -0,0 +1,185 @@
package com.ycwl.basic.facebody.exceptions;
/**
* 不可重试的人脸识别异常
*
* <p>表示操作失败且重试不会改变结果的异常场景,通常由以下原因引起:
* <ul>
* <li>参数错误(如无效的图片 URL、缺失必填字段、参数格式错误)</li>
* <li>认证/授权失败(appId、apiKey、secretKey 错误或权限不足)</li>
* <li>资源不存在(人脸库、用户、人脸不存在)</li>
* <li>业务规则违反(如人脸质量不合格、人脸数量超限)</li>
* <li>不支持的操作或功能</li>
* <li>数据冲突(如尝试创建已存在的资源)</li>
* </ul>
*
* <p>调用方应捕获此异常并进行业务逻辑处理,而非简单重试。
*
* @see FaceBodyException
* @see RetryableFaceBodyException
*/
public class NonRetryableFaceBodyException extends FaceBodyException {
private final Integer errorCode;
private final ErrorCategory category;
/**
* 错误类别枚举
*/
public enum ErrorCategory {
/** 参数验证错误 */
VALIDATION_ERROR,
/** 认证或授权错误 */
AUTHENTICATION_ERROR,
/** 资源不存在 */
RESOURCE_NOT_FOUND,
/** 业务规则违反 */
BUSINESS_RULE_VIOLATION,
/** 不支持的操作 */
UNSUPPORTED_OPERATION,
/** 数据冲突 */
CONFLICT,
/** 其他不可重试错误 */
OTHER
}
/**
* 构造一个不可重试异常
*
* @param message 错误消息
*/
public NonRetryableFaceBodyException(String message) {
super(message);
this.errorCode = null;
this.category = ErrorCategory.OTHER;
}
/**
* 构造一个不可重试异常,包含原始异常信息
*
* @param message 错误消息
* @param cause 原始异常
*/
public NonRetryableFaceBodyException(String message, Throwable cause) {
super(message, cause);
this.errorCode = null;
this.category = ErrorCategory.OTHER;
}
/**
* 构造一个不可重试异常,指定错误类别
*
* @param message 错误消息
* @param category 错误类别
*/
public NonRetryableFaceBodyException(String message, ErrorCategory category) {
super(message);
this.errorCode = null;
this.category = category;
}
/**
* 构造一个不可重试异常,包含错误码和类别
*
* @param message 错误消息
* @param errorCode 第三方服务返回的错误码
* @param category 错误类别
*/
public NonRetryableFaceBodyException(String message, Integer errorCode, ErrorCategory category) {
super(message);
this.errorCode = errorCode;
this.category = category;
}
/**
* 构造一个不可重试异常,包含原始异常、错误码和类别
*
* @param message 错误消息
* @param cause 原始异常
* @param errorCode 第三方服务返回的错误码
* @param category 错误类别
*/
public NonRetryableFaceBodyException(String message, Throwable cause, Integer errorCode, ErrorCategory category) {
super(message, cause);
this.errorCode = errorCode;
this.category = category;
}
/**
* 获取第三方服务返回的错误码
*
* @return 错误码,可能为 null
*/
public Integer getErrorCode() {
return errorCode;
}
/**
* 获取错误类别
*
* @return 错误类别,不会为 null
*/
public ErrorCategory getCategory() {
return category;
}
/**
* 判断是否为参数验证错误
*
* @return true 如果是参数验证错误
*/
public boolean isValidationError() {
return category == ErrorCategory.VALIDATION_ERROR;
}
/**
* 判断是否为认证或授权错误
*
* @return true 如果是认证或授权错误
*/
public boolean isAuthenticationError() {
return category == ErrorCategory.AUTHENTICATION_ERROR;
}
/**
* 判断是否为资源不存在错误
*
* @return true 如果是资源不存在错误
*/
public boolean isResourceNotFound() {
return category == ErrorCategory.RESOURCE_NOT_FOUND;
}
/**
* 判断是否为业务规则违反
*
* @return true 如果是业务规则违反
*/
public boolean isBusinessRuleViolation() {
return category == ErrorCategory.BUSINESS_RULE_VIOLATION;
}
/**
* 判断是否为不支持的操作
*
* @return true 如果是不支持的操作
*/
public boolean isUnsupportedOperation() {
return category == ErrorCategory.UNSUPPORTED_OPERATION;
}
/**
* 判断是否为数据冲突
*
* @return true 如果是数据冲突
*/
public boolean isConflict() {
return category == ErrorCategory.CONFLICT;
}
}

View File

@@ -0,0 +1,139 @@
package com.ycwl.basic.facebody.exceptions;
/**
* 可重试的人脸识别异常
*
* <p>表示操作失败但可以通过重试解决的异常场景,通常由以下原因引起:
* <ul>
* <li>网络连接超时或临时中断</li>
* <li>第三方服务限流(rate limit exceeded)</li>
* <li>服务端临时不可用(5xx 错误)</li>
* <li>并发冲突或资源竞争</li>
* <li>临时资源不足</li>
* </ul>
*
* <p>调用方应捕获此异常并实现重试机制,建议采用指数退避策略。
*
* @see FaceBodyException
* @see NonRetryableFaceBodyException
*/
public class RetryableFaceBodyException extends FaceBodyException {
private final Integer errorCode;
private final Integer maxRetries;
private final Long retryAfterMillis;
/**
* 构造一个可重试异常
*
* @param message 错误消息
*/
public RetryableFaceBodyException(String message) {
super(message);
this.errorCode = null;
this.maxRetries = null;
this.retryAfterMillis = null;
}
/**
* 构造一个可重试异常,包含原始异常信息
*
* @param message 错误消息
* @param cause 原始异常
*/
public RetryableFaceBodyException(String message, Throwable cause) {
super(message, cause);
this.errorCode = null;
this.maxRetries = null;
this.retryAfterMillis = null;
}
/**
* 构造一个可重试异常,包含错误码
*
* @param message 错误消息
* @param errorCode 第三方服务返回的错误码
*/
public RetryableFaceBodyException(String message, Integer errorCode) {
super(message);
this.errorCode = errorCode;
this.maxRetries = null;
this.retryAfterMillis = null;
}
/**
* 构造一个可重试异常,包含完整的重试信息
*
* @param message 错误消息
* @param errorCode 第三方服务返回的错误码
* @param maxRetries 建议的最大重试次数
* @param retryAfterMillis 建议的重试延迟时间(毫秒)
*/
public RetryableFaceBodyException(String message, Integer errorCode, Integer maxRetries, Long retryAfterMillis) {
super(message);
this.errorCode = errorCode;
this.maxRetries = maxRetries;
this.retryAfterMillis = retryAfterMillis;
}
/**
* 构造一个可重试异常,包含原始异常和完整的重试信息
*
* @param message 错误消息
* @param cause 原始异常
* @param errorCode 第三方服务返回的错误码
* @param maxRetries 建议的最大重试次数
* @param retryAfterMillis 建议的重试延迟时间(毫秒)
*/
public RetryableFaceBodyException(String message, Throwable cause, Integer errorCode, Integer maxRetries, Long retryAfterMillis) {
super(message, cause);
this.errorCode = errorCode;
this.maxRetries = maxRetries;
this.retryAfterMillis = retryAfterMillis;
}
/**
* 获取第三方服务返回的错误码
*
* @return 错误码,可能为 null
*/
public Integer getErrorCode() {
return errorCode;
}
/**
* 获取建议的最大重试次数
*
* @return 最大重试次数,可能为 null(表示使用默认值)
*/
public Integer getMaxRetries() {
return maxRetries;
}
/**
* 获取建议的重试延迟时间
*
* @return 重试延迟时间(毫秒),可能为 null(表示使用默认退避策略)
*/
public Long getRetryAfterMillis() {
return retryAfterMillis;
}
/**
* 判断是否有明确的重试延迟时间建议
*
* @return true 如果有明确的延迟时间建议
*/
public boolean hasRetryAfter() {
return retryAfterMillis != null && retryAfterMillis > 0;
}
/**
* 判断是否有建议的最大重试次数
*
* @return true 如果有明确的重试次数限制
*/
public boolean hasMaxRetries() {
return maxRetries != null && maxRetries > 0;
}
}