feat(i18n): 新增多语言国际化支持

引入 II18nService 与 I18nServiceImpl,使 AppleService 及全局异常处理器可按 Accept-Language 返回本地化错误信息;ErrorCode 新增 getCodeAsString;数据库连接改为 keyborad_db。
This commit is contained in:
2025-12-01 21:54:51 +08:00
parent 683accca83
commit bcbb623ee4
9 changed files with 261 additions and 45 deletions

View File

@@ -34,4 +34,12 @@ public enum ErrorCode {
this.message = message;
}
/**
* 获取错误码的字符串表示
*
* @return 错误码的字符串表示
*/
public String getCodeAsString() {
return String.valueOf(code);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View 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);
}

View File

@@ -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("非法的 identityTokenJWT 结构不正确");
throw new BusinessException(ErrorCode.OPERATION_ERROR);
}
Base64.Decoder urlDecoder = Base64.getUrlDecoder();

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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>