“mitao”
2026-05-12 5b5553bb13ae1dbd31e0fb7edfadac76c0642a8c
fix:全局异常处理
2个文件已添加
2个文件已修改
203 ■■■■■ 已修改文件
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/common/GlobalExceptionHandler.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/utils/MsgUtils.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/common/GlobalExceptionHandlerTest.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/utils/MsgUtilsTest.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/common/GlobalExceptionHandler.java
@@ -23,6 +23,7 @@
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) {
@@ -60,6 +61,12 @@
        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);
@@ -75,7 +82,14 @@
    @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) {
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/utils/MsgUtils.java
@@ -49,13 +49,20 @@
    }
    /**
     * 发送阿里云短信验证码。
     * 创建阿里云短信客户端,便于测试覆盖异常分支。
     */
    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)
@@ -73,18 +80,36 @@
            }
            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 {
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/common/GlobalExceptionHandlerTest.java
New file
@@ -0,0 +1,54 @@
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;
    }
}
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/utils/MsgUtilsTest.java
New file
@@ -0,0 +1,90 @@
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;
        }
    }
}