feat(i18n): 新增多语言国际化支持
引入 II18nService 与 I18nServiceImpl,使 AppleService 及全局异常处理器可按 Accept-Language 返回本地化错误信息;ErrorCode 新增 getCodeAsString;数据库连接改为 keyborad_db。
This commit is contained in:
@@ -33,5 +33,13 @@ public enum ErrorCode {
|
|||||||
this.code = code;
|
this.code = code;
|
||||||
this.message = message;
|
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.BaseResponse;
|
||||||
import com.yolo.keyborad.common.ErrorCode;
|
import com.yolo.keyborad.common.ErrorCode;
|
||||||
import com.yolo.keyborad.common.ResultUtils;
|
import com.yolo.keyborad.common.ResultUtils;
|
||||||
|
import com.yolo.keyborad.service.II18nService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
@@ -16,15 +18,45 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private final II18nService i18nService;
|
||||||
|
|
||||||
|
public GlobalExceptionHandler(II18nService i18nService) {
|
||||||
|
this.i18nService = i18nService;
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(BusinessException.class)
|
@ExceptionHandler(BusinessException.class)
|
||||||
public BaseResponse<?> businessExceptionHandler(BusinessException e) {
|
public BaseResponse<?> businessExceptionHandler(BusinessException e, HttpServletRequest request) {
|
||||||
log.error("businessException: " + e.getMessage(), e);
|
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)
|
@ExceptionHandler(RuntimeException.class)
|
||||||
public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
|
public BaseResponse<?> runtimeExceptionHandler(RuntimeException e, HttpServletRequest request) {
|
||||||
log.error("runtimeException", e);
|
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
|
* @date: 2025/12/1 20:40
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.yolo.keyborad.model.entity.I18nMessage;
|
import com.yolo.keyborad.model.entity.I18nMessage;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface I18nMessageMapper {
|
public interface I18nMessageMapper {
|
||||||
|
|
||||||
|
List<I18nMessage> selectByCodeAndLocale(@Param("code") String code, @Param("locale") String locale);
|
||||||
}
|
}
|
||||||
@@ -1,55 +1,34 @@
|
|||||||
package com.yolo.keyborad.model.entity;
|
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 io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
* @date: 2025/12/1 20:40
|
* @date: 2025/12/1 20:40
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Schema
|
@Schema(description="多语言消息表")
|
||||||
|
@Data
|
||||||
|
@TableName("i18n_message")
|
||||||
public class I18nMessage {
|
public class I18nMessage {
|
||||||
@Schema(description="")
|
@TableId("id")
|
||||||
|
@Schema(description="主键")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Schema(description="")
|
@TableField("code")
|
||||||
|
@Schema(description="消息码")
|
||||||
private String code;
|
private String code;
|
||||||
|
|
||||||
@Schema(description="")
|
@TableField("locale")
|
||||||
|
@Schema(description="语言区域")
|
||||||
private String locale;
|
private String locale;
|
||||||
|
|
||||||
@Schema(description="")
|
@TableField("message")
|
||||||
|
@Schema(description="消息内容")
|
||||||
private String message;
|
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 cn.hutool.http.HttpUtil;
|
||||||
import com.alibaba.fastjson.JSONArray;
|
import com.alibaba.fastjson.JSONArray;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.yolo.keyborad.common.ErrorCode;
|
||||||
|
import com.yolo.keyborad.exception.BusinessException;
|
||||||
import com.yolo.keyborad.service.IAppleService;
|
import com.yolo.keyborad.service.IAppleService;
|
||||||
import io.jsonwebtoken.*;
|
import io.jsonwebtoken.*;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
@@ -45,7 +47,7 @@ public class AppleServiceImpl implements IAppleService {
|
|||||||
// 2. 拆分三段 & 使用 Base64URL 解码
|
// 2. 拆分三段 & 使用 Base64URL 解码
|
||||||
String[] parts = identityToken.split("\\.");
|
String[] parts = identityToken.split("\\.");
|
||||||
if (parts.length != 3) {
|
if (parts.length != 3) {
|
||||||
throw new RuntimeException("非法的 identityToken,JWT 结构不正确");
|
throw new BusinessException(ErrorCode.OPERATION_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
Base64.Decoder urlDecoder = Base64.getUrlDecoder();
|
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:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
url: jdbc:postgresql://localhost:5432/postgres
|
url: jdbc:postgresql://localhost:5432/keyborad_db
|
||||||
username: root
|
username: root
|
||||||
password: 123asd
|
password: 123asd
|
||||||
|
|
||||||
|
|||||||
@@ -13,4 +13,8 @@
|
|||||||
<!--@mbg.generated-->
|
<!--@mbg.generated-->
|
||||||
id, code, "locale", message
|
id, code, "locale", message
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectByCodeAndLocale" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM "i18n_message" WHERE code = #{code,jdbcType=VARCHAR} AND locale = #{locale,jdbcType=VARCHAR}
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
Reference in New Issue
Block a user