mirror of
https://github.com/continew-org/continew-admin.git
synced 2025-09-23 03:00:58 +08:00
完善:完善用户登录 API,优化部分包结构(引入 MyBatis Plus、多数据源、P6Spy、Liquibase 等依赖,详情可见 README 介绍)
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.charles7c.cnadmin.common.config.mybatis;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
|
||||
import top.charles7c.cnadmin.common.exception.ServiceException;
|
||||
import top.charles7c.cnadmin.common.model.entity.BaseEntity;
|
||||
import top.charles7c.cnadmin.common.util.helper.LoginHelper;
|
||||
|
||||
/**
|
||||
* MyBatis Plus 元对象处理器配置(插入或修改时自动填充)
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/22 19:52
|
||||
*/
|
||||
public class MyBatisPlusMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/** 创建人 */
|
||||
private static final String CREATE_USER = "createUser";
|
||||
/** 创建时间 */
|
||||
private static final String CREATE_TIME = "createTime";
|
||||
/** 修改人 */
|
||||
private static final String UPDATE_USER = "updateUser";
|
||||
/** 修改时间 */
|
||||
private static final String UPDATE_TIME = "updateTime";
|
||||
|
||||
/**
|
||||
* 插入数据时填充
|
||||
*
|
||||
* @param metaObject
|
||||
* 元对象
|
||||
*/
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
try {
|
||||
if (ObjectUtil.isNull(metaObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long createUser = LoginHelper.getUserId();
|
||||
Date createTime = new Date();
|
||||
if (metaObject.getOriginalObject() instanceof BaseEntity) {
|
||||
// 继承了 BaseEntity 的类,填充创建信息
|
||||
BaseEntity baseEntity = (BaseEntity)metaObject.getOriginalObject();
|
||||
baseEntity.setCreateUser(baseEntity.getCreateUser() != null ? baseEntity.getCreateUser() : createUser);
|
||||
baseEntity.setCreateTime(baseEntity.getCreateTime() != null ? baseEntity.getCreateTime() : createTime);
|
||||
baseEntity.setUpdateUser(baseEntity.getUpdateUser() != null ? baseEntity.getUpdateUser() : createUser);
|
||||
baseEntity.setUpdateTime(baseEntity.getUpdateTime() != null ? baseEntity.getUpdateTime() : createTime);
|
||||
} else {
|
||||
// 未继承 BaseEntity 的类,根据类中拥有的创建信息进行填充,不存在创建信息不进行填充
|
||||
this.fillFieldValue(metaObject, CREATE_USER, createUser, false);
|
||||
this.fillFieldValue(metaObject, CREATE_TIME, createTime, false);
|
||||
this.fillFieldValue(metaObject, UPDATE_USER, createUser, false);
|
||||
this.fillFieldValue(metaObject, UPDATE_TIME, createTime, false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("插入数据时自动填充异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改数据时填充
|
||||
*
|
||||
* @param metaObject
|
||||
* 元对象
|
||||
*/
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
try {
|
||||
if (ObjectUtil.isNull(metaObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long updateUser = LoginHelper.getUserId();
|
||||
Date updateTime = new Date();
|
||||
if (metaObject.getOriginalObject() instanceof BaseEntity) {
|
||||
// 继承了 BaseEntity 的类,填充修改信息
|
||||
BaseEntity baseEntity = (BaseEntity)metaObject.getOriginalObject();
|
||||
baseEntity.setUpdateUser(updateUser);
|
||||
baseEntity.setUpdateTime(updateTime);
|
||||
} else {
|
||||
// 未继承 BaseEntity 的类,根据类中拥有的修改信息进行填充,不存在修改信息不进行填充
|
||||
this.fillFieldValue(metaObject, UPDATE_USER, updateUser, true);
|
||||
this.fillFieldValue(metaObject, UPDATE_TIME, updateTime, true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("修改数据时自动填充异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充属性值
|
||||
*
|
||||
* @param metaObject
|
||||
* 元数据对象
|
||||
* @param fieldName
|
||||
* 要填充的属性名
|
||||
* @param fillFieldValue
|
||||
* 要填充的属性值
|
||||
* @param isOverride
|
||||
* 如果属性值不为空,是否覆盖(true 覆盖、false 不覆盖)
|
||||
*/
|
||||
private void fillFieldValue(MetaObject metaObject, String fieldName, Object fillFieldValue, boolean isOverride) {
|
||||
if (metaObject.hasSetter(fieldName)) {
|
||||
Object fieldValue = metaObject.getValue(fieldName);
|
||||
setFieldValByName(fieldName, fieldValue != null && !isOverride ? fieldValue : fillFieldValue, metaObject);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.charles7c.cnadmin.common.config.mybatis;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
|
||||
import cn.hutool.core.net.NetUtil;
|
||||
|
||||
/**
|
||||
* MyBatis Plus 配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/22 19:51
|
||||
*/
|
||||
@Configuration
|
||||
@MapperScan("${mybatis-plus.mapper-package}")
|
||||
public class MybatisPlusConfiguration {
|
||||
|
||||
/**
|
||||
* 插件配置
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
@Bean
|
||||
MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 分页插件
|
||||
interceptor.addInnerInterceptor(paginationInnerInterceptor());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页插件配置(<a href="https://baomidou.com/pages/97710a/#paginationinnerinterceptor">...</a>)
|
||||
*/
|
||||
private PaginationInnerInterceptor paginationInnerInterceptor() {
|
||||
// 对于单一数据库类型来说,都建议配置该值,避免每次分页都去抓取数据库类型
|
||||
// PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
|
||||
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
|
||||
// 溢出总页数后是否进行处理
|
||||
paginationInnerInterceptor.setOverflow(false);
|
||||
// 单页分页条数限制
|
||||
paginationInnerInterceptor.setMaxLimit(-1L);
|
||||
return paginationInnerInterceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 元对象处理器配置(插入或修改时自动填充)
|
||||
*/
|
||||
@Bean
|
||||
MetaObjectHandler metaObjectHandler() {
|
||||
return new MyBatisPlusMetaObjectHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* ID 生成器配置,仅在主键类型(idType)配置为 ASSIGN_ID 或 ASSIGN_UUID 时有效(使用网卡信息绑定雪花生成器,防止集群雪花 ID 重复)
|
||||
*/
|
||||
@Bean
|
||||
IdentifierGenerator idGenerator() {
|
||||
return new DefaultIdentifierGenerator(NetUtil.getLocalhost());
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.charles7c.cnadmin.common.exception;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/23 22:55
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
public class ServiceException extends RuntimeException {
|
||||
|
||||
public ServiceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
@@ -19,10 +19,12 @@ package top.charles7c.cnadmin.common.handler;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
@@ -37,6 +39,8 @@ import cn.hutool.core.util.StrUtil;
|
||||
|
||||
import top.charles7c.cnadmin.common.exception.BadRequestException;
|
||||
import top.charles7c.cnadmin.common.model.vo.R;
|
||||
import top.charles7c.cnadmin.common.util.ExceptionUtils;
|
||||
import top.charles7c.cnadmin.common.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
@@ -85,7 +89,8 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(BindException.class)
|
||||
public R handleBindException(BindException e, HttpServletRequest request) {
|
||||
log.error("请求地址'{}',参数验证失败", request.getRequestURI(), e);
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), e.getMessage());
|
||||
String message = StreamUtils.join(e.getAllErrors(), DefaultMessageSourceResolvable::getDefaultMessage, ",");
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +100,8 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public R constraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
|
||||
log.error("请求地址'{}',参数验证失败", request.getRequestURI(), e);
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), e.getMessage());
|
||||
String message = StreamUtils.join(e.getConstraintViolations(), ConstraintViolation::getMessage, ",");
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +111,8 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public R handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
|
||||
log.error("请求地址'{}',参数验证失败", request.getRequestURI(), e);
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), e.getMessage());
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), ExceptionUtils
|
||||
.exToNull(() -> Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.charles7c.cnadmin.common.model.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录用户信息
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/24 13:01
|
||||
*/
|
||||
@Data
|
||||
public class LoginUser implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户 ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 性别(0未知 1男 2女)
|
||||
*/
|
||||
private Integer gender;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 头像地址
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String notes;
|
||||
|
||||
/**
|
||||
* 状态(1启用 2禁用)
|
||||
*/
|
||||
private Integer status;
|
||||
}
|
@@ -21,6 +21,9 @@ import java.util.Date;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
|
||||
/**
|
||||
* 实体类基类
|
||||
*
|
||||
@@ -35,20 +38,24 @@ public class BaseEntity implements Serializable {
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createUser;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 修改人
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updateUser;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
}
|
||||
|
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.charles7c.cnadmin.common.util;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
|
||||
/**
|
||||
* Stream 工具类
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/22 19:51
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class StreamUtils {
|
||||
|
||||
/**
|
||||
* 将集合中的指定字段使用分隔符拼接成字符串
|
||||
*
|
||||
* @param collection
|
||||
* 集合
|
||||
* @param function
|
||||
* 字段方法
|
||||
* @param delimiter
|
||||
* 分隔符
|
||||
* @param <E>
|
||||
* /
|
||||
* @return 拼接结果
|
||||
*/
|
||||
public static <E> String join(Collection<E> collection, Function<E, String> function, CharSequence delimiter) {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
|
||||
}
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.charles7c.cnadmin.common.util.helper;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
|
||||
import top.charles7c.cnadmin.common.model.dto.LoginUser;
|
||||
import top.charles7c.cnadmin.common.util.ExceptionUtils;
|
||||
|
||||
/**
|
||||
* 登录助手
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/24 12:58
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class LoginHelper {
|
||||
|
||||
private static final String LOGIN_USER_KEY = "LOGIN_USER";
|
||||
|
||||
/**
|
||||
* 用户登录并缓存用户信息
|
||||
*
|
||||
* @param loginUser
|
||||
* 登录用户信息
|
||||
*/
|
||||
public static void login(LoginUser loginUser) {
|
||||
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
|
||||
StpUtil.login(loginUser.getUserId());
|
||||
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录用户信息
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
public static LoginUser getLoginUser() {
|
||||
LoginUser loginUser = (LoginUser)SaHolder.getStorage().get(LOGIN_USER_KEY);
|
||||
if (loginUser != null) {
|
||||
return loginUser;
|
||||
}
|
||||
try {
|
||||
loginUser = (LoginUser)StpUtil.getTokenSession().get(LOGIN_USER_KEY);
|
||||
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录用户 ID
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
public static Long getUserId() {
|
||||
return ExceptionUtils.exToNull(() -> getLoginUser().getUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录用户名
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
public static String getUsername() {
|
||||
return ExceptionUtils.exToNull(() -> getLoginUser().getUsername());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录用户昵称
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
public static String getNickname() {
|
||||
return ExceptionUtils.exToNull(() -> getLoginUser().getNickname());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user