初始代码

This commit is contained in:
wangmingwei
2026-04-21 17:41:09 +08:00
parent 186ec6683a
commit b686ecac5f
493 changed files with 52349 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yunzhupaas</groupId>
<artifactId>yunzhupaas-boot-common</artifactId>
<version>5.2.0-RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>yunzhupaas-common-i18n</artifactId>
<dependencies>
<dependency>
<groupId>com.yunzhupaas</groupId>
<artifactId>yunzhupaas-common-event</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,75 @@
package com.yunzhupaas.i18n.config;
import com.yunzhupaas.config.ConfigValueUtil;
import com.yunzhupaas.i18n.constant.I18nApiConst;
import com.yunzhupaas.i18n.core.DynamicMessageSource;
import com.yunzhupaas.i18n.core.MyReloadableResourceBundleMessageSource;
import com.yunzhupaas.i18n.provider.*;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.context.MessageSourceProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.List;
/**
* 国际化语言配置类
*/
@Configuration(proxyBeanMethods = false)
public class I18nAutoConfiguration {
@Primary
@Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)
public MessageSource messageSource(MessageSourceProperties properties, List<MessageSourceProvider> messageSourceProviders, DynamicMessageSource dynamicMessageSource) {
ReloadableResourceBundleMessageSource messageSource = new MyReloadableResourceBundleMessageSource(messageSourceProviders);
if (StringUtils.hasText(properties.getBasename())) {
messageSource.setBasenames(StringUtils
.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
}
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
dynamicMessageSource.setParentMessageSource(messageSource);
return dynamicMessageSource;
}
@Bean
@ConditionalOnMissingBean
public DynamicMessageSource getDynamicMessageSource(I18nMessageProvider i18nMessageProvider){
return new DynamicMessageSource(i18nMessageProvider);
}
@Bean
@ConditionalOnMissingBean
public DynamicMessageProvider getDynamicMessageProvider(){
return new MyDynamicMessageProvider();
}
@Bean
@ConditionalOnMissingBean
public I18nMessageProvider getI18nMessageProvider(ConfigValueUtil configValueUtil, DynamicMessageProvider dynamicMessageProvider){
return new MyI18nMessageProvider(configValueUtil, dynamicMessageProvider);
}
@Bean
public I18nApiConst getI18nApiConst(ConfigValueUtil configValueUtil){
return new I18nApiConst(configValueUtil.getApiDomain());
}
}

View File

@@ -0,0 +1,31 @@
package com.yunzhupaas.i18n.config;
import jakarta.annotation.Resource;
import com.yunzhupaas.event.ProjectEventListener;
import com.yunzhupaas.i18n.constant.I18nConst;
import com.yunzhupaas.i18n.provider.MyI18nMessageProvider;
import com.yunzhupaas.module.ProjectEventInstance;
import org.springframework.context.annotation.Configuration;
/**
* 国际化语言Redis缓存监听配置
* 收到租户多语言缓存变动时清空租户多语言缓存
*/
@Configuration(proxyBeanMethods = false)
public class I18nRefreshListenerConfig {
@Resource
private MyI18nMessageProvider myI18nMessageProvider;
@ProjectEventListener(channelRegex = {I18nConst.CACHE_KEY_SERVER + ".*", I18nConst.CACHE_KEY_FRONT + ".*"})
// @ProjectEventListener(channelSpel = {"#root.channel.startsWith(T(com.yunzhupaas.i18n.constant.I18nConst).CACHE_KEY_SERVER) || #root.channel.startsWith(T(com.yunzhupaas.i18n.constant.I18nConst).CACHE_KEY_FRONT)"})
public void onRedisKeySetEvent(ProjectEventInstance redisEvent){
String key = redisEvent.getChannel();
// 获取多租户编码
key = key.replace(I18nConst.CACHE_KEY_SERVER, "").replace(I18nConst.CACHE_KEY_FRONT, "");
myI18nMessageProvider.removeTenantCache(key);
}
}

View File

