1. 统一包名修改
This commit is contained in:
66
zt-framework/zt-spring-boot-starter-env/pom.xml
Normal file
66
zt-framework/zt-spring-boot-starter-env/pom.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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">
|
||||
<parent>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-framework</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>zt-spring-boot-starter-env</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
开发环境拓展,实现类似阿里的特性环境的能力
|
||||
1. https://segmentfault.com/a/1190000018022987
|
||||
</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring 核心 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- RPC 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Registry 注册中心相关 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.zt.plat.framework.env.config;
|
||||
|
||||
import com.zt.plat.framework.env.core.fegin.EnvLoadBalancerClientFactory;
|
||||
import com.zt.plat.framework.env.core.fegin.EnvRequestInterceptor;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification;
|
||||
import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 多环境的 RPC 组件的自动配置
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(EnvProperties.class)
|
||||
public class CloudEnvRpcAutoConfiguration {
|
||||
|
||||
// ========== Feign 相关 ==========
|
||||
|
||||
/**
|
||||
* 创建 {@link EnvLoadBalancerClientFactory} Bean
|
||||
*
|
||||
* 参考 {@link LoadBalancerAutoConfiguration#loadBalancerClientFactory(LoadBalancerClientsProperties)} 方法
|
||||
*/
|
||||
@Bean
|
||||
public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties,
|
||||
ObjectProvider<List<LoadBalancerClientSpecification>> configurations) {
|
||||
EnvLoadBalancerClientFactory clientFactory = new EnvLoadBalancerClientFactory(properties);
|
||||
clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList));
|
||||
return clientFactory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EnvRequestInterceptor envRequestInterceptor() {
|
||||
return new EnvRequestInterceptor();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.zt.plat.framework.env.config;
|
||||
|
||||
import com.zt.plat.framework.common.enums.WebFilterOrderEnum;
|
||||
import com.zt.plat.framework.env.core.web.EnvWebFilter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* 多环境的 Web 组件的自动配置
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
@EnableConfigurationProperties(EnvProperties.class)
|
||||
public class CloudEnvWebAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 创建 {@link EnvWebFilter} Bean
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<EnvWebFilter> envWebFilterFilter() {
|
||||
EnvWebFilter filter = new EnvWebFilter();
|
||||
FilterRegistrationBean<EnvWebFilter> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(WebFilterOrderEnum.ENV_TAG_FILTER);
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.zt.plat.framework.env.config;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.common.util.collection.SetUtils;
|
||||
import com.zt.plat.framework.env.core.util.EnvUtils;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.env.EnvironmentPostProcessor;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static com.zt.plat.framework.env.core.util.EnvUtils.HOST_NAME_VALUE;
|
||||
|
||||
/**
|
||||
* 多环境的 {@link EnvEnvironmentPostProcessor} 实现类
|
||||
* 将 cloud.env.tag 设置到 nacos 等组件对应的 tag 配置项,当且仅当它们不存在时
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class EnvEnvironmentPostProcessor implements EnvironmentPostProcessor {
|
||||
|
||||
private static final Set<String> TARGET_TAG_KEYS = SetUtils.asSet(
|
||||
"spring.cloud.nacos.discovery.metadata.tag" // Nacos 注册中心
|
||||
// MQ TODO
|
||||
);
|
||||
|
||||
@Override
|
||||
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
|
||||
// 0. 设置 ${HOST_NAME} 兜底的环境变量
|
||||
String hostNameKey = StrUtil.subBetween(HOST_NAME_VALUE, "{", "}");
|
||||
if (!environment.containsProperty(hostNameKey)) {
|
||||
environment.getSystemProperties().put(hostNameKey, EnvUtils.getHostName());
|
||||
}
|
||||
|
||||
// 1.1 如果没有 cloud.env.tag 配置项,则不进行配置项的修改
|
||||
String tag = EnvUtils.getTag(environment);
|
||||
if (StrUtil.isEmpty(tag)) {
|
||||
return;
|
||||
}
|
||||
// 1.2 需要修改的配置项
|
||||
for (String targetTagKey : TARGET_TAG_KEYS) {
|
||||
String targetTagValue = environment.getProperty(targetTagKey);
|
||||
if (StrUtil.isNotEmpty(targetTagValue)) {
|
||||
continue;
|
||||
}
|
||||
environment.getSystemProperties().put(targetTagKey, tag);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.zt.plat.framework.env.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 环境配置
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "cloud.env")
|
||||
@Data
|
||||
public class EnvProperties {
|
||||
|
||||
public static final String TAG_KEY = "cloud.env.tag";
|
||||
|
||||
/**
|
||||
* 环境标签
|
||||
*/
|
||||
private String tag;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.zt.plat.framework.env.core.context;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 开发环境上下文
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class EnvContextHolder {
|
||||
|
||||
/**
|
||||
* 标签的上下文
|
||||
*
|
||||
* 使用 {@link List} 的原因,可能存在多层设置或者清理
|
||||
*/
|
||||
private static final ThreadLocal<List<String>> TAG_CONTEXT = TransmittableThreadLocal.withInitial(ArrayList::new);
|
||||
|
||||
public static void setTag(String tag) {
|
||||
TAG_CONTEXT.get().add(tag);
|
||||
}
|
||||
|
||||
public static String getTag() {
|
||||
return CollUtil.getLast(TAG_CONTEXT.get());
|
||||
}
|
||||
|
||||
public static void removeTag() {
|
||||
List<String> tags = TAG_CONTEXT.get();
|
||||
if (CollUtil.isEmpty(tags)) {
|
||||
return;
|
||||
}
|
||||
tags.remove(tags.size() - 1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.zt.plat.framework.env.core.fegin;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.common.util.collection.CollectionUtils;
|
||||
import com.zt.plat.framework.env.core.context.EnvContextHolder;
|
||||
import com.zt.plat.framework.env.core.util.EnvUtils;
|
||||
import com.alibaba.cloud.nacos.balancer.NacosBalancer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.Request;
|
||||
import org.springframework.cloud.client.loadbalancer.Response;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
|
||||
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 多环境的 {@link org.springframework.cloud.client.loadbalancer.LoadBalancerClient} 实现类
|
||||
* 在从服务实例列表选择时,优先选择 tag 匹配的服务实例
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class EnvLoadBalancerClient implements ReactorServiceInstanceLoadBalancer {
|
||||
|
||||
/**
|
||||
* 用于获取 serviceId 对应的服务实例的列表
|
||||
*/
|
||||
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
|
||||
/**
|
||||
* 需要获取的服务实例名
|
||||
*
|
||||
* 暂时用于打印 logger 日志
|
||||
*/
|
||||
private final String serviceId;
|
||||
/**
|
||||
* 被代理的 ReactiveLoadBalancer 对象
|
||||
*/
|
||||
private final ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer;
|
||||
|
||||
@Override
|
||||
public Mono<Response<ServiceInstance>> choose(Request request) {
|
||||
// 情况一,没有 tag 时,使用默认的 reactiveLoadBalancer 实现负载均衡
|
||||
String tag = EnvContextHolder.getTag();
|
||||
if (StrUtil.isEmpty(tag)) {
|
||||
return Mono.from(reactiveLoadBalancer.choose(request));
|
||||
}
|
||||
|
||||
// 情况二,有 tag 时,使用 tag 匹配服务实例
|
||||
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
|
||||
return supplier.get(request).next().map(list -> getInstanceResponse(list, tag));
|
||||
}
|
||||
|
||||
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, String tag) {
|
||||
// 如果服务实例为空,则直接返回
|
||||
if (CollUtil.isEmpty(instances)) {
|
||||
log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId);
|
||||
return new EmptyResponse();
|
||||
}
|
||||
|
||||
// 筛选满足条件的实例列表
|
||||
List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> tag.equals(EnvUtils.getTag(instance)));
|
||||
if (CollUtil.isEmpty(chooseInstances)) {
|
||||
log.warn("[getInstanceResponse][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag);
|
||||
chooseInstances = instances;
|
||||
}
|
||||
|
||||
// TODO 芋艿:https://juejin.cn/post/7056770721858469896 想通网段
|
||||
|
||||
// 随机 + 权重获取实例列表 TODO 芋艿:目前直接使用 Nacos 提供的方法,如果替换注册中心,需要重新失败该方法
|
||||
return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.zt.plat.framework.env.core.fegin;
|
||||
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
|
||||
/**
|
||||
* 多环境的 {@link LoadBalancerClientFactory} 实现类
|
||||
* 目的:在创建 {@link ReactiveLoadBalancer} 时,会额外增加 {@link EnvLoadBalancerClient} 代理,用于 tag 过滤服务实例
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class EnvLoadBalancerClientFactory extends LoadBalancerClientFactory {
|
||||
|
||||
public EnvLoadBalancerClientFactory(LoadBalancerClientsProperties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
|
||||
ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer = super.getInstance(serviceId);
|
||||
// 参考 {@link com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancerClientConfiguration#nacosLoadBalancer(Environment, LoadBalancerClientFactory, NacosDiscoveryProperties)} 方法
|
||||
return new EnvLoadBalancerClient(super.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
|
||||
serviceId, reactiveLoadBalancer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.zt.plat.framework.env.core.fegin;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.env.core.context.EnvContextHolder;
|
||||
import com.zt.plat.framework.env.core.util.EnvUtils;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.RequestTemplate;
|
||||
|
||||
/**
|
||||
* 多环境的 {@link RequestInterceptor} 实现类:Feign 请求时,将 tag 设置到 header 中,继续透传给被调用的服务
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class EnvRequestInterceptor implements RequestInterceptor {
|
||||
|
||||
@Override
|
||||
public void apply(RequestTemplate requestTemplate) {
|
||||
String tag = EnvContextHolder.getTag();
|
||||
if (StrUtil.isNotEmpty(tag)) {
|
||||
EnvUtils.setTag(requestTemplate, tag);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package com.zt.plat.framework.env.core;
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.zt.plat.framework.env.core.util;
|
||||
|
||||
import com.zt.plat.framework.env.config.EnvProperties;
|
||||
import feign.RequestTemplate;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 环境 Utils
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class EnvUtils {
|
||||
|
||||
private static final String HEADER_TAG = "tag";
|
||||
|
||||
public static final String HOST_NAME_VALUE = "${HOSTNAME}";
|
||||
|
||||
public static String getTag(HttpServletRequest request) {
|
||||
String tag = request.getHeader(HEADER_TAG);
|
||||
// 如果请求的是 "${HOSTNAME}",则解析成对应的本地主机名
|
||||
// 目的:特殊逻辑,解决 IDEA Rest Client 不支持环境变量的读取,所以就服务器来做
|
||||
return Objects.equals(tag, HOST_NAME_VALUE) ? getHostName() : tag;
|
||||
}
|
||||
|
||||
public static String getTag(ServiceInstance instance) {
|
||||
return instance.getMetadata().get(HEADER_TAG);
|
||||
}
|
||||
|
||||
public static String getTag(Environment environment) {
|
||||
String tag = environment.getProperty(EnvProperties.TAG_KEY);
|
||||
// 如果请求的是 "${HOSTNAME}",则解析成对应的本地主机名
|
||||
// 目的:特殊逻辑,解决 IDEA Rest Client 不支持环境变量的读取,所以就服务器来做
|
||||
return Objects.equals(tag, HOST_NAME_VALUE) ? getHostName() : tag;
|
||||
}
|
||||
|
||||
public static void setTag(RequestTemplate requestTemplate, String tag) {
|
||||
requestTemplate.header(HEADER_TAG, tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得 hostname 主机名
|
||||
*
|
||||
* @return 主机名
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static String getHostName() {
|
||||
return InetAddress.getLocalHost().getHostName();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.zt.plat.framework.env.core.web;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.env.core.context.EnvContextHolder;
|
||||
import com.zt.plat.framework.env.core.util.EnvUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 环境的 {@link jakarta.servlet.Filter} 实现类
|
||||
* 当有 tag 请求头时,设置到 {@link EnvContextHolder} 的标签上下文
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class EnvWebFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
// 如果没有 tag,则走默认的流程
|
||||
String tag = EnvUtils.getTag(request);
|
||||
if (StrUtil.isEmpty(tag)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果有 tag,则设置到上下文
|
||||
EnvContextHolder.setTag(tag);
|
||||
try {
|
||||
chain.doFilter(request, response);
|
||||
} finally {
|
||||
EnvContextHolder.removeTag();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 开发环境拓展,实现类似阿里的特性环境的能力
|
||||
* 1. https://segmentfault.com/a/1190000018022987
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
package com.zt.plat.framework.env;
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
||||
com.zt.plat.framework.env.config.EnvEnvironmentPostProcessor
|
||||
@@ -0,0 +1,2 @@
|
||||
com.zt.plat.framework.env.config.CloudEnvWebAutoConfiguration
|
||||
com.zt.plat.framework.env.config.CloudEnvRpcAutoConfiguration
|
||||
Reference in New Issue
Block a user