数据库问文档重构
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package com.zyplayer.doc.db.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.zyplayer.doc.core.annotation.AuthMan;
|
||||
import com.zyplayer.doc.data.config.security.DocUserDetails;
|
||||
import com.zyplayer.doc.data.config.security.DocUserUtil;
|
||||
import com.zyplayer.doc.data.repository.manage.entity.DbDatasource;
|
||||
import com.zyplayer.doc.data.service.manage.DbDatasourceService;
|
||||
import com.zyplayer.doc.db.framework.configuration.DatasourceUtil;
|
||||
import com.zyplayer.doc.db.framework.db.bean.DatabaseFactoryBean;
|
||||
import com.zyplayer.doc.db.framework.db.bean.DatabaseRegistrationBean;
|
||||
import com.zyplayer.doc.db.framework.json.DocDbResponseJson;
|
||||
import com.zyplayer.doc.db.framework.json.ResponseJson;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 数据源控制器
|
||||
*
|
||||
* @author 暮光:城中城
|
||||
* @since 2019年6月29日
|
||||
*/
|
||||
@AuthMan
|
||||
@RestController
|
||||
@RequestMapping("/zyplayer-doc-db/datasource")
|
||||
public class DbDatasourceController {
|
||||
|
||||
@Resource
|
||||
DatabaseRegistrationBean databaseRegistrationBean;
|
||||
@Resource
|
||||
DbDatasourceService dbDatasourceService;
|
||||
|
||||
@PostMapping(value = "/list")
|
||||
public ResponseJson list() {
|
||||
QueryWrapper<DbDatasource> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("yn", 1);
|
||||
List<DbDatasource> datasourceList = dbDatasourceService.list(wrapper);
|
||||
return DocDbResponseJson.ok(datasourceList);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/update")
|
||||
public ResponseJson update(DbDatasource dbDatasource) {
|
||||
if (StringUtils.isBlank(dbDatasource.getDriverClassName())) {
|
||||
return DocDbResponseJson.warn("驱动类必选");
|
||||
} else if (StringUtils.isBlank(dbDatasource.getSourceUrl())) {
|
||||
return DocDbResponseJson.warn("地址必填");
|
||||
} else if (StringUtils.isBlank(dbDatasource.getSourceName())) {
|
||||
return DocDbResponseJson.warn("用户名必填");
|
||||
} else if (StringUtils.isBlank(dbDatasource.getSourcePassword())) {
|
||||
return DocDbResponseJson.warn("密码必填");
|
||||
}
|
||||
Long sourceId = Optional.ofNullable(dbDatasource.getId()).orElse(0L);
|
||||
if (sourceId > 0) {
|
||||
dbDatasourceService.updateById(dbDatasource);
|
||||
} else {
|
||||
DocUserDetails currentUser = DocUserUtil.getCurrentUser();
|
||||
dbDatasource.setCreateTime(new Date());
|
||||
dbDatasource.setCreateUserId(currentUser.getUserId());
|
||||
dbDatasource.setYn(1);
|
||||
dbDatasourceService.save(dbDatasource);
|
||||
}
|
||||
List<DatabaseFactoryBean> newFactoryBeanList = new LinkedList<>();
|
||||
List<DatabaseFactoryBean> databaseFactoryBeanList = databaseRegistrationBean.getDatabaseFactoryBeanList();
|
||||
for (DatabaseFactoryBean factoryBean : databaseFactoryBeanList) {
|
||||
if (Objects.equals(factoryBean.getId(), sourceId)) {
|
||||
try {
|
||||
// 关闭旧的数据源
|
||||
factoryBean.getDataSource().close();
|
||||
factoryBean.getDataSource().destroy();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
newFactoryBeanList.add(factoryBean);
|
||||
}
|
||||
}
|
||||
// 创建新的数据源
|
||||
DatabaseFactoryBean databaseFactoryBean = DatasourceUtil.createDatabaseFactoryBean(dbDatasource);
|
||||
if (databaseFactoryBean != null) {
|
||||
newFactoryBeanList.add(databaseFactoryBean);
|
||||
}
|
||||
databaseRegistrationBean.setDatabaseFactoryBeanList(newFactoryBeanList);
|
||||
|
||||
if (databaseFactoryBean == null) {
|
||||
return DocDbResponseJson.warn("创建数据源失败,请检查配置是否正确");
|
||||
}
|
||||
return DocDbResponseJson.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,24 @@
|
||||
package com.zyplayer.doc.db.framework.configuration;
|
||||
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import com.zyplayer.doc.db.framework.db.bean.DbConfigBean;
|
||||
import com.zyplayer.doc.db.framework.db.interceptor.SqlLogInterceptor;
|
||||
import org.apache.ibatis.plugin.Interceptor;
|
||||
import org.mybatis.spring.SqlSessionFactoryBean;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.zyplayer.doc.data.repository.manage.entity.DbDatasource;
|
||||
import com.zyplayer.doc.data.service.manage.DbDatasourceService;
|
||||
import com.zyplayer.doc.db.framework.db.bean.DatabaseFactoryBean;
|
||||
import com.zyplayer.doc.db.framework.db.bean.DatabaseRegistrationBean;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.zyplayer.doc.db.framework.db.bean.DatabaseFactoryBean;
|
||||
import com.zyplayer.doc.db.framework.db.bean.DatabaseFactoryBean.DatabaseProduct;
|
||||
import com.zyplayer.doc.db.framework.db.bean.DatabaseRegistrationBean;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class ApplicationListenerBean implements ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
@Autowired
|
||||
@javax.annotation.Resource
|
||||
DatabaseRegistrationBean databaseRegistrationBean;
|
||||
@javax.annotation.Resource
|
||||
DbDatasourceService dbDatasourceService;
|
||||
|
||||
private volatile static boolean IS_INIT = false;
|
||||
|
||||
@@ -41,76 +29,15 @@ public class ApplicationListenerBean implements ApplicationListener<ContextRefre
|
||||
}
|
||||
// 会被调用两次
|
||||
IS_INIT = true;
|
||||
Integer dataSourceIndex = 0;
|
||||
SqlLogInterceptor sqlLogInterceptor = new SqlLogInterceptor();
|
||||
List<DatabaseFactoryBean> databaseFactoryBeanList = new LinkedList<>();
|
||||
for (DbConfigBean dbConfigBean : databaseRegistrationBean.getDbConfigList()) {
|
||||
try {
|
||||
// 数据源配置
|
||||
Properties xaProperties = new Properties();
|
||||
xaProperties.setProperty("driverClassName", dbConfigBean.getDriverClassName());
|
||||
xaProperties.setProperty("url", dbConfigBean.getUrl());
|
||||
xaProperties.setProperty("username", dbConfigBean.getUsername());
|
||||
xaProperties.setProperty("password", dbConfigBean.getPassword());
|
||||
xaProperties.setProperty("maxActive", "500");
|
||||
xaProperties.setProperty("testOnBorrow", "true");
|
||||
xaProperties.setProperty("testWhileIdle", "true");
|
||||
xaProperties.setProperty("validationQuery", "select 'x'");
|
||||
// 数据源
|
||||
AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
|
||||
dataSource.setXaProperties(xaProperties);
|
||||
dataSource.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
|
||||
dataSource.setUniqueResourceName("zyplayer-doc-db" + (dataSourceIndex++));
|
||||
dataSource.setMaxPoolSize(500);
|
||||
dataSource.setMinPoolSize(1);
|
||||
dataSource.setMaxLifetime(60);
|
||||
// 描述连接信息的对象
|
||||
DatabaseFactoryBean databaseFactoryBean = new DatabaseFactoryBean();
|
||||
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
|
||||
String productName = metaData.getDatabaseProductName().toLowerCase();
|
||||
Resource[] resources = null;
|
||||
String dbUrl = metaData.getURL();
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
if (productName.indexOf("mysql") >= 0) {
|
||||
// jdbc:mysql://192.168.0.1:3306/user_info?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
|
||||
String[] urlParamArr = dbUrl.split("\\?");
|
||||
String[] urlDbNameArr = urlParamArr[0].split("/");
|
||||
if (urlDbNameArr.length >= 2) {
|
||||
databaseFactoryBean.setDbName(urlDbNameArr[urlDbNameArr.length - 1]);
|
||||
databaseFactoryBean.setHost(urlDbNameArr[urlDbNameArr.length - 2]);
|
||||
}
|
||||
databaseFactoryBean.setDatabaseProduct(DatabaseProduct.MYSQL);
|
||||
resources = resolver.getResources("classpath:com/zyplayer/doc/db/framework/db/mapper/mysql/*.xml");
|
||||
} else if (productName.indexOf("sql server") >= 0) {
|
||||
// jdbc:jtds:sqlserver://192.168.0.1:33434;socketTimeout=60;DatabaseName=user_info;
|
||||
String[] urlParamArr = dbUrl.split(";");
|
||||
String[] urlDbNameArr = urlParamArr[0].split("/");
|
||||
databaseFactoryBean.setHost(urlDbNameArr[urlDbNameArr.length - 1]);
|
||||
for (String urlParam : urlParamArr) {
|
||||
String[] keyValArr = urlParam.split("=");
|
||||
if (keyValArr.length >= 2 && keyValArr[0].equalsIgnoreCase("DatabaseName")) {
|
||||
databaseFactoryBean.setDbName(keyValArr[1]);
|
||||
}
|
||||
}
|
||||
databaseFactoryBean.setDatabaseProduct(DatabaseProduct.SQLSERVER);
|
||||
resources = resolver.getResources("classpath:com/zyplayer/doc/db/framework/db/mapper/sqlserver/*.xml");
|
||||
}
|
||||
if (resources == null) {
|
||||
continue;
|
||||
}
|
||||
// 创建sqlSessionTemplate
|
||||
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
|
||||
sqlSessionFactoryBean.setDataSource(dataSource);
|
||||
sqlSessionFactoryBean.setMapperLocations(resources);
|
||||
sqlSessionFactoryBean.setPlugins(new Interceptor[]{sqlLogInterceptor});
|
||||
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactoryBean.getObject());
|
||||
// 组装自定义的bean
|
||||
databaseFactoryBean.setDataSource(dataSource);
|
||||
databaseFactoryBean.setSqlSessionTemplate(sqlSessionTemplate);
|
||||
databaseFactoryBean.setUrl(dbUrl);
|
||||
|
||||
QueryWrapper<DbDatasource> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("yn", 1);
|
||||
List<DbDatasource> datasourceList = dbDatasourceService.list(wrapper);
|
||||
for (DbDatasource dbDatasource : datasourceList) {
|
||||
DatabaseFactoryBean databaseFactoryBean = DatasourceUtil.createDatabaseFactoryBean(dbDatasource);
|
||||
if (databaseFactoryBean != null) {
|
||||
databaseFactoryBeanList.add(databaseFactoryBean);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
databaseRegistrationBean.setDatabaseFactoryBeanList(databaseFactoryBeanList);
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.zyplayer.doc.db.framework.configuration;
|
||||
|
||||
import com.zyplayer.doc.data.repository.manage.entity.DbDatasource;
|
||||
import com.zyplayer.doc.db.framework.db.bean.DatabaseFactoryBean;
|
||||
import com.zyplayer.doc.db.framework.db.interceptor.SqlLogInterceptor;
|
||||
import org.apache.ibatis.plugin.Interceptor;
|
||||
import org.mybatis.spring.SqlSessionFactoryBean;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.util.Properties;
|
||||
|
||||
public class DatasourceUtil {
|
||||
private static SqlLogInterceptor sqlLogInterceptor = new SqlLogInterceptor();
|
||||
|
||||
public static DatabaseFactoryBean createDatabaseFactoryBean(DbDatasource dbDatasource){
|
||||
try {
|
||||
// 数据源配置
|
||||
Properties xaProperties = new Properties();
|
||||
xaProperties.setProperty("driverClassName", dbDatasource.getDriverClassName());
|
||||
xaProperties.setProperty("url", dbDatasource.getSourceUrl());
|
||||
xaProperties.setProperty("username", dbDatasource.getSourceName());
|
||||
xaProperties.setProperty("password", dbDatasource.getSourcePassword());
|
||||
xaProperties.setProperty("maxActive", "500");
|
||||
xaProperties.setProperty("breakAfterAcquireFailure", "true");
|
||||
xaProperties.setProperty("testOnBorrow", "true");
|
||||
xaProperties.setProperty("testWhileIdle", "true");
|
||||
xaProperties.setProperty("validationQuery", "select 'x'");
|
||||
// 数据源
|
||||
AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
|
||||
dataSource.setXaProperties(xaProperties);
|
||||
dataSource.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
|
||||
dataSource.setUniqueResourceName("zyplayer-doc-db" + dbDatasource.getId());
|
||||
dataSource.setMaxPoolSize(500);
|
||||
dataSource.setMinPoolSize(1);
|
||||
dataSource.setMaxLifetime(60);
|
||||
// 描述连接信息的对象
|
||||
DatabaseFactoryBean databaseFactoryBean = new DatabaseFactoryBean();
|
||||
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
|
||||
String productName = metaData.getDatabaseProductName().toLowerCase();
|
||||
Resource[] resources = null;
|
||||
String dbUrl = metaData.getURL();
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
if (productName.indexOf("mysql") >= 0) {
|
||||
// jdbc:mysql://192.168.0.1:3306/user_info?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
|
||||
String[] urlParamArr = dbUrl.split("\\?");
|
||||
String[] urlDbNameArr = urlParamArr[0].split("/");
|
||||
if (urlDbNameArr.length >= 2) {
|
||||
databaseFactoryBean.setDbName(urlDbNameArr[urlDbNameArr.length - 1]);
|
||||
databaseFactoryBean.setHost(urlDbNameArr[urlDbNameArr.length - 2]);
|
||||
}
|
||||
databaseFactoryBean.setDatabaseProduct(DatabaseFactoryBean.DatabaseProduct.MYSQL);
|
||||
resources = resolver.getResources("classpath:com/zyplayer/doc/db/framework/db/mapper/mysql/*.xml");
|
||||
} else if (productName.indexOf("sql server") >= 0) {
|
||||
// jdbc:jtds:sqlserver://192.168.0.1:33434;socketTimeout=60;DatabaseName=user_info;
|
||||
String[] urlParamArr = dbUrl.split(";");
|
||||
String[] urlDbNameArr = urlParamArr[0].split("/");
|
||||
databaseFactoryBean.setHost(urlDbNameArr[urlDbNameArr.length - 1]);
|
||||
for (String urlParam : urlParamArr) {
|
||||
String[] keyValArr = urlParam.split("=");
|
||||
if (keyValArr.length >= 2 && keyValArr[0].equalsIgnoreCase("DatabaseName")) {
|
||||
databaseFactoryBean.setDbName(keyValArr[1]);
|
||||
}
|
||||
}
|
||||
databaseFactoryBean.setDatabaseProduct(DatabaseFactoryBean.DatabaseProduct.SQLSERVER);
|
||||
resources = resolver.getResources("classpath:com/zyplayer/doc/db/framework/db/mapper/sqlserver/*.xml");
|
||||
}
|
||||
if (resources == null) {
|
||||
return null;
|
||||
}
|
||||
// 创建sqlSessionTemplate
|
||||
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
|
||||
sqlSessionFactoryBean.setDataSource(dataSource);
|
||||
sqlSessionFactoryBean.setMapperLocations(resources);
|
||||
sqlSessionFactoryBean.setPlugins(new Interceptor[]{sqlLogInterceptor});
|
||||
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactoryBean.getObject());
|
||||
// 组装自定义的bean
|
||||
databaseFactoryBean.setId(dbDatasource.getId());
|
||||
databaseFactoryBean.setDataSource(dataSource);
|
||||
databaseFactoryBean.setSqlSessionTemplate(sqlSessionTemplate);
|
||||
databaseFactoryBean.setUrl(dbUrl);
|
||||
return databaseFactoryBean;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,80 @@
|
||||
package com.zyplayer.doc.db.framework.db.bean;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
|
||||
/**
|
||||
* 描述连接信息的对象
|
||||
* @author 暮光:城中城
|
||||
* @since 2018年8月8日
|
||||
*/
|
||||
public class DatabaseFactoryBean {
|
||||
private DataSource dataSource;
|
||||
private SqlSessionTemplate sqlSessionTemplate;
|
||||
private String url;
|
||||
private String host;
|
||||
private String dbName;
|
||||
private DatabaseProduct databaseProduct;
|
||||
|
||||
public static enum DatabaseProduct {
|
||||
MYSQL, SQLSERVER
|
||||
}
|
||||
|
||||
public DataSource getDataSource() {
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
public void setDataSource(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public String getDbName() {
|
||||
return dbName;
|
||||
}
|
||||
|
||||
public void setDbName(String dbName) {
|
||||
this.dbName = dbName;
|
||||
}
|
||||
|
||||
public DatabaseProduct getDatabaseProduct() {
|
||||
return databaseProduct;
|
||||
}
|
||||
|
||||
public void setDatabaseProduct(DatabaseProduct databaseProduct) {
|
||||
this.databaseProduct = databaseProduct;
|
||||
}
|
||||
|
||||
public SqlSessionTemplate getSqlSessionTemplate() {
|
||||
return sqlSessionTemplate;
|
||||
}
|
||||
|
||||
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
|
||||
this.sqlSessionTemplate = sqlSessionTemplate;
|
||||
}
|
||||
|
||||
}
|
||||
package com.zyplayer.doc.db.framework.db.bean;
|
||||
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
|
||||
|
||||
/**
|
||||
* 描述连接信息的对象
|
||||
* @author 暮光:城中城
|
||||
* @since 2018年8月8日
|
||||
*/
|
||||
public class DatabaseFactoryBean {
|
||||
private Long id;
|
||||
private AtomikosDataSourceBean dataSource;
|
||||
private SqlSessionTemplate sqlSessionTemplate;
|
||||
private String url;
|
||||
private String host;
|
||||
private String dbName;
|
||||
private DatabaseProduct databaseProduct;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public static enum DatabaseProduct {
|
||||
MYSQL, SQLSERVER
|
||||
}
|
||||
|
||||
public AtomikosDataSourceBean getDataSource() {
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
public void setDataSource(AtomikosDataSourceBean dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public String getDbName() {
|
||||
return dbName;
|
||||
}
|
||||
|
||||
public void setDbName(String dbName) {
|
||||
this.dbName = dbName;
|
||||
}
|
||||
|
||||
public DatabaseProduct getDatabaseProduct() {
|
||||
return databaseProduct;
|
||||
}
|
||||
|
||||
public void setDatabaseProduct(DatabaseProduct databaseProduct) {
|
||||
this.databaseProduct = databaseProduct;
|
||||
}
|
||||
|
||||
public SqlSessionTemplate getSqlSessionTemplate() {
|
||||
return sqlSessionTemplate;
|
||||
}
|
||||
|
||||
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
|
||||
this.sqlSessionTemplate = sqlSessionTemplate;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user