fix: localize customer-facing errors
Deploy olons-server production / deploy (push) Successful in 1m48s
Deploy olons-server production / deploy (push) Successful in 1m48s
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,6 +24,85 @@ type apiError struct {
|
|||||||
ServerTime string `json:"server_time"`
|
ServerTime string `json:"server_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var apiErrorMessages = map[string]string{
|
||||||
|
"MALFORMED_REQUEST": "请求参数格式错误",
|
||||||
|
"METHOD_NOT_ALLOWED": "当前请求方式不支持",
|
||||||
|
"AUTH_REQUIRED": "登录已失效,请重新登录",
|
||||||
|
"FORBIDDEN": "当前账户无权执行此操作",
|
||||||
|
"INTERNAL": "服务异常,请稍后重试",
|
||||||
|
"SERVICE_UNAVAILABLE": "服务暂不可用,请稍后重试",
|
||||||
|
"USERNAME_INVALID_FORMAT": "用户名格式不正确",
|
||||||
|
"PHONE_INVALID_FORMAT": "手机号码格式不正确",
|
||||||
|
"PASSWORD_INVALID_FORMAT": "密码格式不正确",
|
||||||
|
"REFERRAL_CODE_INVALID": "邀请码无效,请检查后重新输入",
|
||||||
|
"PRIVACY_POLICY_NOT_ACCEPTED": "请先阅读并同意用户协议和隐私政策",
|
||||||
|
"ACCOUNT_ALREADY_REGISTERED": "该账户或手机号已经注册",
|
||||||
|
"INVALID_CREDENTIALS": "手机号或密码错误",
|
||||||
|
"RATE_LIMITED": "操作过于频繁,请稍后再试",
|
||||||
|
"AVATAR_ATTACHMENT_INVALID": "头像图片无效,请重新上传",
|
||||||
|
"CURRENT_PASSWORD_INVALID": "当前登录密码不正确",
|
||||||
|
"LOGIN_PASSWORD_INVALID": "登录密码不正确",
|
||||||
|
"NEW_PASSWORD_INVALID": "新密码格式不正确",
|
||||||
|
"OLD_TRADE_PASSWORD_REQUIRED": "请输入原支付密码",
|
||||||
|
"OLD_TRADE_PASSWORD_INVALID": "原支付密码不正确",
|
||||||
|
"TRADE_PASSWORD_INVALID": "支付密码不正确",
|
||||||
|
"TRADE_PASSWORD_NOT_SET": "请先设置支付密码",
|
||||||
|
"ACCOUNT_NOT_FOUND": "账户不存在",
|
||||||
|
"ACCOUNT_CHANGED": "账户信息已发生变化,请重试",
|
||||||
|
"CATEGORY_ID_INVALID": "商品分类参数不正确",
|
||||||
|
"PRODUCT_ID_INVALID": "商品参数不正确",
|
||||||
|
"PRODUCT_NOT_FOUND": "商品不存在或已下架",
|
||||||
|
"PAGE_INVALID": "页码参数不正确",
|
||||||
|
"PAGE_SIZE_INVALID": "每页数量参数不正确",
|
||||||
|
"IMAGE_INVALID": "图片格式或大小不符合要求",
|
||||||
|
"HALL_ID_INVALID": "任务大厅参数不正确",
|
||||||
|
"ORDER_ID_INVALID": "订单参数不正确",
|
||||||
|
"ORDER_STATUS_INVALID": "订单状态参数不正确",
|
||||||
|
"EVIDENCE_INVALID": "任务凭证不符合要求",
|
||||||
|
"TASK_DISABLED": "领取功能暂未开放",
|
||||||
|
"OUTSIDE_BUSINESS_HOURS": "当前不在营业时间内",
|
||||||
|
"INSUFFICIENT_BALANCE": "可用余额不足",
|
||||||
|
"DAILY_LIMIT_REACHED": "今日领取次数已用完",
|
||||||
|
"LEVEL_NOT_ELIGIBLE": "当前会员等级不满足领取条件",
|
||||||
|
"ACCOUNT_RESTRICTED": "账户状态受限,请联系客服",
|
||||||
|
"PENDING_ORDER_EXISTS": "存在未完成的任务,请先处理",
|
||||||
|
"NO_TASK_AVAILABLE": "当前暂无可领取任务",
|
||||||
|
"ORDER_NOT_FOUND": "任务订单不存在",
|
||||||
|
"ORDER_STATE_INVALID": "当前任务状态不允许此操作",
|
||||||
|
"ORDER_EXPIRED": "该任务已超时",
|
||||||
|
"RESOURCE_NOT_FOUND": "请求的内容不存在",
|
||||||
|
"CUSTOMER_SERVICE_NOT_FOUND": "客服已下线或不存在",
|
||||||
|
"CUSTOMER_SERVICE_NOT_BOUND": "客服账号尚未配置",
|
||||||
|
"IM_RELATION_PENDING": "好友申请已发送,请等待客服确认",
|
||||||
|
"IM_RELATION_NOT_ALLOWED": "当前无法与该客服建立会话",
|
||||||
|
"IM_SERVICE_UNAVAILABLE": "客服系统暂不可用,请稍后重试",
|
||||||
|
"ACCOUNT_TYPE_INVALID": "收款账户类型不正确",
|
||||||
|
"PAYMENT_ACCOUNT_FIELDS_INVALID": "收款账户信息填写不完整或格式不正确",
|
||||||
|
"CURRENCY_INVALID": "币种参数不正确",
|
||||||
|
"WITHDRAWAL_AMOUNT_INVALID": "取出金额不符合当前规则",
|
||||||
|
"RECHARGE_AMOUNT_INVALID": "储入金额不符合当前规则",
|
||||||
|
"RECHARGE_REMARK_INVALID": "储入备注格式不正确",
|
||||||
|
"RECHARGE_VOUCHER_INVALID": "储入凭证无效,请重新上传",
|
||||||
|
"IDEMPOTENCY_KEY_INVALID": "请求标识无效,请刷新后重试",
|
||||||
|
"LEDGER_FILTER_INVALID": "账变记录筛选条件不正确",
|
||||||
|
"WITHDRAWAL_STATUS_INVALID": "取出状态参数不正确",
|
||||||
|
"RECHARGE_STATUS_INVALID": "储入状态参数不正确",
|
||||||
|
"RECHARGE_ORDER_ID_INVALID": "储入订单参数不正确",
|
||||||
|
"WITHDRAWAL_ORDER_ID_INVALID": "取出订单参数不正确",
|
||||||
|
"PAYMENT_ACCOUNT_EXISTS": "该收款账户已经存在",
|
||||||
|
"PAYMENT_ACCOUNT_INVALID": "收款账户未核验或不可用",
|
||||||
|
"WITHDRAWAL_DISABLED": "取出功能暂未开放",
|
||||||
|
"WITHDRAWAL_CHANNEL_UNAVAILABLE": "当前没有可用的取出通道",
|
||||||
|
"RECHARGE_CHANNEL_UNAVAILABLE": "当前没有可用的储入通道",
|
||||||
|
"WITHDRAWAL_DAILY_LIMIT_REACHED": "今日取出次数已用完",
|
||||||
|
"WITHDRAWAL_ORDER_NOT_FOUND": "取出订单不存在",
|
||||||
|
"RECHARGE_ORDER_NOT_FOUND": "储入订单不存在",
|
||||||
|
"IDEMPOTENCY_CONFLICT": "请勿重复提交不同的请求",
|
||||||
|
"QR_ATTACHMENT_INVALID": "二维码图片无效,请重新上传",
|
||||||
|
"BANNER_POSITION_INVALID": "轮播图位置参数不正确",
|
||||||
|
"CONTENT_KEY_INVALID": "内容页面参数不正确",
|
||||||
|
}
|
||||||
|
|
||||||
func writeSuccess(w http.ResponseWriter, r *http.Request, data any) {
|
func writeSuccess(w http.ResponseWriter, r *http.Request, data any) {
|
||||||
writeJSON(w, r, http.StatusOK, map[string]any{
|
writeJSON(w, r, http.StatusOK, map[string]any{
|
||||||
"code": 0, "data": data, "request_id": requestID(r),
|
"code": 0, "data": data, "request_id": requestID(r),
|
||||||
@@ -31,10 +111,20 @@ func writeSuccess(w http.ResponseWriter, r *http.Request, data any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeError(w http.ResponseWriter, r *http.Request, status, code int, reason, message string, details map[string]any) {
|
func writeError(w http.ResponseWriter, r *http.Request, status, code int, reason, message string, details map[string]any) {
|
||||||
writeJSON(w, r, status, apiError{Code: code, Reason: reason, Message: message, Details: details,
|
writeJSON(w, r, status, apiError{Code: code, Reason: reason, Message: localizedAPIErrorMessage(reason, message), Details: details,
|
||||||
RequestID: requestID(r), ServerTime: time.Now().UTC().Format(time.RFC3339Nano)})
|
RequestID: requestID(r), ServerTime: time.Now().UTC().Format(time.RFC3339Nano)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func localizedAPIErrorMessage(reason, message string) string {
|
||||||
|
if localized := apiErrorMessages[reason]; localized != "" {
|
||||||
|
return localized
|
||||||
|
}
|
||||||
|
if strings.IndexFunc(message, func(r rune) bool { return unicode.Is(unicode.Han, r) }) >= 0 {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return "请求失败,请稍后重试"
|
||||||
|
}
|
||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, r *http.Request, status int, value any) {
|
func writeJSON(w http.ResponseWriter, r *http.Request, status int, value any) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
if requestID := requestID(r); requestID != "" {
|
if requestID := requestID(r); requestID != "" {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLocalizedAPIErrorMessage(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
reason string
|
||||||
|
message string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "login password", reason: "LOGIN_PASSWORD_INVALID", message: "login password is invalid", want: "登录密码不正确"},
|
||||||
|
{name: "known validation", reason: "PHONE_INVALID_FORMAT", message: "validation failed", want: "手机号码格式不正确"},
|
||||||
|
{name: "preserve chinese", reason: "CUSTOM_REASON", message: "自定义中文提示", want: "自定义中文提示"},
|
||||||
|
{name: "hide unknown english", reason: "CUSTOM_REASON", message: "unexpected dependency error", want: "请求失败,请稍后重试"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got := localizedAPIErrorMessage(test.reason, test.message); got != test.want {
|
||||||
|
t.Fatalf("localizedAPIErrorMessage() = %q, want %q", got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { dateTime, dateTimeSeconds } from './api';
|
import { dateTime, dateTimeSeconds, errorText } from './api';
|
||||||
|
|
||||||
describe('Beijing business time display', () => {
|
describe('Beijing business time display', () => {
|
||||||
it.each(['UTC', 'America/New_York', 'Asia/Shanghai'])(
|
it.each(['UTC', 'America/New_York', 'Asia/Shanghai'])(
|
||||||
@@ -28,3 +28,13 @@ describe('Beijing business time display', () => {
|
|||||||
expect(dateTimeSeconds('invalid-time')).toBe('--');
|
expect(dateTimeSeconds('invalid-time')).toBe('--');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Chinese error display', () => {
|
||||||
|
it('keeps Chinese business messages', () => {
|
||||||
|
expect(errorText(new Error('登录密码不正确'))).toBe('登录密码不正确');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not expose raw English errors to users', () => {
|
||||||
|
expect(errorText(new Error('login password is invalid'))).toBe('操作失败,请稍后重试');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+56
-5
@@ -5,33 +5,75 @@ const DEVICE_KEY = 'olons_web_device';
|
|||||||
const LOGIN_NOTICE_PENDING_KEY = 'olons_login_notice_pending';
|
const LOGIN_NOTICE_PENDING_KEY = 'olons_login_notice_pending';
|
||||||
|
|
||||||
const reasonMessages: Record<string, string> = {
|
const reasonMessages: Record<string, string> = {
|
||||||
|
MALFORMED_REQUEST: '请求参数格式错误',
|
||||||
|
METHOD_NOT_ALLOWED: '当前请求方式不支持',
|
||||||
AUTH_REQUIRED: '登录已失效,请重新登录',
|
AUTH_REQUIRED: '登录已失效,请重新登录',
|
||||||
FORBIDDEN: '当前账户无权执行此操作',
|
FORBIDDEN: '当前账户无权执行此操作',
|
||||||
|
USERNAME_INVALID_FORMAT: '用户名格式不正确',
|
||||||
|
PHONE_INVALID_FORMAT: '手机号码格式不正确',
|
||||||
|
PASSWORD_INVALID_FORMAT: '密码格式不正确',
|
||||||
INVALID_CREDENTIALS: '手机号或密码错误',
|
INVALID_CREDENTIALS: '手机号或密码错误',
|
||||||
ACCOUNT_ALREADY_REGISTERED: '该账户或手机号已经注册',
|
ACCOUNT_ALREADY_REGISTERED: '该账户或手机号已经注册',
|
||||||
REFERRAL_CODE_INVALID: '邀请码无效,请检查后重新输入',
|
REFERRAL_CODE_INVALID: '邀请码无效,请检查后重新输入',
|
||||||
|
PRIVACY_POLICY_NOT_ACCEPTED: '请先阅读并同意用户协议和隐私政策',
|
||||||
RATE_LIMITED: '操作过于频繁,请稍后再试',
|
RATE_LIMITED: '操作过于频繁,请稍后再试',
|
||||||
|
AVATAR_ATTACHMENT_INVALID: '头像图片无效,请重新上传',
|
||||||
|
CURRENT_PASSWORD_INVALID: '当前登录密码不正确',
|
||||||
|
LOGIN_PASSWORD_INVALID: '登录密码不正确',
|
||||||
|
NEW_PASSWORD_INVALID: '新密码格式不正确',
|
||||||
|
OLD_TRADE_PASSWORD_REQUIRED: '请输入原支付密码',
|
||||||
|
OLD_TRADE_PASSWORD_INVALID: '原支付密码不正确',
|
||||||
|
ACCOUNT_NOT_FOUND: '账户不存在',
|
||||||
|
ACCOUNT_CHANGED: '账户信息已发生变化,请重试',
|
||||||
|
CATEGORY_ID_INVALID: '商品分类参数不正确',
|
||||||
|
PRODUCT_ID_INVALID: '商品参数不正确',
|
||||||
|
PRODUCT_NOT_FOUND: '商品不存在或已下架',
|
||||||
|
PAGE_INVALID: '页码参数不正确',
|
||||||
|
PAGE_SIZE_INVALID: '每页数量参数不正确',
|
||||||
|
HALL_ID_INVALID: '任务大厅参数不正确',
|
||||||
|
ORDER_ID_INVALID: '订单参数不正确',
|
||||||
|
ORDER_STATUS_INVALID: '订单状态参数不正确',
|
||||||
|
EVIDENCE_INVALID: '任务凭证不符合要求',
|
||||||
TASK_DISABLED: '领取功能暂未开放',
|
TASK_DISABLED: '领取功能暂未开放',
|
||||||
OUTSIDE_BUSINESS_HOURS: '当前不在营业时间内',
|
OUTSIDE_BUSINESS_HOURS: '当前不在营业时间内',
|
||||||
INSUFFICIENT_BALANCE: '可用余额不足299元,暂时无法领取',
|
INSUFFICIENT_BALANCE: '可用余额不足299元,暂时无法领取',
|
||||||
DAILY_LIMIT_REACHED: '今日领取次数已用完',
|
DAILY_LIMIT_REACHED: '今日领取次数已用完',
|
||||||
|
LEVEL_NOT_ELIGIBLE: '当前会员等级不满足领取条件',
|
||||||
|
ACCOUNT_RESTRICTED: '账户状态受限,请联系客服',
|
||||||
PENDING_ORDER_EXISTS: '存在未完成的任务,请先处理',
|
PENDING_ORDER_EXISTS: '存在未完成的任务,请先处理',
|
||||||
NO_TASK_AVAILABLE: '当前暂无可领取任务',
|
NO_TASK_AVAILABLE: '当前暂无可领取任务',
|
||||||
|
ORDER_NOT_FOUND: '任务订单不存在',
|
||||||
ORDER_EXPIRED: '该任务已超时',
|
ORDER_EXPIRED: '该任务已超时',
|
||||||
ORDER_STATE_INVALID: '当前任务状态不允许此操作',
|
ORDER_STATE_INVALID: '当前任务状态不允许此操作',
|
||||||
TRADE_PASSWORD_NOT_SET: '请先设置支付密码',
|
TRADE_PASSWORD_NOT_SET: '请先设置支付密码',
|
||||||
TRADE_PASSWORD_INVALID: '支付密码不正确',
|
TRADE_PASSWORD_INVALID: '支付密码不正确',
|
||||||
|
ACCOUNT_TYPE_INVALID: '收款账户类型不正确',
|
||||||
PAYMENT_ACCOUNT_FIELDS_INVALID: '绑卡信息填写不完整或格式不正确',
|
PAYMENT_ACCOUNT_FIELDS_INVALID: '绑卡信息填写不完整或格式不正确',
|
||||||
|
CURRENCY_INVALID: '币种参数不正确',
|
||||||
QR_ATTACHMENT_INVALID: '二维码图片无效,请重新上传',
|
QR_ATTACHMENT_INVALID: '二维码图片无效,请重新上传',
|
||||||
IMAGE_INVALID: '图片格式或大小不符合要求',
|
IMAGE_INVALID: '图片格式或大小不符合要求',
|
||||||
|
RECHARGE_REMARK_INVALID: '储入备注格式不正确',
|
||||||
PAYMENT_ACCOUNT_INVALID: '收款账户未核验或不可用',
|
PAYMENT_ACCOUNT_INVALID: '收款账户未核验或不可用',
|
||||||
|
PAYMENT_ACCOUNT_EXISTS: '该收款账户已经存在',
|
||||||
WITHDRAWAL_DISABLED: '取出功能暂未开放',
|
WITHDRAWAL_DISABLED: '取出功能暂未开放',
|
||||||
WITHDRAWAL_AMOUNT_INVALID: '取出金额不符合当前规则',
|
WITHDRAWAL_AMOUNT_INVALID: '取出金额不符合当前规则',
|
||||||
WITHDRAWAL_DAILY_LIMIT_REACHED: '今日取出次数已用完',
|
WITHDRAWAL_DAILY_LIMIT_REACHED: '今日取出次数已用完',
|
||||||
WITHDRAWAL_CHANNEL_UNAVAILABLE: '当前没有可用的取出通道',
|
WITHDRAWAL_CHANNEL_UNAVAILABLE: '当前没有可用的取出通道',
|
||||||
|
WITHDRAWAL_STATUS_INVALID: '取出状态参数不正确',
|
||||||
|
WITHDRAWAL_ORDER_ID_INVALID: '取出订单参数不正确',
|
||||||
|
WITHDRAWAL_ORDER_NOT_FOUND: '取出订单不存在',
|
||||||
RECHARGE_AMOUNT_INVALID: '储入金额不符合当前通道规则',
|
RECHARGE_AMOUNT_INVALID: '储入金额不符合当前通道规则',
|
||||||
RECHARGE_CHANNEL_UNAVAILABLE: '当前没有可用的储入通道',
|
RECHARGE_CHANNEL_UNAVAILABLE: '当前没有可用的储入通道',
|
||||||
RECHARGE_VOUCHER_INVALID: '储入凭证无效',
|
RECHARGE_VOUCHER_INVALID: '储入凭证无效',
|
||||||
|
RECHARGE_STATUS_INVALID: '储入状态参数不正确',
|
||||||
|
RECHARGE_ORDER_ID_INVALID: '储入订单参数不正确',
|
||||||
|
RECHARGE_ORDER_NOT_FOUND: '储入订单不存在',
|
||||||
|
IDEMPOTENCY_KEY_INVALID: '请求标识无效,请刷新后重试',
|
||||||
|
IDEMPOTENCY_CONFLICT: '请勿重复提交不同的请求',
|
||||||
|
LEDGER_FILTER_INVALID: '账变记录筛选条件不正确',
|
||||||
|
RESOURCE_NOT_FOUND: '请求的内容不存在',
|
||||||
|
BANNER_POSITION_INVALID: '轮播图位置参数不正确',
|
||||||
|
CONTENT_KEY_INVALID: '内容页面参数不正确',
|
||||||
SERVICE_UNAVAILABLE: '服务暂不可用,请稍后重试',
|
SERVICE_UNAVAILABLE: '服务暂不可用,请稍后重试',
|
||||||
CUSTOMER_SERVICE_NOT_FOUND: '客服已下线或不存在',
|
CUSTOMER_SERVICE_NOT_FOUND: '客服已下线或不存在',
|
||||||
CUSTOMER_SERVICE_NOT_BOUND: '客服账号尚未配置',
|
CUSTOMER_SERVICE_NOT_BOUND: '客服账号尚未配置',
|
||||||
@@ -90,9 +132,7 @@ client.interceptors.response.use(
|
|||||||
const reason = payload?.reason || '';
|
const reason = payload?.reason || '';
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new MobileApiError(
|
new MobileApiError(
|
||||||
reasonMessages[reason] ||
|
localizedErrorMessage(reason, payload?.message, status),
|
||||||
payload?.message ||
|
|
||||||
(status ? '请求失败,请稍后重试' : '网络连接失败'),
|
|
||||||
reason,
|
reason,
|
||||||
status
|
status
|
||||||
)
|
)
|
||||||
@@ -110,7 +150,7 @@ export async function request<T>(config: AxiosRequestConfig): Promise<T> {
|
|||||||
if (response.data.code !== 0) {
|
if (response.data.code !== 0) {
|
||||||
const reason = response.data.reason || '';
|
const reason = response.data.reason || '';
|
||||||
throw new MobileApiError(
|
throw new MobileApiError(
|
||||||
reasonMessages[reason] || response.data.message || '请求失败',
|
localizedErrorMessage(reason, response.data.message, response.status),
|
||||||
reason,
|
reason,
|
||||||
response.status
|
response.status
|
||||||
);
|
);
|
||||||
@@ -218,5 +258,16 @@ export function safeImage(url?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function errorText(error: unknown) {
|
export function errorText(error: unknown) {
|
||||||
return error instanceof Error ? error.message : '操作失败,请稍后重试';
|
if (!(error instanceof Error)) return '操作失败,请稍后重试';
|
||||||
|
return containsChinese(error.message) ? error.message : '操作失败,请稍后重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
function localizedErrorMessage(reason: string, message: string | undefined, status: number) {
|
||||||
|
if (reasonMessages[reason]) return reasonMessages[reason];
|
||||||
|
if (message && containsChinese(message)) return message;
|
||||||
|
return status ? '请求失败,请稍后重试' : '网络连接失败,请检查网络后重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsChinese(value: string) {
|
||||||
|
return /[\u3400-\u9fff]/.test(value);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user