diff --git a/src/main/java/com/yolo/keyborad/common/ErrorCode.java b/src/main/java/com/yolo/keyborad/common/ErrorCode.java index 753614e..416f4ab 100644 --- a/src/main/java/com/yolo/keyborad/common/ErrorCode.java +++ b/src/main/java/com/yolo/keyborad/common/ErrorCode.java @@ -33,5 +33,13 @@ public enum ErrorCode { this.code = code; this.message = message; } - -} + + /** + * 获取错误码的字符串表示 + * + * @return 错误码的字符串表示 + */ + public String getCodeAsString() { + return String.valueOf(code); + } +} \ No newline at end of file diff --git a/src/main/java/com/yolo/keyborad/exception/GlobalExceptionHandler.java b/src/main/java/com/yolo/keyborad/exception/GlobalExceptionHandler.java index 02568ab..346ea02 100644 --- a/src/main/java/com/yolo/keyborad/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/yolo/keyborad/exception/GlobalExceptionHandler.java @@ -3,6 +3,8 @@ package com.yolo.keyborad.exception; import com.yolo.keyborad.common.BaseResponse; import com.yolo.keyborad.common.ErrorCode; import com.yolo.keyborad.common.ResultUtils; +import com.yolo.keyborad.service.II18nService; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -16,15 +18,45 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; @Slf4j public class GlobalExceptionHandler { + private final II18nService i18nService; + + public GlobalExceptionHandler(II18nService i18nService) { + this.i18nService = i18nService; + } + @ExceptionHandler(BusinessException.class) - public BaseResponse businessExceptionHandler(BusinessException e) { + public BaseResponse businessExceptionHandler(BusinessException e, HttpServletRequest request) { log.error("businessException: " + e.getMessage(), e); - return ResultUtils.error(e.getCode(), e.getMessage()); + + // 获取请求头中的Accept-Language + String acceptLanguage = request.getHeader("Accept-Language"); + + // 根据错误码获取国际化消息 + String errorMessage = i18nService.getMessageWithAcceptLanguage(String.valueOf(e.getCode()), acceptLanguage); + + // 如果没有找到国际化消息,使用原始消息 + if (errorMessage == null) { + errorMessage = e.getMessage(); + } + + return ResultUtils.error(e.getCode(), errorMessage); } @ExceptionHandler(RuntimeException.class) - public BaseResponse runtimeExceptionHandler(RuntimeException e) { + public BaseResponse runtimeExceptionHandler(RuntimeException e, HttpServletRequest request) { log.error("runtimeException", e); - return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage()); + + // 获取请求头中的Accept-Language + String acceptLanguage = request.getHeader("Accept-Language"); + + // 根据错误码获取国际化消息 + String errorMessage = i18nService.getMessageWithAcceptLanguage(String.valueOf(ErrorCode.SYSTEM_ERROR.getCode()), acceptLanguage); + + // 如果没有找到国际化消息,使用原始消息 + if (errorMessage == null) { + errorMessage = e.getMessage(); + } + + return ResultUtils.error(ErrorCode.SYSTEM_ERROR.getCode(), errorMessage); } -} +} \ No newline at end of file diff --git a/src/main/java/com/yolo/keyborad/mapper/I18nMessageMapper.java b/src/main/java/com/yolo/keyborad/mapper/I18nMessageMapper.java index a6f1228..2841a7d 100644 --- a/src/main/java/com/yolo/keyborad/mapper/I18nMessageMapper.java +++ b/src/main/java/com/yolo/keyborad/mapper/I18nMessageMapper.java @@ -5,11 +5,15 @@ package com.yolo.keyborad.mapper; * @date: 2025/12/1 20:40 */ +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.yolo.keyborad.model.entity.I18nMessage; +import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; + import java.util.List; public interface I18nMessageMapper { + List selectByCodeAndLocale(@Param("code") String code, @Param("locale") String locale); } \ No newline at end of file diff --git a/src/main/java/com/yolo/keyborad/model/entity/I18nMessage.java b/src/main/java/com/yolo/keyborad/model/entity/I18nMessage.java index 4cc187b..73f97dd 100644 --- a/src/main/java/com/yolo/keyborad/model/entity/I18nMessage.java +++ b/src/main/java/com/yolo/keyborad/model/entity/I18nMessage.java @@ -1,55 +1,34 @@ package com.yolo.keyborad.model.entity; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; /* * @author: ziin * @date: 2025/12/1 20:40 */ -@Schema +@Schema(description="多语言消息表") +@Data +@TableName("i18n_message") public class I18nMessage { - @Schema(description="") + @TableId("id") + @Schema(description="主键") private Long id; - @Schema(description="") + @TableField("code") + @Schema(description="消息码") private String code; - @Schema(description="") + @TableField("locale") + @Schema(description="语言区域") private String locale; - @Schema(description="") + @TableField("message") + @Schema(description="消息内容") private String message; - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getCode() { - return code; - } - - public void setCode(String code) { - this.code = code; - } - - public String getLocale() { - return locale; - } - - public void setLocale(String locale) { - this.locale = locale; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } } \ No newline at end of file diff --git a/src/main/java/com/yolo/keyborad/service/II18nService.java b/src/main/java/com/yolo/keyborad/service/II18nService.java new file mode 100644 index 0000000..808ae82 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/service/II18nService.java @@ -0,0 +1,46 @@ +package com.yolo.keyborad.service; + +/** + * 国际化服务接口 + * + * @author ziin + * @date 2025/12/1 + */ +public interface II18nService { + + /** + * 根据错误码和语言获取错误消息 + * + * @param code 错误码 + * @param acceptLanguage 请求头中的accept-language + * @return 国际化后的错误消息 + */ + String getMessageWithAcceptLanguage(String code, String acceptLanguage); + + /** + * 根据错误码和语言获取错误消息 + * + * @param code 错误码 + * @param locale 语言代码 + * @return 国际化后的错误消息 + */ + String getMessageWithLocale(String code, String locale); + + /** + * 从Redis缓存中获取错误消息 + * + * @param code 错误码 + * @param locale 语言代码 + * @return 错误消息,如果不存在则返回null + */ + String getMessageFromCache(String code, String locale); + + /** + * 将错误消息缓存到Redis + * + * @param code 错误码 + * @param locale 语言代码 + * @param message 错误消息 + */ + void cacheMessage(String code, String locale, String message); +} \ No newline at end of file diff --git a/src/main/java/com/yolo/keyborad/service/impl/AppleServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/AppleServiceImpl.java index dd41ff7..60be80a 100644 --- a/src/main/java/com/yolo/keyborad/service/impl/AppleServiceImpl.java +++ b/src/main/java/com/yolo/keyborad/service/impl/AppleServiceImpl.java @@ -3,6 +3,8 @@ package com.yolo.keyborad.service.impl; import cn.hutool.http.HttpUtil; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; +import com.yolo.keyborad.common.ErrorCode; +import com.yolo.keyborad.exception.BusinessException; import com.yolo.keyborad.service.IAppleService; import io.jsonwebtoken.*; import lombok.AllArgsConstructor; @@ -45,7 +47,7 @@ public class AppleServiceImpl implements IAppleService { // 2. 拆分三段 & 使用 Base64URL 解码 String[] parts = identityToken.split("\\."); if (parts.length != 3) { - throw new RuntimeException("非法的 identityToken,JWT 结构不正确"); + throw new BusinessException(ErrorCode.OPERATION_ERROR); } Base64.Decoder urlDecoder = Base64.getUrlDecoder(); diff --git a/src/main/java/com/yolo/keyborad/service/impl/I18nServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/I18nServiceImpl.java new file mode 100644 index 0000000..bb16ebc --- /dev/null +++ b/src/main/java/com/yolo/keyborad/service/impl/I18nServiceImpl.java @@ -0,0 +1,141 @@ +package com.yolo.keyborad.service.impl; + +import com.yolo.keyborad.mapper.I18nMessageMapper; +import com.yolo.keyborad.model.entity.I18nMessage; +import com.yolo.keyborad.service.II18nService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import jakarta.annotation.Resource; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** + * 国际化服务实现类 + * + * @author ziin + * @date 2025/12/1 + */ +@Service +@Slf4j +public class I18nServiceImpl implements II18nService { + + @Resource + private I18nMessageMapper i18nMessageMapper; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + private static final String CACHE_PREFIX = "i18n:message:"; + private static final long CACHE_EXPIRE_HOURS = 24; + + @Override + public String getMessageWithAcceptLanguage(String code, String acceptLanguage) { + // 解析accept-language,获取首选语言 + String locale = parseAcceptLanguage(acceptLanguage); + return getMessageWithLocale(code, locale); + } + + @Override + public String getMessageWithLocale(String code, String locale) { + // 如果没有指定语言,使用默认语言 + if (!StringUtils.hasText(locale)) { + locale = Locale.getDefault().getLanguage(); + } + + // 先从缓存中获取 + String message = getMessageFromCache(code, locale); + if (message != null) { + return message; + } + + // 缓存中没有,从数据库中获取 + message = getMessageFromDatabase(code, locale); + + // 如果数据库中也没有,尝试获取默认语言的消息 + if (message == null && !Locale.getDefault().getLanguage().equals(locale)) { + message = getMessageFromDatabase(code, Locale.getDefault().getLanguage()); + } + + // 将消息缓存到Redis + if (message != null) { + cacheMessage(code, locale, message); + } + + return message; + } + + @Override + public String getMessageFromCache(String code, String locale) { + try { + String key = CACHE_PREFIX + locale + ":" + code; + return stringRedisTemplate.opsForValue().get(key); + } catch (Exception e) { + log.error("从Redis获取国际化消息失败: code={}, locale={}", code, locale, e); + return null; + } + } + + @Override + public void cacheMessage(String code, String locale, String message) { + try { + String key = CACHE_PREFIX + locale + ":" + code; + stringRedisTemplate.opsForValue().set(key, message, CACHE_EXPIRE_HOURS, TimeUnit.HOURS); + } catch (Exception e) { + log.error("缓存国际化消息到Redis失败: code={}, locale={}", code, locale, e); + } + } + + /** + * 从数据库获取消息 + * + * @param code 错误码 + * @param locale 语言代码 + * @return 错误消息 + */ + private String getMessageFromDatabase(String code, String locale) { + try { + List messages = i18nMessageMapper.selectByCodeAndLocale(code, locale); + if (!messages.isEmpty()) { + return messages.get(0).getMessage(); + } + return null; + } catch (Exception e) { + log.error("从数据库获取国际化消息失败: code={}, locale={}", code, locale, e); + return null; + } + } + + /** + * 解析Accept-Language头,获取首选语言 + * + * @param acceptLanguage Accept-Language头 + * @return 首选语言代码 + */ + private String parseAcceptLanguage(String acceptLanguage) { + if (!StringUtils.hasText(acceptLanguage)) { + return Locale.getDefault().getLanguage(); + } + + try { + // 解析Accept-Language头,例如 "zh-CN,zh;q=0.9,en;q=0.8" + String[] languages = acceptLanguage.split(","); + if (languages.length > 0) { + // 获取第一个语言,并去掉权重部分 + String firstLanguage = languages[0].split(";")[0].trim(); + // 如果是zh-CN这样的格式,只取zh + if (firstLanguage.contains("-")) { + return firstLanguage.split("-")[0]; + } + return firstLanguage; + } + } catch (Exception e) { + log.error("解析Accept-Language失败: {}", acceptLanguage, e); + } + + return Locale.getDefault().getLanguage(); + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index fd4f44f..7e539a4 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,7 +1,7 @@ spring: datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://localhost:5432/postgres + url: jdbc:postgresql://localhost:5432/keyborad_db username: root password: 123asd diff --git a/src/main/resources/mapper/I18nMessageMapper.xml b/src/main/resources/mapper/I18nMessageMapper.xml index 32a71bb..28aae5d 100644 --- a/src/main/resources/mapper/I18nMessageMapper.xml +++ b/src/main/resources/mapper/I18nMessageMapper.xml @@ -13,4 +13,8 @@ id, code, "locale", message + + \ No newline at end of file