From 7c42c5c462951e569c7ab9d855abb22cd92fa4ef Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 31 Oct 2025 15:01:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(facebody):=20=E5=AE=9E=E7=8E=B0=E4=BA=BA?= =?UTF-8?q?=E8=84=B8=E8=AF=86=E5=88=AB=E6=90=9C=E7=B4=A2=E7=9A=84=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加可重试和不可重试异常分类 - 集成百度云错误码分类器 - 实现搜索人脸接口的自动重试逻辑 - 支持根据错误码动态调整重试次数和延迟 - 添加详细的异常日志记录 - 保持与原有逻辑一致的空结果返回行为 --- .../facebody/adapter/BceFaceBodyAdapter.java | 73 +++- .../exceptions/BceErrorCodeClassifier.java | 405 ++++++++++++++++++ .../exceptions/FaceBodyException.java | 17 + .../NonRetryableFaceBodyException.java | 185 ++++++++ .../RetryableFaceBodyException.java | 139 ++++++ 5 files changed, 814 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/facebody/exceptions/BceErrorCodeClassifier.java create mode 100644 src/main/java/com/ycwl/basic/facebody/exceptions/NonRetryableFaceBodyException.java create mode 100644 src/main/java/com/ycwl/basic/facebody/exceptions/RetryableFaceBodyException.java diff --git a/src/main/java/com/ycwl/basic/facebody/adapter/BceFaceBodyAdapter.java b/src/main/java/com/ycwl/basic/facebody/adapter/BceFaceBodyAdapter.java index ba186a77..fdf5ead2 100644 --- a/src/main/java/com/ycwl/basic/facebody/adapter/BceFaceBodyAdapter.java +++ b/src/main/java/com/ycwl/basic/facebody/adapter/BceFaceBodyAdapter.java @@ -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 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); } } diff --git a/src/main/java/com/ycwl/basic/facebody/exceptions/BceErrorCodeClassifier.java b/src/main/java/com/ycwl/basic/facebody/exceptions/BceErrorCodeClassifier.java new file mode 100644 index 00000000..6045ffe0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/facebody/exceptions/BceErrorCodeClassifier.java @@ -0,0 +1,405 @@ +package com.ycwl.basic.facebody.exceptions; + +import java.util.Set; + +/** + * 百度云人脸识别错误码分类器 + * + *

根据百度云人脸识别 API 错误码文档,对错误进行分类: + *

+ * + *

参考文档: https://cloud.baidu.com/doc/FACE/s/5k37c1ujz + * + * @see RetryableFaceBodyException + * @see NonRetryableFaceBodyException + */ +public class BceErrorCodeClassifier { + + /** + * 可重试的错误码集合 + */ + private static final Set 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 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... + } +} diff --git a/src/main/java/com/ycwl/basic/facebody/exceptions/FaceBodyException.java b/src/main/java/com/ycwl/basic/facebody/exceptions/FaceBodyException.java index 459f2d01..30a8573f 100644 --- a/src/main/java/com/ycwl/basic/facebody/exceptions/FaceBodyException.java +++ b/src/main/java/com/ycwl/basic/facebody/exceptions/FaceBodyException.java @@ -1,7 +1,24 @@ package com.ycwl.basic.facebody.exceptions; +/** + * 人脸识别异常基类 + * + *

所有 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); + } } diff --git a/src/main/java/com/ycwl/basic/facebody/exceptions/NonRetryableFaceBodyException.java b/src/main/java/com/ycwl/basic/facebody/exceptions/NonRetryableFaceBodyException.java new file mode 100644 index 00000000..db3fec51 --- /dev/null +++ b/src/main/java/com/ycwl/basic/facebody/exceptions/NonRetryableFaceBodyException.java @@ -0,0 +1,185 @@ +package com.ycwl.basic.facebody.exceptions; + +/** + * 不可重试的人脸识别异常 + * + *

表示操作失败且重试不会改变结果的异常场景,通常由以下原因引起: + *

+ * + *

调用方应捕获此异常并进行业务逻辑处理,而非简单重试。 + * + * @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; + } +} diff --git a/src/main/java/com/ycwl/basic/facebody/exceptions/RetryableFaceBodyException.java b/src/main/java/com/ycwl/basic/facebody/exceptions/RetryableFaceBodyException.java new file mode 100644 index 00000000..96124600 --- /dev/null +++ b/src/main/java/com/ycwl/basic/facebody/exceptions/RetryableFaceBodyException.java @@ -0,0 +1,139 @@ +package com.ycwl.basic.facebody.exceptions; + +/** + * 可重试的人脸识别异常 + * + *

表示操作失败但可以通过重试解决的异常场景,通常由以下原因引起: + *

+ * + *

调用方应捕获此异常并实现重试机制,建议采用指数退避策略。 + * + * @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; + } +}