| | |
| | | public class GlobalExceptionHandler { |
| | | |
| | | private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); |
| | | private static final String DEFAULT_SERVER_ERROR_MESSAGE = "服务器开小差啦,请稍后重试"; |
| | | |
| | | @ExceptionHandler(NotLoginException.class) |
| | | public ApiResponse<Void> handleNotLogin(NotLoginException e, HttpServletRequest request) { |
| | |
| | | return ApiResponse.fail(400, e.getMessage()); |
| | | } |
| | | |
| | | @ExceptionHandler(IllegalStateException.class) |
| | | public ApiResponse<Void> handleIllegalState(IllegalStateException e, HttpServletRequest request) { |
| | | log.warn("业务状态异常 [{} {}]: {}", request.getMethod(), request.getRequestURI(), e.getMessage(), e); |
| | | return ApiResponse.fail(500, resolveClientMessage(e.getMessage())); |
| | | } |
| | | |
| | | @ExceptionHandler(HttpMessageNotReadableException.class) |
| | | public ApiResponse<Void> handleHttpMessageNotReadable(HttpMessageNotReadableException e, HttpServletRequest request) { |
| | | log.warn("请求体解析失败 [{} {}]", request.getMethod(), request.getRequestURI(), e); |
| | |
| | | @ExceptionHandler(Exception.class) |
| | | public ApiResponse<Void> handleException(Exception e, HttpServletRequest request) { |
| | | log.error("未捕获异常 [{} {}]", request.getMethod(), request.getRequestURI(), e); |
| | | return ApiResponse.fail(500, "服务器内部错误"); |
| | | return ApiResponse.fail(500, DEFAULT_SERVER_ERROR_MESSAGE); |
| | | } |
| | | |
| | | private String resolveClientMessage(String message) { |
| | | if (message == null || message.trim().isEmpty()) { |
| | | return DEFAULT_SERVER_ERROR_MESSAGE; |
| | | } |
| | | return message; |
| | | } |
| | | |
| | | private String resolveReadableMessage(Throwable throwable) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 发送阿里云短信验证码。 |
| | | * 创建阿里云短信客户端,便于测试覆盖异常分支。 |
| | | */ |
| | | public void sendMsg(String phone, Map<String,String> params, String templateCode, String signName) throws Exception { |
| | | com.aliyun.dysmsapi20170525.Client client = MsgUtils.createClient( |
| | | protected com.aliyun.dysmsapi20170525.Client createSmsClient() throws Exception { |
| | | return MsgUtils.createClient( |
| | | properties.getAccessKeyId(), |
| | | properties.getAccessKeySecret() |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * 发送阿里云短信验证码。 |
| | | */ |
| | | public void sendMsg(String phone, Map<String,String> params, String templateCode, String signName) throws Exception { |
| | | com.aliyun.dysmsapi20170525.Client client = createSmsClient(); |
| | | SendSmsRequest sendSmsRequest = new SendSmsRequest() |
| | | .setSignName(signName) |
| | | .setTemplateCode(templateCode) |
| | |
| | | } |
| | | log.info("阿里云短信发送成功,templateCode={}, statusCode={}", templateCode, sendSmsResponse.getStatusCode()); |
| | | } catch (TeaException error) { |
| | | com.aliyun.teautil.Common.assertAsString(error.message); |
| | | log.error("阿里云短信发送失败,templateCode={}, message={}", templateCode, error.message); |
| | | log.error("阿里云短信发送失败,templateCode={}, code={}, message={}", |
| | | templateCode, |
| | | error.getCode(), |
| | | resolveExceptionMessage(error, "阿里云短信发送失败"), |
| | | error); |
| | | throw new IllegalStateException("短信发送失败,请稍后重试"); |
| | | } catch (Exception _error) { |
| | | TeaException error = new TeaException(_error.getMessage(), _error); |
| | | com.aliyun.teautil.Common.assertAsString(error.message); |
| | | log.error("阿里云短信发送异常,templateCode={}, message={}", templateCode, error.message); |
| | | } catch (Exception error) { |
| | | log.error("阿里云短信发送异常,templateCode={}, message={}", |
| | | templateCode, |
| | | resolveExceptionMessage(error, "阿里云短信发送异常"), |
| | | error); |
| | | throw new IllegalStateException("短信发送失败,请稍后重试"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 安全读取异常文案,避免阿里云 SDK 异常 message 为空时二次抛错。 |
| | | */ |
| | | private String resolveExceptionMessage(Throwable error, String fallback) { |
| | | if (error == null) { |
| | | return fallback; |
| | | } |
| | | String message = error.getMessage(); |
| | | if (message == null || message.trim().isEmpty()) { |
| | | return fallback; |
| | | } |
| | | return message; |
| | | } |
| | | |
| | | /** |
| | | * 将模板变量转换为阿里云要求的 JSON 字符串。 |
| | | */ |
| | | private String toTemplateParam(Map<String, String> params) throws JsonProcessingException { |
| New file |
| | |
| | | package com.tcm.training.admin.common; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | class GlobalExceptionHandlerTest { |
| | | |
| | | private final GlobalExceptionHandler handler = new GlobalExceptionHandler(); |
| | | |
| | | @Test |
| | | void handleIllegalStateShouldReturnBusinessMessage() { |
| | | ApiResponse<Void> response = handler.handleIllegalState( |
| | | new IllegalStateException("短信发送失败,请稍后重试"), |
| | | mockRequest() |
| | | ); |
| | | |
| | | assertEquals(500, response.getCode()); |
| | | assertEquals("短信发送失败,请稍后重试", response.getMsg()); |
| | | } |
| | | |
| | | @Test |
| | | void handleIllegalStateShouldReturnFriendlyFallbackWhenMessageBlank() { |
| | | ApiResponse<Void> response = handler.handleIllegalState( |
| | | new IllegalStateException(" "), |
| | | mockRequest() |
| | | ); |
| | | |
| | | assertEquals(500, response.getCode()); |
| | | assertEquals("服务器开小差啦,请稍后重试", response.getMsg()); |
| | | } |
| | | |
| | | @Test |
| | | void handleExceptionShouldHideInternalMessage() { |
| | | ApiResponse<Void> response = handler.handleException( |
| | | new RuntimeException("database down"), |
| | | mockRequest() |
| | | ); |
| | | |
| | | assertEquals(500, response.getCode()); |
| | | assertEquals("服务器开小差啦,请稍后重试", response.getMsg()); |
| | | } |
| | | |
| | | private HttpServletRequest mockRequest() { |
| | | HttpServletRequest request = mock(HttpServletRequest.class); |
| | | when(request.getMethod()).thenReturn("POST"); |
| | | when(request.getRequestURI()).thenReturn("/api/app/auth/sms-code"); |
| | | return request; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.utils; |
| | | |
| | | import com.aliyun.dysmsapi20170525.models.SendSmsRequest; |
| | | import com.aliyun.dysmsapi20170525.models.SendSmsResponse; |
| | | import com.aliyun.tea.TeaException; |
| | | import com.aliyun.teaopenapi.models.Config; |
| | | import com.aliyun.teautil.models.RuntimeOptions; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.tcm.training.admin.app.auth.config.AppAliyunSmsProperties; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import java.util.Collections; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | |
| | | /** |
| | | * 阿里云短信工具异常处理测试。 |
| | | */ |
| | | class MsgUtilsTest { |
| | | |
| | | /** |
| | | * 普通异常 message 为空时,应保留统一业务异常而不是被阿里云类型断言覆盖。 |
| | | */ |
| | | @Test |
| | | void sendMsgShouldHandleExceptionWithNullMessage() throws Exception { |
| | | MsgUtils msgUtils = new TestMsgUtils(new FailingSmsClient(new RuntimeException((String) null))); |
| | | |
| | | IllegalStateException exception = assertThrows(IllegalStateException.class, |
| | | () -> msgUtils.sendMsg("13800138000", Collections.singletonMap("code", "123456"), "SMS_TEST", "海诊通")); |
| | | |
| | | assertEquals("短信发送失败,请稍后重试", exception.getMessage()); |
| | | } |
| | | |
| | | /** |
| | | * 阿里云 TeaException message 为空时,也不能二次抛出 TeaUtilException。 |
| | | */ |
| | | @Test |
| | | void sendMsgShouldHandleTeaExceptionWithNullMessage() throws Exception { |
| | | TeaException teaException = new TeaException((String) null, new RuntimeException("sdk failed")); |
| | | MsgUtils msgUtils = new TestMsgUtils(new FailingSmsClient(teaException)); |
| | | |
| | | IllegalStateException exception = assertThrows(IllegalStateException.class, |
| | | () -> msgUtils.sendMsg("13800138000", Collections.singletonMap("code", "123456"), "SMS_TEST", "海诊通")); |
| | | |
| | | assertEquals("短信发送失败,请稍后重试", exception.getMessage()); |
| | | } |
| | | |
| | | /** |
| | | * 可注入短信客户端的测试工具类。 |
| | | */ |
| | | private static class TestMsgUtils extends MsgUtils { |
| | | |
| | | private final com.aliyun.dysmsapi20170525.Client client; |
| | | |
| | | private TestMsgUtils(com.aliyun.dysmsapi20170525.Client client) { |
| | | super(new AppAliyunSmsProperties(), new ObjectMapper()); |
| | | this.client = client; |
| | | } |
| | | |
| | | /** |
| | | * 返回测试短信客户端。 |
| | | */ |
| | | @Override |
| | | protected com.aliyun.dysmsapi20170525.Client createSmsClient() { |
| | | return client; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 固定抛异常的阿里云短信客户端。 |
| | | */ |
| | | private static class FailingSmsClient extends com.aliyun.dysmsapi20170525.Client { |
| | | |
| | | private final Exception exception; |
| | | |
| | | private FailingSmsClient(Exception exception) throws Exception { |
| | | super(new Config().setAccessKeyId("test-ak").setAccessKeySecret("test-sk").setEndpoint("dysmsapi.aliyuncs.com")); |
| | | this.exception = exception; |
| | | } |
| | | |
| | | /** |
| | | * 模拟阿里云发送接口异常。 |
| | | */ |
| | | @Override |
| | | public SendSmsResponse sendSmsWithOptions(SendSmsRequest request, RuntimeOptions runtime) throws Exception { |
| | | throw exception; |
| | | } |
| | | } |
| | | } |