@@ -0,0 +1,63 @@
package com.yunzhupaas.i18n.provider;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.yunzhupaas.base.ActionResult;
import com.yunzhupaas.base.UserInfo;
import com.yunzhupaas.i18n.constant.I18nApiConst;
import com.yunzhupaas.util.Constants;
import com.yunzhupaas.util.StringUtil;
import com.yunzhupaas.util.UserProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import java.util.Locale;
import java.util.Objects;
/**
* 国际化翻译源提供者
*/
@Slf4j
public class MyDynamicMessageProvider implements DynamicMessageProvider {
private int timeout = 3000;
@Override
public String getI18nList(Locale locale) {
UserInfo userInfo = UserProvider.getUser();
String token = userInfo.getToken();
if(StringUtil.isNotEmpty(token)) {
try (HttpResponse httpResponse = HttpRequest.get(I18nApiConst.i18nListUrl)
.timeout(timeout)
.setReadTimeout(timeout)
.setConnectionTimeout(timeout)
.header(Constants.AUTHORIZATION, token)
.header(HttpHeaders.ACCEPT_LANGUAGE, locale.toLanguageTag())
.execute()) {
if (httpResponse.isOk()) {
String result = httpResponse.body();
ActionResult<JSONArray> out = JSON.parseObject(result, new TypeReference<ActionResult<JSONArray>>() {});
if (Objects.equals(Constants.SUCCESS, out.getCode())) {
JSONArray messages = out.getData();
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < messages.size(); i++) {
JSONObject obj = messages.getJSONObject(i);
String code = obj.getString("enCode");
String msg = obj.getString("fullName");
stringBuilder.append(String.format("%s=%s%n", code, msg));
}
return stringBuilder.toString();
}
}
} catch (Exception e) {
log.error("语言翻译内容获取失败: {}", e.getMessage());
}
}
return null;
}
}

View File

@@ -0,0 +1,108 @@
package com.yunzhupaas.i18n.provider;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import com.yunzhupaas.base.UserInfo;
import com.yunzhupaas.config.ConfigValueUtil;
import com.yunzhupaas.constant.GlobalConst;
import com.yunzhupaas.constant.model.MCode;
import com.yunzhupaas.util.TenantHolder;
import com.yunzhupaas.util.UserProvider;
import lombok.extern.slf4j.Slf4j;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* 国际化语言翻译
*/
@Slf4j
public class MyI18nMessageProvider implements I18nMessageProvider {
private ConfigValueUtil configValueUtil;
private final long CACHE_TIME = 48 * 60 * 60000L;
// 未使用的情况下, 默认48小时失效, 使用后重新计算缓存时效
private TimedCache<String, TimedCache<String, Properties>> tenantMessageProperties = CacheUtil.newTimedCache(CACHE_TIME);
// 翻译内容提供者
private DynamicMessageProvider dynamicMessageProvider;
// 租户语言加载锁
private final ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<>();
private Properties emptyProperties = new Properties();
public MyI18nMessageProvider(ConfigValueUtil configValueUtil, DynamicMessageProvider dynamicMessageProvider) {
this.configValueUtil = configValueUtil;
this.dynamicMessageProvider = dynamicMessageProvider;
// 执行定时清理
tenantMessageProperties.schedulePrune(CACHE_TIME);
}
public void removeTenantCache(String tenantId) {
tenantMessageProperties.remove(tenantId);
}
public void loadTenantMessage(String tenantId, Locale locale) {
Properties i18nListProperties = dynamicMessageProvider.getI18nListProperties(locale);
TimedCache<String, Properties> tenantProperties = tenantMessageProperties.get(tenantId);
if (tenantProperties == null) {
tenantProperties = CacheUtil.newTimedCache(CACHE_TIME);
tenantMessageProperties.put(tenantId, tenantProperties);
}
if (i18nListProperties == null) {
// 语言加载失败, 一分钟后重新加载, 避免高频请求
tenantProperties.put(locale.toLanguageTag(), emptyProperties, 60000L);
} else {
// 无论是否有内容返回都是加载成功, 如果properties为空没有翻译数据, 服务端新增翻译时会清空缓存重新加载
tenantProperties.put(locale.toLanguageTag(), i18nListProperties);
}
}
@Override
public MCode getI18nMessage(String code, Locale locale) {
UserInfo userInfo = UserProvider.getUser();
// 未登录不获取
if (userInfo.getToken() == null) {
return null;
}
// 默认租户或者当前租户
String tenantId = GlobalConst.DEFAULT_TENANT_VALUE;
if (configValueUtil.isMultiTenancy()) {
tenantId = TenantHolder.getDatasourceId();
}
// 开启租户未获取到租户 不进行翻译获取
if (tenantId != null) {
// 租户配置中的语言配置
String languageTag = locale.toLanguageTag();
if (!tenantMessageProperties.containsKey(tenantId) || !tenantMessageProperties.get(tenantId).containsKey(languageTag)) {
// 租户其他线程正在加载多语言则直接返回
ReentrantLock lock = lockMap.computeIfAbsent(tenantId, k -> new ReentrantLock());
boolean isLock = false;
try {
isLock = lock.tryLock();
if (isLock) {
loadTenantMessage(tenantId, locale);
} else {
log.debug("[{}]语言加载中, 直接返回", tenantId);
return null;
}
} finally {
if (isLock) {
lock.unlock();
}
}
}
Properties languageProperty = tenantMessageProperties.get(tenantId).get(languageTag, false);
if (languageProperty != null) {
Object message = languageProperty.get(code);
if (message != null) {
return new MCode("", code, message.toString());
}
}
}
return null;
}
}