You've already forked FrameTour-BE
feat(facebody): 实现人脸识别搜索的重试机制
- 添加可重试和不可重试异常分类 - 集成百度云错误码分类器 - 实现搜索人脸接口的自动重试逻辑 - 支持根据错误码动态调整重试次数和延迟 - 添加详细的异常日志记录 - 保持与原有逻辑一致的空结果返回行为
This commit is contained in:
@@ -5,6 +5,10 @@ import com.ycwl.basic.facebody.entity.AddFaceResp;
|
||||
import com.ycwl.basic.facebody.entity.BceFaceBodyConfig;
|
||||
import com.ycwl.basic.facebody.entity.SearchFaceResp;
|
||||
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.IRateLimiter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -273,18 +277,71 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
|
||||
|
||||
@Override
|
||||
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);
|
||||
SearchFaceResp resp = new SearchFaceResp();
|
||||
|
||||
try {
|
||||
AipFace client = getClient();
|
||||
HashMap<String, Object> options = new HashMap<>();
|
||||
options.put("max_user_num", "50");
|
||||
|
||||
try {
|
||||
searchFaceLimiter.acquire();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
|
||||
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);
|
||||
JSONObject resultObj = response.getJSONObject("result");
|
||||
if (resultObj == null) {
|
||||
@@ -308,12 +365,18 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
|
||||
}
|
||||
return resp;
|
||||
} 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) {
|
||||
log.error("搜索人脸失败!", e);
|
||||
return null;
|
||||
// 其他异常(如网络异常)包装为可重试异常
|
||||
log.error("搜索人脸网络异常", e);
|
||||
throw new RetryableFaceBodyException("搜索人脸网络异常: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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...
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,24 @@
|
||||
package com.ycwl.basic.facebody.exceptions;
|
||||
|
||||
/**
|
||||
* 人脸识别异常基类
|
||||
*
|
||||
* <p>所有 facebody 包相关的异常都应继承此类。
|
||||
*
|
||||
* @see RetryableFaceBodyException
|
||||
* @see NonRetryableFaceBodyException
|
||||
*/
|
||||
public class FaceBodyException extends RuntimeException {
|
||||
|
||||
public FaceBodyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FaceBodyException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public FaceBodyException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user