mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-09 14:36:00 +08:00
feat 客户系统消息通知联调, 一些小问题修改
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# CHANGELOG
|
||||
## [v2.0.1]
|
||||
- 增加支付、退款时客户通知功能,支持多次重发
|
||||
- 开源文档增加支付通知和退款通知文档
|
||||
- 增加客户通知任务记录功能
|
||||
- 支持钱包支付、流水记录、各类操作等功能
|
||||
- 支持储值卡支付、流水记录、各类操作等功能
|
||||
@@ -10,7 +11,11 @@
|
||||
- 变更:废弃调用接口时的`version`字段,调用时不再进行传输,SDK中同步进行删除
|
||||
- 优化:订单支持关闭时间记录
|
||||
- 优化:增加退款订单扩展记录
|
||||
- fix: 同步支付通道订单不能正确生成
|
||||
- 优化:SDK增加简单退款、多通道退款等多中测试样例
|
||||
- fix:同步支付通道订单不能正确生成
|
||||
- fix:修复聚合条码支付时付款码未传输问题
|
||||
- fix:修复微信退款同步时, 错误信息未保存问题
|
||||
- fix:修复手动发起退款时上下文未进行初始化的问题
|
||||
|
||||
## [v2.0.0]
|
||||
- 支持支付宝支付:扫码支付、付款码支付、PC支付、H5支付
|
||||
|
||||
45
_doc/Task.md
45
_doc/Task.md
@@ -5,34 +5,41 @@
|
||||
- [x] 储值卡支付, 储值卡流水
|
||||
- [x] 支付回调通知和退款回调通知功能
|
||||
- [x] 回调任务发起
|
||||
- [ ] 支付关闭发送回调
|
||||
- [ ] 退款关闭发送回调
|
||||
- [x] 回调调用记录
|
||||
- [x] SDK接收对象编写
|
||||
- [x] Demo模块接收回调演示
|
||||
- [x] 通道配置启用停用配置生效
|
||||
- [x] 支付对账的序列化生成器有问题,待platform更新
|
||||
|
||||
|
||||
2.0.2:
|
||||
- [ ] 对账功能补全账单比对功能
|
||||
- [ ] 数据库表进行规则, 字段设置长度, 增加索引
|
||||
- [ ] 退款操作支持重试
|
||||
|
||||
2.0.x 版本内容
|
||||
- [ ] 云闪付接入
|
||||
- [ ] 退款操作支持重试
|
||||
- [ ] 增加各类日志记录,例如钱包的各项操作
|
||||
- [ ] 支付流程也改为先落库后支付情况, 避免极端情况导致掉单
|
||||
- [ ] 支付流程涉及异步支付时, 更换支付方式需要控制预防客户重复付款
|
||||
|
||||
**任务池**
|
||||
- [x] 支付SDK编写
|
||||
- [x] 接入支付网关的演示项目
|
||||
- [x] 增加聚合支付功能支持
|
||||
- [x] 增加手机用户收银台功能
|
||||
**任务池**
|
||||
- [x] 支付SDK编写
|
||||
- [x] 接入支付网关的演示项目
|
||||
- [ ] 订单超时支持数据表级别触发
|
||||
- [ ] 支付宝关闭支付时支持撤销方式,
|
||||
- [ ] 支持转账操作, 通过支付通道专有参数进行实现, 转账时只能单个通道进行操作
|
||||
- [ ] 支付成功回调后, 如果订单已超时, 则进入待退款订单中,提示进行退款,或者自动退款
|
||||
- [ ] 增加回调机制(通知客户端)
|
||||
- [ ] 增加消息通知机制(通知客户端)
|
||||
- [ ] 新增支付单预警功能, 处理支付单与网关状态不一致且无法自动修复的情况
|
||||
- [ ] 微信消息通知相关配置
|
||||
- [ ] 钉钉消息通知配置
|
||||
- [ ] 支付宝接口升级为V3接口
|
||||
- [ ] 微信支付接口更新为V3接口
|
||||
- [ ] 增加验签调试等功能的页面
|
||||
- [ ] 请求IP参数增加正则校验
|
||||
- [ ] 增加账户金额表, 记录每天每个通道的账户金额, 并且提供查询接口
|
||||
- [ ] 订单超时支持数据表级别触发
|
||||
- [ ] 支付宝关闭支付时支持撤销方式,
|
||||
- [ ] 支持转账操作, 通过支付通道专有参数进行实现, 转账时只能单个通道进行操作
|
||||
- [ ] 支付成功回调后, 如果订单已超时, 则进入待退款订单中,提示进行退款,或者自动退款
|
||||
- [x] 增加回调机制(通知客户端)
|
||||
- [ ] 增加消息通知机制(通知客户端)
|
||||
- [ ] 新增支付单预警功能, 处理支付单与网关状态不一致且无法自动修复的情况
|
||||
- [ ] 微信消息通知相关配置
|
||||
- [ ] 钉钉消息通知配置
|
||||
- [ ] 支付宝接口升级为V3接口
|
||||
- [ ] 微信支付接口更新为V3接口
|
||||
- [ ] 增加验签调试等功能的页面
|
||||
- [ ] 请求IP参数增加正则校验
|
||||
- [ ] 增加账户金额表, 记录每天每个通道的账户金额, 并且提供查询接口
|
||||
|
||||
@@ -17,6 +17,7 @@ import cn.bootx.platform.daxpay.sdk.model.pay.PayOrderModel;
|
||||
import cn.bootx.platform.daxpay.sdk.net.DaxPayKit;
|
||||
import cn.bootx.platform.daxpay.sdk.param.assist.WxAccessTokenParam;
|
||||
import cn.bootx.platform.daxpay.sdk.param.assist.WxAuthUrlParam;
|
||||
import cn.bootx.platform.daxpay.sdk.param.channel.BarCodePayParam;
|
||||
import cn.bootx.platform.daxpay.sdk.param.channel.WeChatPayParam;
|
||||
import cn.bootx.platform.daxpay.sdk.param.pay.SimplePayParam;
|
||||
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
|
||||
@@ -217,6 +218,9 @@ public class AggregateService {
|
||||
simplePayParam.setAmount(amount);
|
||||
simplePayParam.setChannel(payChannel.getCode());
|
||||
simplePayParam.setPayWay(PayWayEnum.BARCODE.getCode());
|
||||
BarCodePayParam barCodePayParam = new BarCodePayParam();
|
||||
barCodePayParam.setAuthCode(param.getAuthCode());
|
||||
simplePayParam.setChannelParam(barCodePayParam);
|
||||
|
||||
String ip = Optional.ofNullable(WebServletUtil.getRequest())
|
||||
.map(ServletUtil::getClientIP)
|
||||
|
||||
@@ -61,6 +61,7 @@ public class SimplePayOrderTest {
|
||||
param.setChannel(PayChannelEnum.CASH.getCode());
|
||||
param.setPayWay(PayWayEnum.NORMAL.getCode());
|
||||
param.setClientIp("127.0.0.1");
|
||||
param.setAttach("Attach");
|
||||
param.setNotifyUrl("http://127.0.0.1:9000/demo/callback/pay");
|
||||
param.setNotifyUrl("http://127.0.0.1:9000/demo/callback/payObject");
|
||||
DaxPayResult<PayOrderModel> execute = DaxPayKit.execute(param);
|
||||
|
||||
@@ -18,7 +18,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
*
|
||||
* 微信支付记录
|
||||
* @author xxm
|
||||
* @since 2024/2/19
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package cn.bootx.platform.daxpay.service.core.channel.wechat.service;
|
||||
|
||||
import cn.bootx.platform.common.core.util.LocalDateTimeUtil;
|
||||
import cn.bootx.platform.daxpay.code.RefundSyncStatusEnum;
|
||||
import cn.bootx.platform.daxpay.code.PaySyncStatusEnum;
|
||||
import cn.bootx.platform.daxpay.code.RefundSyncStatusEnum;
|
||||
import cn.bootx.platform.daxpay.service.code.WeChatPayCode;
|
||||
import cn.bootx.platform.daxpay.service.core.channel.wechat.entity.WeChatPayConfig;
|
||||
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
|
||||
@@ -10,6 +10,7 @@ import cn.bootx.platform.daxpay.service.core.order.refund.entity.RefundOrder;
|
||||
import cn.bootx.platform.daxpay.service.core.payment.sync.result.PayGatewaySyncResult;
|
||||
import cn.bootx.platform.daxpay.service.core.payment.sync.result.RefundGatewaySyncResult;
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.ijpay.core.enums.SignType;
|
||||
import com.ijpay.core.kit.WxPayKit;
|
||||
@@ -132,12 +133,31 @@ public class WeChatPaySyncService {
|
||||
if (Objects.equals(tradeStatus, WeChatPayCode.REFUND_PROCESSING)) {
|
||||
return syncResult.setSyncStatus(RefundSyncStatusEnum.PROGRESS);
|
||||
}
|
||||
return syncResult.setSyncStatus(RefundSyncStatusEnum.FAIL);
|
||||
String errorMsg = this.getErrorMsg(result);
|
||||
return syncResult.setSyncStatus(RefundSyncStatusEnum.FAIL).setErrorMsg(errorMsg);
|
||||
} catch (Exception e) {
|
||||
log.error("查询退款订单失败:", e);
|
||||
syncResult.setSyncStatus(RefundSyncStatusEnum.PROGRESS).setErrorMsg(e.getMessage());
|
||||
}
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误处理
|
||||
*/
|
||||
/**
|
||||
* 验证错误信息
|
||||
*/
|
||||
private String getErrorMsg(Map<String, String> result) {
|
||||
String returnCode = result.get(WeChatPayCode.RETURN_CODE);
|
||||
String resultCode = result.get(WeChatPayCode.RESULT_CODE);
|
||||
if (!WxPayKit.codeIsOk(returnCode) || !WxPayKit.codeIsOk(resultCode)) {
|
||||
String errorMsg = result.get(WeChatPayCode.ERR_CODE_DES);
|
||||
if (StrUtil.isBlank(errorMsg)) {
|
||||
errorMsg = result.get(WeChatPayCode.RETURN_MSG);
|
||||
}
|
||||
return errorMsg;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import cn.bootx.platform.daxpay.code.PayStatusEnum;
|
||||
import cn.bootx.platform.daxpay.param.pay.PayChannelParam;
|
||||
import cn.bootx.platform.daxpay.param.pay.PayParam;
|
||||
import cn.bootx.platform.daxpay.result.pay.PayResult;
|
||||
import cn.bootx.platform.daxpay.service.common.context.ApiInfoLocal;
|
||||
import cn.bootx.platform.daxpay.service.common.context.PayLocal;
|
||||
import cn.bootx.platform.daxpay.service.common.context.NoticeLocal;
|
||||
import cn.bootx.platform.daxpay.service.common.context.PlatformLocal;
|
||||
@@ -71,9 +72,11 @@ public class PayBuilder {
|
||||
public PayOrderExtra buildPayOrderExtra(PayParam payParam, Long paymentId) {
|
||||
PlatformLocal platform = PaymentContextLocal.get().getPlatformInfo();
|
||||
NoticeLocal noticeInfo = PaymentContextLocal.get().getNoticeInfo();
|
||||
ApiInfoLocal apiInfo = PaymentContextLocal.get().getApiInfo();
|
||||
PayOrderExtra payOrderExtra = new PayOrderExtra()
|
||||
.setClientIp(payParam.getClientIp())
|
||||
.setDescription(payParam.getDescription())
|
||||
.setNoticeSign(apiInfo.isNoticeSign())
|
||||
.setNotifyUrl(noticeInfo.getNotifyUrl())
|
||||
.setReturnUrl(noticeInfo.getReturnUrl())
|
||||
.setReqSign(payParam.getSign())
|
||||
|
||||
@@ -50,10 +50,10 @@ public class PayChannelOrderService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换支付订单关联的异步支付通道, 同时会设置是否支付完成状态
|
||||
* 切换支付订单关联的异步支付通道, 同时会设置是否支付完成状态, 并返回通道订单
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void switchAsyncPayChannel(PayOrder payOrder, PayChannelParam payChannelParam){
|
||||
public PayChannelOrder switchAsyncPayChannel(PayOrder payOrder, PayChannelParam payChannelParam){
|
||||
PayLocal payInfo = PaymentContextLocal.get().getPayInfo();
|
||||
// 是否支付完成
|
||||
PayStatusEnum payStatus = payInfo.isPayComplete() ? PayStatusEnum.SUCCESS : PayStatusEnum.PROGRESS;
|
||||
@@ -81,15 +81,18 @@ public class PayChannelOrderService {
|
||||
channelOrderManager.deleteByPaymentIdAndAsync(payOrder.getId());
|
||||
channelOrderManager.save(payChannelOrder);
|
||||
payInfo.getPayChannelOrders().add(payChannelOrder);
|
||||
return payChannelOrder;
|
||||
} else {
|
||||
// 更新支付通道信息
|
||||
payOrderChannelOpt.get()
|
||||
PayChannelOrder payChannelOrder = payOrderChannelOpt.get();
|
||||
payChannelOrder
|
||||
.setPayWay(payChannelParam.getWay())
|
||||
.setPayTime(LocalDateTime.now())
|
||||
.setChannelExtra(channelParamStr)
|
||||
.setStatus(payStatus.getCode());
|
||||
channelOrderManager.updateById(payOrderChannelOpt.get());
|
||||
channelOrderManager.updateById(payChannelOrder);
|
||||
payInfo.getPayChannelOrders().add(payOrderChannelOpt.get());
|
||||
return payChannelOrder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
|
||||
import cn.bootx.platform.daxpay.service.core.order.pay.service.PayOrderQueryService;
|
||||
import cn.bootx.platform.daxpay.service.core.order.pay.service.PayOrderService;
|
||||
import cn.bootx.platform.daxpay.service.core.payment.close.factory.PayCloseStrategyFactory;
|
||||
import cn.bootx.platform.daxpay.service.core.payment.notice.service.ClientNoticeService;
|
||||
import cn.bootx.platform.daxpay.service.core.record.close.entity.PayCloseRecord;
|
||||
import cn.bootx.platform.daxpay.service.core.record.close.service.PayCloseRecordService;
|
||||
import cn.bootx.platform.daxpay.service.func.AbsPayCloseStrategy;
|
||||
@@ -44,6 +45,7 @@ public class PayCloseService {
|
||||
private final PayOrderService payOrderService;
|
||||
private final PayOrderQueryService payOrderQueryService;
|
||||
private final PayCloseRecordService payCloseRecordService;
|
||||
private final ClientNoticeService clientsService;;
|
||||
|
||||
private final LockTemplate lockTemplate;
|
||||
|
||||
@@ -76,9 +78,10 @@ public class PayCloseService {
|
||||
* 关闭支付记录
|
||||
*/
|
||||
private void close(PayOrder payOrder) {
|
||||
List<PayChannelOrder> payChannelOrders;
|
||||
try {
|
||||
// 状态检查, 只有支付中可以进行取消支付
|
||||
if (!Objects.equals(payOrder.getStatus(), PayStatusEnum.PROGRESS.getCode())){
|
||||
if (!Objects.equals(payOrder.getStatus(), PayStatusEnum.PROGRESS.getCode())) {
|
||||
throw new PayFailureException("订单不是支付中, 无法进行关闭订单");
|
||||
}
|
||||
|
||||
@@ -88,7 +91,8 @@ public class PayCloseService {
|
||||
.collect(Collectors.toMap(PayChannelOrder::getChannel, Function.identity(), CollectorsFunction::retainLatest));
|
||||
|
||||
// 1.获取支付方式, 通过工厂生成对应的策略组
|
||||
List<String> channels = orderChannelMap.values().stream()
|
||||
List<String> channels = orderChannelMap.values()
|
||||
.stream()
|
||||
.map(PayChannelOrder::getChannel)
|
||||
.collect(Collectors.toList());
|
||||
List<AbsPayCloseStrategy> payCloseStrategies = PayCloseStrategyFactory.createAsyncLast(channels);
|
||||
@@ -98,7 +102,8 @@ public class PayCloseService {
|
||||
|
||||
// 2.初始化关闭支付的参数
|
||||
for (AbsPayCloseStrategy strategy : payCloseStrategies) {
|
||||
strategy.initCloseParam(payOrder, orderChannelMap.get(strategy.getChannel().getCode()));
|
||||
strategy.initCloseParam(payOrder, orderChannelMap.get(strategy.getChannel()
|
||||
.getCode()));
|
||||
}
|
||||
|
||||
// 3.关闭前准备
|
||||
@@ -111,29 +116,30 @@ public class PayCloseService {
|
||||
payCloseStrategies.forEach(AbsPayCloseStrategy::doSuccessHandler);
|
||||
|
||||
// 6.更新支付通道订单的状态
|
||||
List<PayChannelOrder> payChannelOrders = payCloseStrategies.stream()
|
||||
payChannelOrders = payCloseStrategies.stream()
|
||||
.map(AbsPayCloseStrategy::getChannelOrder)
|
||||
.collect(Collectors.toList());
|
||||
payChannelOrderManager.updateAllById(payChannelOrders);
|
||||
}
|
||||
catch (PayFailureException e) {
|
||||
} catch (PayFailureException e) {
|
||||
// 记录关闭失败的记录
|
||||
this.saveRecord(payOrder,false,e.getMessage());
|
||||
this.saveRecord(payOrder, false, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 关闭成功后处理
|
||||
this.successHandler(payOrder);
|
||||
this.successHandler(payOrder, payChannelOrders);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功后处理方法
|
||||
*/
|
||||
private void successHandler(PayOrder payOrder){
|
||||
private void successHandler(PayOrder payOrder, List<PayChannelOrder> payChannelOrders){
|
||||
// 取消订单
|
||||
payOrder.setStatus(PayStatusEnum.CLOSE.getCode())
|
||||
.setCloseTime(LocalDateTime.now());
|
||||
payOrderService.updateById(payOrder);
|
||||
// 发送通知
|
||||
clientsService.registerPayNotice(payOrder,null, payChannelOrders);
|
||||
this.saveRecord(payOrder,true,null);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import cn.bootx.platform.daxpay.service.core.channel.alipay.entity.AliPayConfig;
|
||||
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayConfigService;
|
||||
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayRecordService;
|
||||
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayService;
|
||||
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayChannelOrder;
|
||||
import cn.bootx.platform.daxpay.service.core.order.pay.service.PayChannelOrderService;
|
||||
import cn.bootx.platform.daxpay.service.func.AbsPayStrategy;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
@@ -26,7 +27,7 @@ import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROT
|
||||
|
||||
/**
|
||||
* 支付宝支付
|
||||
*
|
||||
* 注意: channelOrder对象需要单独处理, 直接获取会空指针
|
||||
* @author xxm
|
||||
* @since 2021/2/27
|
||||
*/
|
||||
@@ -102,10 +103,10 @@ public class AliPayStrategy extends AbsPayStrategy {
|
||||
@Override
|
||||
public void doSuccessHandler() {
|
||||
PayLocal asyncPayInfo = PaymentContextLocal.get().getPayInfo();
|
||||
channelOrderService.switchAsyncPayChannel(this.getOrder(), this.getPayChannelParam());
|
||||
PayChannelOrder payChannelOrder = channelOrderService.switchAsyncPayChannel(this.getOrder(), this.getPayChannelParam());
|
||||
// 支付完成, 保存记录
|
||||
if (asyncPayInfo.isPayComplete()) {
|
||||
aliRecordService.pay(this.getOrder(), this.getChannelOrder());
|
||||
aliRecordService.pay(this.getOrder(), payChannelOrder);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,5 +53,4 @@ public class CashPayStrategy extends AbsPayStrategy {
|
||||
public void doPayHandler() {
|
||||
cashRecordService.pay(this.getChannelOrder(),this.getOrder().getTitle());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import cn.bootx.platform.daxpay.service.core.channel.wechat.entity.WeChatPayConf
|
||||
import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WeChatPayConfigService;
|
||||
import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WeChatPayRecordService;
|
||||
import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WeChatPayService;
|
||||
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayChannelOrder;
|
||||
import cn.bootx.platform.daxpay.service.core.order.pay.service.PayChannelOrderService;
|
||||
import cn.bootx.platform.daxpay.service.func.AbsPayStrategy;
|
||||
import cn.bootx.platform.daxpay.service.param.channel.wechat.WeChatPayParam;
|
||||
@@ -24,7 +25,7 @@ import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROT
|
||||
|
||||
/**
|
||||
* 微信支付
|
||||
*
|
||||
* 注意: channelOrder对象需要单独处理, 直接获取会空指针
|
||||
* @author xxm
|
||||
* @since 2021/4/5
|
||||
*/
|
||||
@@ -90,12 +91,12 @@ public class WeChatPayStrategy extends AbsPayStrategy {
|
||||
*/
|
||||
@Override
|
||||
public void doSuccessHandler() {
|
||||
channelOrderService.switchAsyncPayChannel(this.getOrder(), this.getPayChannelParam());
|
||||
PayChannelOrder payChannelOrder = channelOrderService.switchAsyncPayChannel(this.getOrder(), this.getPayChannelParam());
|
||||
this.getOrder().setAsyncChannel(this.getChannel().getCode());
|
||||
PayLocal asyncPayInfo = PaymentContextLocal.get().getPayInfo();
|
||||
// 是否支付完成, 保存流水记录
|
||||
if (asyncPayInfo.isPayComplete()){
|
||||
weChatPayRecordService.pay(this.getOrder(), this.getChannelOrder());
|
||||
weChatPayRecordService.pay(this.getOrder(), payChannelOrder);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ import lombok.experimental.Accessors;
|
||||
@Schema(title = "现金记录")
|
||||
public class CashRecordDto extends BaseDto {
|
||||
|
||||
/** 标题 */
|
||||
@Schema(description = "标题")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 业务类型
|
||||
* @see VoucherRecordTypeEnum
|
||||
|
||||
@@ -22,6 +22,10 @@ public class VoucherRecordDto extends BaseDto {
|
||||
@Schema(description = "储值卡id")
|
||||
private Long voucherId;
|
||||
|
||||
/** 标题 */
|
||||
@Schema(description = "标题")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 业务类型
|
||||
* @see VoucherRecordTypeEnum
|
||||
|
||||
Reference in New Issue
Block a user