fix(payment): 解决微信支付异常处理和SQL Runner配置问题
- 在application.yml中启用enable-sql-runner配置以解决SqlRunner删除操作报错 - 在application-ysb.yml中补充完整的mybatis-plus配置包括SQL Runner支持 - 修改ShopDealerWithdrawController中openid获取逻辑,统一使用小程序openid避免微信400错误 - 更新ShopDealerWithdrawMapper.xml中字段别名映射确保数据正确显示 - 在WxTransferService中增强ServiceException处理,提供更友好的错误信息 - 添加详细的异常转换方法toPaymentException解析微信API错误详情 - 补充必要的Gson依赖导入处理JSON响应数据
This commit is contained in:
86
com/wechat/pay/java/core/exception/ServiceException.java
Normal file
86
com/wechat/pay/java/core/exception/ServiceException.java
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package com.wechat.pay.java.core.exception;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.wechat.pay.java.core.http.HttpRequest;
|
||||||
|
import com.wechat.pay.java.core.util.GsonUtil;
|
||||||
|
|
||||||
|
/** 发送HTTP请求成功,返回异常时抛出。例如返回状态码小于200或大于等于300、返回体参数不完整。 */
|
||||||
|
public class ServiceException extends WechatPayException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -7174975090366956652L;
|
||||||
|
|
||||||
|
private final HttpRequest httpRequest;
|
||||||
|
private final int httpStatusCode;
|
||||||
|
private final String responseBody;
|
||||||
|
private String errorCode;
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回状态码小于200或大于300调用
|
||||||
|
*
|
||||||
|
* @param httpRequest http请求
|
||||||
|
* @param httpStatusCode http状态码
|
||||||
|
* @param responseBody http返回体
|
||||||
|
*/
|
||||||
|
public ServiceException(HttpRequest httpRequest, int httpStatusCode, String responseBody) {
|
||||||
|
super(
|
||||||
|
String.format(
|
||||||
|
"Wrong HttpStatusCode[%d]%nhttpResponseBody[%.1024s]\tHttpRequest[%s]",
|
||||||
|
httpStatusCode, responseBody, httpRequest));
|
||||||
|
this.httpRequest = httpRequest;
|
||||||
|
this.httpStatusCode = httpStatusCode;
|
||||||
|
this.responseBody = responseBody;
|
||||||
|
if (responseBody != null && !responseBody.isEmpty()) {
|
||||||
|
JsonObject jsonObject = GsonUtil.getGson().fromJson(responseBody, JsonObject.class);
|
||||||
|
JsonElement code = jsonObject.get("code");
|
||||||
|
JsonElement message = jsonObject.get("message");
|
||||||
|
this.errorCode = code == null ? null : code.getAsString();
|
||||||
|
this.errorMessage = message == null ? null : message.getAsString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取序列化版本UID
|
||||||
|
*
|
||||||
|
* @return UID
|
||||||
|
*/
|
||||||
|
public static long getSerialVersionUID() {
|
||||||
|
return serialVersionUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取HTTP请求
|
||||||
|
*
|
||||||
|
* @return HTTP请求
|
||||||
|
*/
|
||||||
|
public HttpRequest getHttpRequest() {
|
||||||
|
return httpRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取HTTP返回体
|
||||||
|
*
|
||||||
|
* @return HTTP返回体
|
||||||
|
*/
|
||||||
|
public String getResponseBody() {
|
||||||
|
return responseBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取HTTP状态码
|
||||||
|
*
|
||||||
|
* @return HTTP状态码
|
||||||
|
*/
|
||||||
|
public int getHttpStatusCode() {
|
||||||
|
return httpStatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorCode() {
|
||||||
|
return errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
151
com/wechat/pay/java/core/http/AbstractHttpClient.java
Normal file
151
com/wechat/pay/java/core/http/AbstractHttpClient.java
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package com.wechat.pay.java.core.http;
|
||||||
|
|
||||||
|
import static com.wechat.pay.java.core.http.Constant.ACCEPT;
|
||||||
|
import static com.wechat.pay.java.core.http.Constant.AUTHORIZATION;
|
||||||
|
import static com.wechat.pay.java.core.http.Constant.OS;
|
||||||
|
import static com.wechat.pay.java.core.http.Constant.REQUEST_ID;
|
||||||
|
import static com.wechat.pay.java.core.http.Constant.USER_AGENT;
|
||||||
|
import static com.wechat.pay.java.core.http.Constant.USER_AGENT_FORMAT;
|
||||||
|
import static com.wechat.pay.java.core.http.Constant.VERSION;
|
||||||
|
import static com.wechat.pay.java.core.http.Constant.WECHAT_PAY_SERIAL;
|
||||||
|
import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
|
||||||
|
import static java.net.HttpURLConnection.HTTP_OK;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
import com.wechat.pay.java.core.auth.Credential;
|
||||||
|
import com.wechat.pay.java.core.auth.Validator;
|
||||||
|
import com.wechat.pay.java.core.exception.MalformedMessageException;
|
||||||
|
import com.wechat.pay.java.core.exception.ServiceException;
|
||||||
|
import com.wechat.pay.java.core.exception.ValidationException;
|
||||||
|
import com.wechat.pay.java.core.http.HttpRequest.Builder;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/** 请求客户端抽象基类 */
|
||||||
|
public abstract class AbstractHttpClient implements HttpClient {
|
||||||
|
|
||||||
|
protected final Credential credential;
|
||||||
|
protected final Validator validator;
|
||||||
|
|
||||||
|
public AbstractHttpClient(Credential credential, Validator validator) {
|
||||||
|
this.credential = requireNonNull(credential);
|
||||||
|
this.validator = requireNonNull(validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> HttpResponse<T> execute(HttpRequest httpRequest, Class<T> responseClass) {
|
||||||
|
HttpRequest innerRequest =
|
||||||
|
new Builder()
|
||||||
|
.url(httpRequest.getUrl())
|
||||||
|
.httpMethod(httpRequest.getHttpMethod())
|
||||||
|
.headers(httpRequest.getHeaders())
|
||||||
|
.addHeader(AUTHORIZATION, getAuthorization(httpRequest))
|
||||||
|
.addHeader(USER_AGENT, getUserAgent())
|
||||||
|
.addHeader(WECHAT_PAY_SERIAL, getWechatPaySerial())
|
||||||
|
.body(httpRequest.getBody())
|
||||||
|
.build();
|
||||||
|
OriginalResponse originalResponse = innerExecute(innerRequest);
|
||||||
|
validateResponse(originalResponse);
|
||||||
|
return assembleHttpResponse(originalResponse, responseClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream download(String url) {
|
||||||
|
HttpRequest originRequest =
|
||||||
|
new HttpRequest.Builder().httpMethod(HttpMethod.GET).url(url).build();
|
||||||
|
HttpRequest httpRequest =
|
||||||
|
new HttpRequest.Builder()
|
||||||
|
.url(url)
|
||||||
|
.httpMethod(HttpMethod.GET)
|
||||||
|
.addHeader(AUTHORIZATION, getAuthorization(originRequest))
|
||||||
|
.addHeader(ACCEPT, "*/*")
|
||||||
|
.addHeader(USER_AGENT, getUserAgent())
|
||||||
|
.addHeader(WECHAT_PAY_SERIAL, getWechatPaySerial())
|
||||||
|
.build();
|
||||||
|
return innerDownload(httpRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract InputStream innerDownload(HttpRequest httpRequest);
|
||||||
|
|
||||||
|
protected abstract OriginalResponse innerExecute(HttpRequest httpRequest);
|
||||||
|
|
||||||
|
private void validateResponse(OriginalResponse originalResponse) {
|
||||||
|
|
||||||
|
if (isInvalidHttpCode(originalResponse.getStatusCode())) {
|
||||||
|
throw new ServiceException(
|
||||||
|
originalResponse.getRequest(),
|
||||||
|
originalResponse.getStatusCode(),
|
||||||
|
originalResponse.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalResponse.getBody() != null
|
||||||
|
&& !originalResponse.getBody().isEmpty()
|
||||||
|
&& !MediaType.APPLICATION_JSON.equalsWith(originalResponse.getContentType())) {
|
||||||
|
throw new MalformedMessageException(
|
||||||
|
String.format(
|
||||||
|
"Unsupported content-type[%s]%nhttpRequest[%s]",
|
||||||
|
originalResponse.getContentType(), originalResponse.getRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validator.validate(originalResponse.getHeaders(), originalResponse.getBody())) {
|
||||||
|
String requestId = originalResponse.getHeaders().getHeader(REQUEST_ID);
|
||||||
|
throw new ValidationException(
|
||||||
|
String.format(
|
||||||
|
"Validate response failed,the WechatPay signature is incorrect.%n"
|
||||||
|
+ "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
|
||||||
|
requestId, originalResponse.getHeaders(), originalResponse.getBody()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isInvalidHttpCode(int httpCode) {
|
||||||
|
return httpCode < HTTP_OK || httpCode >= HTTP_MULT_CHOICE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> HttpResponse<T> assembleHttpResponse(
|
||||||
|
OriginalResponse originalResponse, Class<T> responseClass) {
|
||||||
|
return new HttpResponse.Builder<T>()
|
||||||
|
.originalResponse(originalResponse)
|
||||||
|
.serviceResponseType(responseClass)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSignBody(RequestBody requestBody) {
|
||||||
|
if (requestBody == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (requestBody instanceof JsonRequestBody) {
|
||||||
|
return ((JsonRequestBody) requestBody).getBody();
|
||||||
|
}
|
||||||
|
if (requestBody instanceof FileRequestBody) {
|
||||||
|
return ((FileRequestBody) requestBody).getMeta();
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
String.format("Unsupported RequestBody Type[%s]", requestBody.getClass().getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUserAgent() {
|
||||||
|
return String.format(
|
||||||
|
USER_AGENT_FORMAT,
|
||||||
|
getClass().getPackage().getImplementationVersion(),
|
||||||
|
OS,
|
||||||
|
VERSION == null ? "Unknown" : VERSION,
|
||||||
|
credential.getClass().getSimpleName(),
|
||||||
|
validator.getClass().getSimpleName(),
|
||||||
|
getHttpClientInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getWechatPaySerial() {
|
||||||
|
return this.validator.getSerialNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取http客户端信息,用于User-Agent。 格式:客户端名称/版本 示例:okhttp3/4.9.3
|
||||||
|
*
|
||||||
|
* @return 客户端信息
|
||||||
|
*/
|
||||||
|
protected abstract String getHttpClientInfo();
|
||||||
|
|
||||||
|
private String getAuthorization(HttpRequest request) {
|
||||||
|
return credential.getAuthorization(
|
||||||
|
request.getUri(), request.getHttpMethod().name(), getSignBody(request.getBody()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ import com.wechat.pay.java.core.http.JsonRequestBody;
|
|||||||
import com.wechat.pay.java.core.http.MediaType;
|
import com.wechat.pay.java.core.http.MediaType;
|
||||||
import com.wechat.pay.java.core.exception.ServiceException;
|
import com.wechat.pay.java.core.exception.ServiceException;
|
||||||
import com.wechat.pay.java.core.util.GsonUtil;
|
import com.wechat.pay.java.core.util.GsonUtil;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -214,7 +216,8 @@ public class WxTransferService {
|
|||||||
"未传入完整且对应的转账场景报备信息:请在配置中设置 wechatpay.transfer.scene-report-infos-json(需与 transfer_scene_id="
|
"未传入完整且对应的转账场景报备信息:请在配置中设置 wechatpay.transfer.scene-report-infos-json(需与 transfer_scene_id="
|
||||||
+ transferSceneId + " 的报备信息一致)");
|
+ transferSceneId + " 的报备信息一致)");
|
||||||
} else {
|
} else {
|
||||||
throw se;
|
// 透传更友好的错误信息(避免前端看到 SDK 的 Wrong HttpStatusCode…)
|
||||||
|
throw toPaymentException(se);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TransferBillsResponse response = httpResponse.getServiceResponse();
|
TransferBillsResponse response = httpResponse.getServiceResponse();
|
||||||
@@ -226,6 +229,9 @@ public class WxTransferService {
|
|||||||
} catch (PaymentException e) {
|
} catch (PaymentException e) {
|
||||||
// 业务/参数错误保持原样抛出,避免被包装成 systemError
|
// 业务/参数错误保持原样抛出,避免被包装成 systemError
|
||||||
throw e;
|
throw e;
|
||||||
|
} catch (ServiceException se) {
|
||||||
|
// 兜底:上面的分支未覆盖到的 ServiceException 统一转换为业务可读信息
|
||||||
|
throw toPaymentException(se);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("微信商家转账失败(升级版): tenantId={}, outBillNo={}, openid={}, amountFen={}, err={}",
|
log.error("微信商家转账失败(升级版): tenantId={}, outBillNo={}, openid={}, amountFen={}, err={}",
|
||||||
tenantId, outBillNo, openid, amountFen, e.getMessage(), e);
|
tenantId, outBillNo, openid, amountFen, e.getMessage(), e);
|
||||||
@@ -233,6 +239,62 @@ public class WxTransferService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PaymentException toPaymentException(ServiceException se) {
|
||||||
|
String code = se.getErrorCode();
|
||||||
|
String msg = se.getErrorMessage();
|
||||||
|
String detail = extractDetail(se.getResponseBody());
|
||||||
|
|
||||||
|
// 微信侧 400 通常是请求参数/业务规则不匹配(如 openid 与 appid 不匹配、单号重复、报备信息不一致等)
|
||||||
|
if (se.getHttpStatusCode() == 400) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("微信商家转账请求无效");
|
||||||
|
if (StrUtil.isNotBlank(msg)) {
|
||||||
|
sb.append(":").append(msg);
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(detail)) {
|
||||||
|
sb.append("(").append(detail).append(")");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(code)) {
|
||||||
|
sb.append(" [").append(code).append("]");
|
||||||
|
}
|
||||||
|
return PaymentException.paramError(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他状态码按系统错误处理,保留 code/msg 便于排查
|
||||||
|
StringBuilder sb = new StringBuilder("微信商家转账失败");
|
||||||
|
if (StrUtil.isNotBlank(msg)) {
|
||||||
|
sb.append(":").append(msg);
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(detail)) {
|
||||||
|
sb.append("(").append(detail).append(")");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(code)) {
|
||||||
|
sb.append(" [").append(code).append("]");
|
||||||
|
}
|
||||||
|
return PaymentException.systemError(sb.toString(), se);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractDetail(String responseBody) {
|
||||||
|
if (StrUtil.isBlank(responseBody)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JsonObject obj = GsonUtil.getGson().fromJson(responseBody, JsonObject.class);
|
||||||
|
if (obj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JsonElement detailEl = obj.get("detail");
|
||||||
|
if (detailEl != null && !detailEl.isJsonNull()) {
|
||||||
|
// 常见为字符串;若为对象/数组,toString() 也能提供排错信息
|
||||||
|
String detail = detailEl.isJsonPrimitive() ? detailEl.getAsString() : detailEl.toString();
|
||||||
|
return StrUtil.isBlank(detail) ? null : detail;
|
||||||
|
}
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static String limitLen(String s, int maxLen) {
|
private static String limitLen(String s, int maxLen) {
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -238,16 +238,17 @@ public class ShopDealerWithdrawController extends BaseController {
|
|||||||
return fail("tenantId为空,无法发起微信转账");
|
return fail("tenantId为空,无法发起微信转账");
|
||||||
}
|
}
|
||||||
|
|
||||||
String openid = StrUtil.isNotBlank(db.getOpenId()) ? db.getOpenId() : db.getOfficeOpenid();
|
// 小程序“收款确认页”只能使用小程序openid;公众号openid传入会导致微信侧 400(INVALID_REQUEST)。
|
||||||
|
String openid = db.getOpenId();
|
||||||
if (StrUtil.isBlank(openid)) {
|
if (StrUtil.isBlank(openid)) {
|
||||||
// 兜底:从分销商信息关联获取openid
|
// 兜底:从分销商信息关联获取openid(同样应为小程序openid)
|
||||||
ShopDealerUser dealerUser = shopDealerUserService.getByUserIdRel(db.getUserId());
|
ShopDealerUser dealerUser = shopDealerUserService.getByUserIdRel(db.getUserId());
|
||||||
if (dealerUser != null && StrUtil.isNotBlank(dealerUser.getOpenid())) {
|
if (dealerUser != null && StrUtil.isNotBlank(dealerUser.getOpenid())) {
|
||||||
openid = dealerUser.getOpenid();
|
openid = dealerUser.getOpenid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (StrUtil.isBlank(openid)) {
|
if (StrUtil.isBlank(openid)) {
|
||||||
return fail("用户openid为空,无法拉起微信收款确认页");
|
return fail("用户小程序openid为空,无法拉起微信收款确认页");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用提现记录ID构造单号,保持幂等;微信要求 5-32 且仅字母/数字
|
// 使用提现记录ID构造单号,保持幂等;微信要求 5-32 且仅字母/数字
|
||||||
|
|||||||
@@ -4,7 +4,13 @@
|
|||||||
|
|
||||||
<!-- 关联查询sql -->
|
<!-- 关联查询sql -->
|
||||||
<sql id="selectSql">
|
<sql id="selectSql">
|
||||||
SELECT a.*, b.nickname, b.phone AS phone, b.avatar,b.openid,b.office_openid, c.real_name as realName
|
SELECT a.*,
|
||||||
|
b.nickname,
|
||||||
|
b.phone AS phone,
|
||||||
|
b.avatar,
|
||||||
|
b.openid AS openId,
|
||||||
|
b.office_openid AS officeOpenid,
|
||||||
|
c.real_name AS realName
|
||||||
FROM shop_dealer_withdraw a
|
FROM shop_dealer_withdraw a
|
||||||
LEFT JOIN gxwebsoft_core.sys_user b ON a.user_id = b.user_id
|
LEFT JOIN gxwebsoft_core.sys_user b ON a.user_id = b.user_id
|
||||||
LEFT JOIN gxwebsoft_core.sys_user_verify c ON a.user_id = c.user_id AND c.status = 1
|
LEFT JOIN gxwebsoft_core.sys_user_verify c ON a.user_id = c.user_id AND c.status = 1
|
||||||
|
|||||||
@@ -44,7 +44,21 @@ mqtt:
|
|||||||
connection-timeout: 10
|
connection-timeout: 10
|
||||||
keep-alive-interval: 20
|
keep-alive-interval: 20
|
||||||
auto-reconnect: true
|
auto-reconnect: true
|
||||||
|
# Mybatis-plus配置
|
||||||
|
mybatis-plus:
|
||||||
|
mapper-locations: classpath*:com/gxwebsoft/**/*Mapper.xml
|
||||||
|
configuration:
|
||||||
|
map-underscore-to-camel-case: true
|
||||||
|
cache-enabled: true
|
||||||
|
global-config:
|
||||||
|
banner: false
|
||||||
|
# SqlRunner.db().xxx 需要开启该开关,否则会报:
|
||||||
|
# Mapped Statements collection does not contain value for com.baomidou.mybatisplus.core.mapper.SqlRunner.Delete
|
||||||
|
enable-sql-runner: true
|
||||||
|
db-config:
|
||||||
|
id-type: auto
|
||||||
|
logic-delete-value: 1
|
||||||
|
logic-not-delete-value: 0
|
||||||
# 框架配置
|
# 框架配置
|
||||||
config:
|
config:
|
||||||
# 文件服务器
|
# 文件服务器
|
||||||
|
|||||||
@@ -82,7 +82,10 @@ mybatis-plus:
|
|||||||
map-underscore-to-camel-case: true
|
map-underscore-to-camel-case: true
|
||||||
cache-enabled: true
|
cache-enabled: true
|
||||||
global-config:
|
global-config:
|
||||||
:banner: false
|
banner: false
|
||||||
|
# SqlRunner.db().xxx 需要开启该开关,否则会报:
|
||||||
|
# Mapped Statements collection does not contain value for com.baomidou.mybatisplus.core.mapper.SqlRunner.Delete
|
||||||
|
enable-sql-runner: true
|
||||||
db-config:
|
db-config:
|
||||||
id-type: auto
|
id-type: auto
|
||||||
logic-delete-value: 1
|
logic-delete-value: 1
|
||||||
|
|||||||
Reference in New Issue
Block a user