feat(i18n): 新增多语言国际化支持
引入 II18nService 与 I18nServiceImpl,使 AppleService 及全局异常处理器可按 Accept-Language 返回本地化错误信息;ErrorCode 新增 getCodeAsString;数据库连接改为 keyborad_db。
This commit is contained in:
@@ -34,4 +34,12 @@ public enum ErrorCode {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误码的字符串表示
|
||||
*
|
||||
* @return 错误码的字符串表示
|
||||
*/
|
||||
public String getCodeAsString() {
|
||||
return String.valueOf(code);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<I18nMessage> selectByCodeAndLocale(@Param("code") String code, @Param("locale") String locale);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
46
src/main/java/com/yolo/keyborad/service/II18nService.java
Normal file
46
src/main/java/com/yolo/keyborad/service/II18nService.java
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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<I18nMessage> 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -13,4 +13,8 @@
|
||||
<!--@mbg.generated-->
|
||||
id, code, "locale", message
|
||||
</sql>
|
||||
|
||||
<select id="selectByCodeAndLocale" resultMap="BaseResultMap">
|
||||
SELECT * FROM "i18n_message" WHERE code = #{code,jdbcType=VARCHAR} AND locale = #{locale,jdbcType=VARCHAR}
|
||||
</select>
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user