feat:修复已知问题,提升系统稳定性
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
package ai.remi.demo.web;
|
||||
package ai.remi.boot.web;
|
||||
|
||||
import ai.remi.comm.feign.annotation.EnableRemiFeignClients;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
@@ -10,7 +10,7 @@ import org.springframework.jmx.support.RegistrationPolicy;
|
||||
/**
|
||||
* 系统服务启动程序
|
||||
*/
|
||||
//@EnableLogRecord
|
||||
//@EnableLogRecord
|
||||
@EnableDiscoveryClient
|
||||
@EnableRemiFeignClients
|
||||
@SpringBootApplication(scanBasePackages = "ai.remi")
|
||||
@@ -18,10 +18,11 @@ import org.springframework.jmx.support.RegistrationPolicy;
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
//System.setProperty("cfg.env","local");
|
||||
|
||||
SpringApplication.run(Application.class, args);
|
||||
|
||||
System.out.println("(♥◠‿◠)ノ゙ Sdt Boot Web Startup Completed! ლ(´ڡ`ლ)゙ ");
|
||||
System.out.println("(♥◠‿◠)ノ゙ Remi Demo Web Startup Completed! ლ(´ڡ`ლ)゙ ");
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package ai.remi.boot.web.controller;
|
||||
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import ai.remi.comm.core.result.PagerBean;
|
||||
import ai.remi.comm.core.result.ResultBean;
|
||||
import ai.remi.comm.domain.query.PageQuery;
|
||||
import ai.remi.comm.exception.util.MessageUtils;
|
||||
import ai.remi.comm.redis.service.RedisService;
|
||||
import ai.remi.comm.util.asserts.AssertUtils;
|
||||
import ai.remi.comm.util.collection.CollectionUtils;
|
||||
import ai.remi.comm.util.id.SnowflakeUtils;
|
||||
import ai.remi.boot.domain.converter.UserConverter;
|
||||
import ai.remi.boot.domain.dto.post.UserPostDTO;
|
||||
import ai.remi.boot.domain.dto.put.UserPutDTO;
|
||||
import ai.remi.boot.domain.entity.User;
|
||||
import ai.remi.boot.domain.enums.SsoCacheEnum;
|
||||
import ai.remi.boot.domain.query.UserQuery;
|
||||
import ai.remi.boot.domain.vo.UserVO;
|
||||
import ai.remi.boot.infra.mapper.UserMapper;
|
||||
import ai.remi.boot.server.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author DianJiu
|
||||
* @email dianjiuxyz@gmail.com
|
||||
* @desc 用户表(User)控制层
|
||||
*/
|
||||
@Validated
|
||||
@RestController
|
||||
@Tag(name = "用户管理")
|
||||
@RequestMapping("user")
|
||||
public class UserController {
|
||||
|
||||
/**
|
||||
* 服务对象
|
||||
*/
|
||||
@Resource
|
||||
private RedisService redisService;
|
||||
@Resource
|
||||
private UserService userService;
|
||||
@Resource
|
||||
private UserMapper userMapper;
|
||||
@Resource
|
||||
private DeptUserService deptUserService;
|
||||
@Resource
|
||||
private UserPostService userPostService;
|
||||
|
||||
|
||||
/**
|
||||
* 通过主键查询单条数据
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 单条数据
|
||||
*/
|
||||
@GetMapping("{id}")
|
||||
@Operation(summary = "通过userId获取用户信息")
|
||||
public ResultBean<UserVO> get(@PathVariable @Validated @NotBlank(message = "ID不能为空") String id) {
|
||||
UserVO userVO = userService.getUserById(id);
|
||||
return ResultBean.success(userVO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过主键查询单条数据
|
||||
*
|
||||
* @param token 主键
|
||||
* @return 单条数据
|
||||
*/
|
||||
@GetMapping("token/{token}")
|
||||
@Operation(summary = "通过Token获取用户信息")
|
||||
public ResultBean<UserVO> getUserByToken(@PathVariable @Validated @NotBlank(message = "Token不能为空") String token) throws Exception {
|
||||
Map<Object, Object> hmget = redisService.hmget(SsoCacheEnum.USER_INFO.getKey(token));
|
||||
AssertUtils.isTrue(CollectionUtils.isNotEmpty(hmget), MessageUtils.getMessage("check.userinfo.token.error"));
|
||||
return ResultBean.success(BeanUtil.toBean(hmget, UserVO.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过实体不为空的属性作为筛选条件查询对象列表
|
||||
*
|
||||
* @param userQuery 实例对象
|
||||
* @return 对象列表
|
||||
*/
|
||||
@PostMapping(value = "/list")
|
||||
@Operation(summary = "查询所有")
|
||||
public ResultBean<List<UserVO>> list(@RequestBody UserQuery userQuery) {
|
||||
//执行分页查询
|
||||
List<UserVO> listResult = userMapper.selectListByUser(userQuery);
|
||||
return ResultBean.success(listResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询所有数据
|
||||
*
|
||||
* @param pageQuery 分页对象
|
||||
* @param userQuery 查询实体
|
||||
* @return 分页对象
|
||||
*/
|
||||
@PostMapping(value = "/page")
|
||||
@Operation(summary = "分页查询")
|
||||
public ResultBean<PagerBean<UserVO>> page(PageQuery pageQuery, UserQuery userQuery) {
|
||||
//查询总条数
|
||||
Long total = userMapper.selectCountByUser(userQuery);
|
||||
//执行分页查询
|
||||
List<UserVO> userVOS = userMapper.selectPageByUser(pageQuery.getPageNum() - 1, pageQuery.getPageSize(), userQuery);
|
||||
PagerBean<UserVO> pageBean = new PagerBean<>(total, Long.valueOf(pageQuery.getPageNum()), Long.valueOf(pageQuery.getPageSize()), userVOS);
|
||||
return ResultBean.success(pageBean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增数据
|
||||
*
|
||||
* @param userDTO 实体对象
|
||||
* @return 新增结果
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "新增数据")
|
||||
//@LogRecord(content = "录入用户信息", businessType = BusinessType.INSERT)
|
||||
@Transactional(rollbackFor = Exception.class, transactionManager = "transactionManager")
|
||||
public ResultBean<Boolean> insert(@RequestBody @Validated UserPostDTO userDTO) {
|
||||
//处理格式转换
|
||||
User user = UserConverter.INSTANT.postDtoToEntity(userDTO);
|
||||
user.setId(SnowflakeUtils.getInstance().nextIdStr());
|
||||
//保存部门用户中间表
|
||||
DeptUser deptUser = DeptUser.builder()
|
||||
.deptId(userDTO.getDeptId())
|
||||
.deptCode(userDTO.getDeptCode())
|
||||
.userId(user.getId()).userCode(user.getUserCode()).build();
|
||||
deptUserService.save(deptUser);
|
||||
// 保存用户职位职级(即岗位)
|
||||
UserPost userPost = UserPost.builder()
|
||||
.positionId(userDTO.getPositionId()).positionCode(userDTO.getPositionCode())
|
||||
.rankId(userDTO.getRankId()).rankCode(userDTO.getRankCode())
|
||||
.userId(user.getId()).userCode(user.getUserCode()).build();
|
||||
userPostService.save(userPost);
|
||||
//执行用户信息保存
|
||||
return ResultBean.success(userService.save(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改数据
|
||||
*
|
||||
* @param userDTO 实体对象
|
||||
* @return 修改结果
|
||||
*/
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "修改数据")
|
||||
//@LogRecord(content = "更新用户信息", businessType = BusinessType.UPDATE)
|
||||
@Transactional(rollbackFor = Exception.class, transactionManager = "transactionManager")
|
||||
public ResultBean<Boolean> update(@RequestBody @Validated UserPutDTO userDTO) {
|
||||
//处理格式转换
|
||||
User user = UserConverter.INSTANT.putDtoToEntity(userDTO);
|
||||
//保存公司部门中间表
|
||||
DeptUser deptUser = DeptUser.builder()
|
||||
.deptId(userDTO.getDeptId())
|
||||
.deptCode(userDTO.getDeptCode())
|
||||
.userId(user.getId()).userCode(user.getUserCode()).build();
|
||||
deptUserService.saveOrUpdate(deptUser, Wrappers.<DeptUser>lambdaUpdate().eq(DeptUser::getUserId, userDTO.getId()));
|
||||
// 保存用户职位职级(即岗位)
|
||||
UserPost userPost = UserPost.builder()
|
||||
.positionId(userDTO.getPositionId()).positionCode(userDTO.getPositionCode())
|
||||
.rankId(userDTO.getRankId()).rankCode(userDTO.getRankCode())
|
||||
.userId(user.getId()).userCode(user.getUserCode()).build();
|
||||
userPostService.saveOrUpdate(userPost, Wrappers.<UserPost>lambdaUpdate().eq(UserPost::getUserId, userDTO.getId()));
|
||||
//执行数据更新
|
||||
return ResultBean.success(userService.updateById(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
*
|
||||
* @param ids 主键结合
|
||||
* @return 删除结果
|
||||
*/
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除数据")
|
||||
//@LogRecord(content = "删除用户信息", businessType = BusinessType.DELETE)
|
||||
@Transactional(rollbackFor = Exception.class, transactionManager = "transactionManager")
|
||||
public ResultBean<Boolean> delete(@RequestParam("ids") List<String> ids) {
|
||||
for (String id : ids) {
|
||||
// 删除部门用户中间表
|
||||
LambdaQueryWrapper<DeptUser> deptUserQuery = Wrappers.lambdaQuery(DeptUser.class).eq(DeptUser::getUserId, id);
|
||||
deptUserService.remove(deptUserQuery);
|
||||
// 删除用户岗位中间表
|
||||
LambdaQueryWrapper<UserPost> userPostQuery = Wrappers.lambdaQuery(UserPost.class).eq(UserPost::getUserId, id);
|
||||
userPostService.remove(userPostQuery);
|
||||
}
|
||||
return ResultBean.success(userService.removeByIds(ids));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ spring:
|
||||
name: remi-demo-web
|
||||
profiles:
|
||||
active: dev
|
||||
#active: sit
|
||||
cloud:
|
||||
# nacos
|
||||
nacos:
|
||||
@@ -15,11 +16,14 @@ spring:
|
||||
password: nacos
|
||||
discovery:
|
||||
# 配置中心的地址
|
||||
server-addr: ${NACOS_HOST:192.168.31.204}:${NACOS_PORT:8848}
|
||||
#server-addr: ${NACOS_HOST:192.168.31.18}:${NACOS_PORT:8848}
|
||||
server-addr: ${NACOS_HOST:192.168.31.200}:${NACOS_PORT:30888}
|
||||
#命名空间
|
||||
namespace: ${SERVICE_NAME_SPACE_NAME:dev}
|
||||
#namespace: ${SERVICE_NAME_SPACE_NAME:sit}
|
||||
# 配置文件的环境
|
||||
group: ${SERVICE_DISCOVERY_GROUP_NAME:dev}
|
||||
#group: ${SERVICE_DISCOVERY_GROUP_NAME:sit}
|
||||
# 配置中心
|
||||
config:
|
||||
# 配置中心的地址
|
||||
@@ -42,4 +46,7 @@ spring:
|
||||
- dataId: remi-cloud-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
|
||||
group: ${spring.cloud.nacos.discovery.group}
|
||||
#是否自动刷新
|
||||
refresh: true
|
||||
refresh: true
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
# Author: DianJiu
|
||||
# 指定基础镜像,这是分阶段构建的前期阶段
|
||||
FROM harbor.njydsz.com/library/jdk:17 as builder
|
||||
# 维护者
|
||||
MAINTAINER DianJiu <dianjiuxyz@gmail.com>
|
||||
#VOLUME:挂载目录
|
||||
VOLUME /temp
|
||||
|
||||
#ADD 将app.jar文件将拷贝到容器中,默认拷贝到根路径下
|
||||
#也可以直接写成ADD app.jar /app.jar 或者 ADD app.jar /
|
||||
ADD remi-demo-web-3.1.1.jar remi-demo-web.jar
|
||||
ADD remi-demo-web-3.1.0.jar remi-demo-web.jar
|
||||
|
||||
#修改app.jar这个文件的访问时间和修改时间为当前时间,而不会修改文件的内容。
|
||||
RUN bash -c "touch /remi-demo-web.jar"
|
||||
@@ -21,4 +19,4 @@ RUN bash -c "touch /remi-demo-web.jar"
|
||||
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/remi-demo-web.jar"]
|
||||
|
||||
#EXPOSE 容器暴露端口
|
||||
EXPOSE 8080
|
||||
EXPOSE 1010
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
|
||||
## 如果基于Docker部署项目 直接运行如下脚本即可
|
||||
```shell
|
||||
sh ./docker_deploy.sh
|
||||
|
||||
docker logs -f remi-demo-web
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
docker stop remi-demo-web
|
||||
docker rm -f remi-demo-web
|
||||
docker rmi remi-demo-web
|
||||
#先删除原有的镜像
|
||||
docker images | grep remi-demo-web | awk '{print $3}' | xargs docker rmi -f
|
||||
docker build -t remi-demo-web .
|
||||
docker images
|
||||
docker run --restart=always -t -dit -p 8080:8080 -e TZ="Asia/Shanghai" --name remi-demo-web remi-demo-web
|
||||
docker ps
|
||||
docker images | grep remi-demo-web
|
||||
#把新镜像推送到私服
|
||||
docker login -uadmin -pRemi123 harbor.******.com
|
||||
docker tag remi-demo-web harbor.******.com/remi-top/remi-demo-web:3.1.0
|
||||
docker tag remi-demo-web harbor.******.com/remi-top/remi-demo-web
|
||||
docker push harbor.******.com/remi-top/remi-demo-web:3.1.0
|
||||
docker push harbor.******.com/remi-top/remi-demo-web
|
||||
#基于Docker启动容器
|
||||
docker run --restart=always -t -dit -p 1010:1010 --name remi-demo-web harbor.******.com/remi-top/remi-demo-web
|
||||
|
||||
@@ -5,6 +5,22 @@ not.logged.in=not logged in or token is invalid
|
||||
#****************************????****************************
|
||||
system.error=System error
|
||||
assert.error=Verify error
|
||||
add.dept.error=The same department already exists?
|
||||
delete.dept.error=There are sub-dept or user under the current dept that have not been removed?
|
||||
delete.company.error=There are sub-company or dept under the current company that have not been removed?
|
||||
only.support.code.error=Only the authorization code method is supported
|
||||
check.app.secret.error=Verify the appSecret is exceptional
|
||||
check.auth.code.error=The code is incorrect or out of date
|
||||
check.userinfo.token.error=The Token is incorrect or out of date
|
||||
check.ticket.error=No ticket exist, please log in again
|
||||
app.code.error=Generate an AppCode that collides more than 10 times, please regenerate it
|
||||
add.menu.error=The same menu already exists?
|
||||
add.groupUser.error=The same user {0} already exists?
|
||||
add.roleUser.error=The same user {0} already exists?
|
||||
add.roleCord.error=The same cord {0} already exists?
|
||||
add.rolePost.error=The same post {0} already exists?
|
||||
add.roleGroup.error=The same group {0} already exists?
|
||||
check.login.error=The username or password is wrong, please check and try again!
|
||||
#****************************????****************************
|
||||
other.system.error=Other system error
|
||||
call.feign.error=Call feign error
|
||||
|
||||
@@ -6,6 +6,22 @@ not.logged.in=未登录或Token失效
|
||||
#****************************系统异常****************************
|
||||
system.error=系统异常
|
||||
assert.error=校验失败
|
||||
add.dept.error=当前公司下已存在相同部门,请检查部门名称和部门编码!
|
||||
delete.dept.error=当前部门下有子部门或员工未移除,不允许直接被删除!
|
||||
delete.company.error=当前公司下有子子公司或部门未移除,不允许直接被删除!
|
||||
only.support.code.error=仅支持授权码方式
|
||||
check.app.secret.error=校验appSecret异常
|
||||
check.auth.code.error=code有误或已过期
|
||||
check.userinfo.token.error=token有误或已过期
|
||||
check.ticket.error=不存在凭证,请重新登陆
|
||||
app.code.error=尝试生成AppCode发生碰撞超过10次,请重新生成!
|
||||
add.menu.error=当前应用下已存在相同菜单,请检查菜单名称和菜单编码!
|
||||
add.groupUser.error=当前用户组下已存在该用户,请检查用户:{0}!
|
||||
add.roleUser.error=当前角色下已存在该用户,请检查用户:{0}!
|
||||
add.roleCord.error=当前角色下已存在该组织,请检查组织:{0}!
|
||||
add.rolePost.error=当前角色下已存在该岗位,请检查岗位:{0}!
|
||||
add.roleGroup.error=当前角色下已存在该用户组,请检查用户组:{0}!
|
||||
check.login.error=用户名或密码错误,请检查后重试!
|
||||
#****************************三方异常****************************
|
||||
other.system.error=三方系统异常
|
||||
call.feign.error=调用Feign异常
|
||||
|
||||
22
remi-demo/remi-demo-web/src/main/resources/k8s/Dockerfile
Normal file
22
remi-demo/remi-demo-web/src/main/resources/k8s/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# Docker image for springboot application
|
||||
# VERSION 0.0.1
|
||||
# Author: DianJiu
|
||||
# 指定基础镜像,这是分阶段构建的前期阶段
|
||||
FROM harbor.njydsz.com/library/jdk:17 as builder
|
||||
#VOLUME:挂载目录
|
||||
VOLUME /temp
|
||||
|
||||
#ADD 将app.jar文件将拷贝到容器中,默认拷贝到根路径下
|
||||
#也可以直接写成ADD app.jar /app.jar 或者 ADD app.jar /
|
||||
ADD remi-demo-web-3.1.0.jar remi-demo-web.jar
|
||||
|
||||
#修改app.jar这个文件的访问时间和修改时间为当前时间,而不会修改文件的内容。
|
||||
RUN bash -c "touch /remi-demo-web.jar"
|
||||
|
||||
#ENTRYPOINT 应用启动命令 参数设定
|
||||
#由于tomcat的session id的生成主要通过java.security.SecureRandom生成随机数来实现,导致启动过程很慢
|
||||
#通过-Djava.security.egd=file:/dev/./urandom参数可以加快随机数产生过程
|
||||
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/remi-demo-web.jar"]
|
||||
|
||||
#EXPOSE 容器暴露端口
|
||||
EXPOSE 1010
|
||||
13
remi-demo/remi-demo-web/src/main/resources/k8s/README.md
Normal file
13
remi-demo/remi-demo-web/src/main/resources/k8s/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
## 如果基于K8S部署项目 直接运行如下脚本即可
|
||||
```shell
|
||||
sh ./docker_deploy.sh
|
||||
|
||||
sh ./k8s_deploy.sh
|
||||
|
||||
kubectl get pods -n remi-top
|
||||
|
||||
kubectl describe pod remi-demo-web -n remi-top
|
||||
|
||||
kubectl get deployment remi-demo-web -n remi-top -o yaml
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
## Service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: remi-demo-web
|
||||
labels:
|
||||
app: remi-demo-web
|
||||
spec:
|
||||
type: NodePort
|
||||
ports:
|
||||
- name: remi-demo-web
|
||||
port: 1010
|
||||
protocol: TCP
|
||||
targetPort: 1010
|
||||
nodePort: 30010
|
||||
selector:
|
||||
app: remi-demo-web
|
||||
---
|
||||
## Deployment
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: remi-demo-web
|
||||
labels:
|
||||
app: remi-demo-web
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: remi-demo-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: remi-demo-web
|
||||
spec:
|
||||
containers:
|
||||
- image: harbor.******.com/remi-top/remi-demo-web
|
||||
imagePullPolicy: Always
|
||||
name: remi-demo-web
|
||||
ports:
|
||||
- containerPort: 1010
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: NACOS_HOST
|
||||
value: 192.168.31.200
|
||||
- name: NACOS_PORT
|
||||
value: "30888"
|
||||
- name: NAME_SPACE
|
||||
value: dev
|
||||
- name: GROUP_NAME
|
||||
value: dev
|
||||
- name: spring.application.name
|
||||
value: remi-sys-web
|
||||
- name: spring.profiles.active
|
||||
value: dev
|
||||
resources:
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: "500Mi"
|
||||
requests:
|
||||
cpu: "0.2"
|
||||
memory: "200Mi"
|
||||
volumeMounts:
|
||||
- name: logs
|
||||
mountPath: /logs
|
||||
volumes:
|
||||
- name: logs
|
||||
persistentVolumeClaim:
|
||||
claimName: remi-log
|
||||
@@ -0,0 +1,10 @@
|
||||
#先删除原有的镜像
|
||||
docker images | grep remi-demo-web | awk '{print $3}' | xargs docker rmi -f
|
||||
docker build -t remi-demo-web .
|
||||
docker images | grep remi-demo-web
|
||||
#把新镜像推送到私服
|
||||
docker login -uadmin -pRemi123 harbor.******.com
|
||||
docker tag remi-demo-web harbor.******.com/remi-top/remi-demo-web:3.1.0
|
||||
docker tag remi-demo-web harbor.******.com/remi-top/remi-demo-web
|
||||
docker push harbor.******.com/remi-top/remi-demo-web:3.1.0
|
||||
docker push harbor.******.com/remi-top/remi-demo-web
|
||||
@@ -0,0 +1,5 @@
|
||||
#先删除原有的Pod
|
||||
kubectl delete deployment remi-demo-web -n remi-top --grace-period=0 --force
|
||||
kubectl delete svc remi-demo-web -n remi-top
|
||||
#启动新的Pod
|
||||
kubectl create -f remi-demo-web.yaml -n remi-top
|
||||
Reference in New Issue
Block a user