update pay function
This commit is contained in:
274
src/services/predictionMarketService.api.js
Normal file
274
src/services/predictionMarketService.api.js
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* 预测市场服务 - API 版本
|
||||
* 调用真实的后端 API,数据存储到 MySQL 数据库
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { getApiBase } from '@utils/apiConfig';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: getApiBase(),
|
||||
timeout: 10000,
|
||||
withCredentials: true, // 携带 Cookie(session)
|
||||
});
|
||||
|
||||
// ==================== 积分系统 API ====================
|
||||
|
||||
/**
|
||||
* 获取用户积分账户
|
||||
*/
|
||||
export const getUserAccount = async () => {
|
||||
try {
|
||||
const response = await api.get('/api/prediction/credit/account');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取积分账户失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 领取每日奖励(100积分)
|
||||
*/
|
||||
export const claimDailyBonus = async () => {
|
||||
try {
|
||||
const response = await api.post('/api/prediction/credit/daily-bonus');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('领取每日奖励失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 预测话题 API ====================
|
||||
|
||||
/**
|
||||
* 创建预测话题
|
||||
* @param {Object} topicData - { title, description, category, deadline }
|
||||
*/
|
||||
export const createTopic = async (topicData) => {
|
||||
try {
|
||||
const response = await api.post('/api/prediction/topics', topicData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('创建预测话题失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取预测话题列表
|
||||
* @param {Object} params - { status, category, sort_by, page, per_page }
|
||||
*/
|
||||
export const getTopics = async (params = {}) => {
|
||||
try {
|
||||
const response = await api.get('/api/prediction/topics', { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取话题列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取预测话题详情
|
||||
* @param {number} topicId
|
||||
*/
|
||||
export const getTopicDetail = async (topicId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/prediction/topics/${topicId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取话题详情失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 结算预测话题(仅创建者可操作)
|
||||
* @param {number} topicId
|
||||
* @param {string} result - 'yes' | 'no' | 'draw'
|
||||
*/
|
||||
export const settleTopic = async (topicId, result) => {
|
||||
try {
|
||||
const response = await api.post(`/api/prediction/topics/${topicId}/settle`, { result });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('结算话题失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 交易 API ====================
|
||||
|
||||
/**
|
||||
* 买入预测份额
|
||||
* @param {Object} tradeData - { topic_id, direction, shares }
|
||||
*/
|
||||
export const buyShares = async (tradeData) => {
|
||||
try {
|
||||
const response = await api.post('/api/prediction/trade/buy', tradeData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('买入份额失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户持仓列表
|
||||
*/
|
||||
export const getUserPositions = async () => {
|
||||
try {
|
||||
const response = await api.get('/api/prediction/positions');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取持仓列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 评论 API ====================
|
||||
|
||||
/**
|
||||
* 发表话题评论
|
||||
* @param {number} topicId
|
||||
* @param {Object} commentData - { content, parent_id }
|
||||
*/
|
||||
export const createComment = async (topicId, commentData) => {
|
||||
try {
|
||||
const response = await api.post(`/api/prediction/topics/${topicId}/comments`, commentData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('发表评论失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取话题评论列表
|
||||
* @param {number} topicId
|
||||
* @param {Object} params - { page, per_page }
|
||||
*/
|
||||
export const getComments = async (topicId, params = {}) => {
|
||||
try {
|
||||
const response = await api.get(`/api/prediction/topics/${topicId}/comments`, { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取评论列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 点赞/取消点赞评论
|
||||
* @param {number} commentId
|
||||
*/
|
||||
export const likeComment = async (commentId) => {
|
||||
try {
|
||||
const response = await api.post(`/api/prediction/comments/${commentId}/like`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('点赞评论失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 工具函数(价格计算保留在前端,用于实时预览)====================
|
||||
|
||||
export const MARKET_CONFIG = {
|
||||
MAX_SEATS_PER_SIDE: 5,
|
||||
TAX_RATE: 0.02,
|
||||
MIN_PRICE: 50,
|
||||
MAX_PRICE: 950,
|
||||
BASE_PRICE: 500,
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算当前价格(简化版AMM)
|
||||
* @param {number} yesShares - Yes方总份额
|
||||
* @param {number} noShares - No方总份额
|
||||
* @returns {Object} {yes: price, no: price}
|
||||
*/
|
||||
export const calculatePrice = (yesShares, noShares) => {
|
||||
const totalShares = yesShares + noShares;
|
||||
|
||||
if (totalShares === 0) {
|
||||
return {
|
||||
yes: MARKET_CONFIG.BASE_PRICE,
|
||||
no: MARKET_CONFIG.BASE_PRICE,
|
||||
};
|
||||
}
|
||||
|
||||
const yesProb = yesShares / totalShares;
|
||||
const noProb = noShares / totalShares;
|
||||
|
||||
let yesPrice = yesProb * 1000;
|
||||
let noPrice = noProb * 1000;
|
||||
|
||||
yesPrice = Math.max(MARKET_CONFIG.MIN_PRICE, Math.min(MARKET_CONFIG.MAX_PRICE, yesPrice));
|
||||
noPrice = Math.max(MARKET_CONFIG.MIN_PRICE, Math.min(MARKET_CONFIG.MAX_PRICE, noPrice));
|
||||
|
||||
return { yes: Math.round(yesPrice), no: Math.round(noPrice) };
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算交易税
|
||||
* @param {number} amount - 交易金额
|
||||
* @returns {number} 税费
|
||||
*/
|
||||
export const calculateTax = (amount) => {
|
||||
return Math.floor(amount * MARKET_CONFIG.TAX_RATE);
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算买入成本(用于前端预览)
|
||||
* @param {number} currentShares - 当前方总份额
|
||||
* @param {number} otherShares - 对手方总份额
|
||||
* @param {number} buyAmount - 买入数量
|
||||
* @returns {Object} { amount, tax, total }
|
||||
*/
|
||||
export const calculateBuyCost = (currentShares, otherShares, buyAmount) => {
|
||||
const currentPrice = calculatePrice(currentShares, otherShares);
|
||||
const afterShares = currentShares + buyAmount;
|
||||
const afterPrice = calculatePrice(afterShares, otherShares);
|
||||
|
||||
const avgPrice = (currentPrice.yes + afterPrice.yes) / 2;
|
||||
const amount = avgPrice * buyAmount;
|
||||
const tax = calculateTax(amount);
|
||||
const total = amount + tax;
|
||||
|
||||
return {
|
||||
amount: Math.round(amount),
|
||||
tax: Math.round(tax),
|
||||
total: Math.round(total),
|
||||
avgPrice: Math.round(avgPrice),
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
// 积分系统
|
||||
getUserAccount,
|
||||
claimDailyBonus,
|
||||
|
||||
// 话题管理
|
||||
createTopic,
|
||||
getTopics,
|
||||
getTopicDetail,
|
||||
settleTopic,
|
||||
|
||||
// 交易
|
||||
buyShares,
|
||||
getUserPositions,
|
||||
|
||||
// 评论
|
||||
createComment,
|
||||
getComments,
|
||||
likeComment,
|
||||
|
||||
// 工具函数
|
||||
calculatePrice,
|
||||
calculateTax,
|
||||
calculateBuyCost,
|
||||
MARKET_CONFIG,
|
||||
};
|
||||
@@ -36,8 +36,7 @@ import {
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import { getTopic } from '@services/predictionMarketService';
|
||||
import { getUserAccount } from '@services/creditSystemService';
|
||||
import { getTopicDetail, getUserAccount } from '@services/predictionMarketService.api';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
import TradeModal from './components/TradeModal';
|
||||
|
||||
@@ -63,10 +62,11 @@ const PredictionTopicDetail = () => {
|
||||
|
||||
// 加载话题数据
|
||||
useEffect(() => {
|
||||
const loadTopic = () => {
|
||||
const topicData = getTopic(topicId);
|
||||
if (topicData) {
|
||||
setTopic(topicData);
|
||||
const loadTopic = async () => {
|
||||
try {
|
||||
const response = await getTopicDetail(topicId);
|
||||
if (response.success) {
|
||||
setTopic(response.data);
|
||||
} else {
|
||||
toast({
|
||||
title: '话题不存在',
|
||||
@@ -75,14 +75,33 @@ const PredictionTopicDetail = () => {
|
||||
});
|
||||
navigate('/value-forum');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取话题详情失败:', error);
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
navigate('/value-forum');
|
||||
}
|
||||
};
|
||||
|
||||
const loadAccount = async () => {
|
||||
if (!user) return;
|
||||
try {
|
||||
const response = await getUserAccount();
|
||||
if (response.success) {
|
||||
setUserAccount(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账户失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadTopic();
|
||||
|
||||
if (user) {
|
||||
setUserAccount(getUserAccount(user.id));
|
||||
}
|
||||
}, [topicId, user]);
|
||||
loadAccount();
|
||||
}, [topicId, user, toast, navigate]);
|
||||
|
||||
// 打开交易弹窗
|
||||
const handleOpenTrade = (mode) => {
|
||||
@@ -100,10 +119,21 @@ const PredictionTopicDetail = () => {
|
||||
};
|
||||
|
||||
// 交易成功回调
|
||||
const handleTradeSuccess = () => {
|
||||
const handleTradeSuccess = async () => {
|
||||
// 刷新话题数据
|
||||
setTopic(getTopic(topicId));
|
||||
setUserAccount(getUserAccount(user.id));
|
||||
try {
|
||||
const topicResponse = await getTopicDetail(topicId);
|
||||
if (topicResponse.success) {
|
||||
setTopic(topicResponse.data);
|
||||
}
|
||||
|
||||
const accountResponse = await getUserAccount();
|
||||
if (accountResponse.success) {
|
||||
setUserAccount(accountResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!topic) {
|
||||
|
||||
@@ -29,8 +29,8 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { Zap, Calendar, DollarSign } from 'lucide-react';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import { createTopic } from '@services/predictionMarketService';
|
||||
import { getUserAccount, CREDIT_CONFIG } from '@services/creditSystemService';
|
||||
import { createTopic, getUserAccount } from '@services/predictionMarketService.api';
|
||||
import { CREDIT_CONFIG } from '@services/creditSystemService';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
|
||||
const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
|
||||
@@ -46,9 +46,23 @@ const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [userAccount, setUserAccount] = useState(null);
|
||||
|
||||
// 获取用户余额
|
||||
const userAccount = user ? getUserAccount(user.id) : null;
|
||||
// 异步获取用户余额
|
||||
useEffect(() => {
|
||||
const fetchAccount = async () => {
|
||||
if (!user || !isOpen) return;
|
||||
try {
|
||||
const response = await getUserAccount();
|
||||
if (response.success) {
|
||||
setUserAccount(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账户失败:', error);
|
||||
}
|
||||
};
|
||||
fetchAccount();
|
||||
}, [user, isOpen]);
|
||||
|
||||
// 处理表单变化
|
||||
const handleChange = (field, value) => {
|
||||
@@ -80,7 +94,7 @@ const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
|
||||
}
|
||||
|
||||
// 检查余额
|
||||
if (userAccount.balance < CREDIT_CONFIG.CREATE_TOPIC_COST) {
|
||||
if (!userAccount || userAccount.balance < CREDIT_CONFIG.CREATE_TOPIC_COST) {
|
||||
toast({
|
||||
title: '积分不足',
|
||||
description: `创建话题需要${CREDIT_CONFIG.CREATE_TOPIC_COST}积分`,
|
||||
@@ -94,24 +108,18 @@ const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
|
||||
const deadline = new Date();
|
||||
deadline.setDate(deadline.getDate() + parseInt(formData.deadline_days));
|
||||
|
||||
const settlement_date = new Date(deadline);
|
||||
settlement_date.setDate(settlement_date.getDate() + 1);
|
||||
|
||||
// 创建话题
|
||||
const newTopic = createTopic({
|
||||
author_id: user.id,
|
||||
author_name: user.name || user.username,
|
||||
author_avatar: user.avatar,
|
||||
// 调用 API 创建话题
|
||||
const response = await createTopic({
|
||||
title: formData.title,
|
||||
description: formData.description,
|
||||
category: formData.category,
|
||||
deadline: deadline.toISOString(),
|
||||
settlement_date: settlement_date.toISOString(),
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
toast({
|
||||
title: '创建成功!',
|
||||
description: `话题已发布,扣除${CREDIT_CONFIG.CREATE_TOPIC_COST}积分`,
|
||||
description: `话题已发布,剩余 ${response.data.new_balance} 积分`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
@@ -126,10 +134,17 @@ const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
|
||||
|
||||
// 通知父组件
|
||||
if (onTopicCreated) {
|
||||
onTopicCreated(newTopic);
|
||||
onTopicCreated(response.data);
|
||||
}
|
||||
|
||||
onClose();
|
||||
|
||||
// 刷新账户数据
|
||||
const accountResponse = await getUserAccount();
|
||||
if (accountResponse.success) {
|
||||
setUserAccount(accountResponse.data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建话题失败:', error);
|
||||
toast({
|
||||
|
||||
@@ -33,14 +33,13 @@ import { TrendingUp, TrendingDown, DollarSign, AlertCircle, Zap } from 'lucide-r
|
||||
import { motion } from 'framer-motion';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import {
|
||||
buyPosition,
|
||||
sellPosition,
|
||||
buyShares,
|
||||
getUserAccount,
|
||||
calculateBuyCost,
|
||||
calculateSellRevenue,
|
||||
calculateTax,
|
||||
getTopic,
|
||||
} from '@services/predictionMarketService';
|
||||
import { getUserAccount, CREDIT_CONFIG } from '@services/creditSystemService';
|
||||
MARKET_CONFIG,
|
||||
} from '@services/predictionMarketService.api';
|
||||
import { CREDIT_CONFIG } from '@services/creditSystemService';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
@@ -53,9 +52,23 @@ const TradeModal = ({ isOpen, onClose, topic, mode = 'buy', onTradeSuccess }) =>
|
||||
const [selectedOption, setSelectedOption] = useState('yes');
|
||||
const [shares, setShares] = useState(1);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [userAccount, setUserAccount] = useState(null);
|
||||
|
||||
// 获取用户账户
|
||||
const userAccount = user ? getUserAccount(user.id) : null;
|
||||
// 异步获取用户账户
|
||||
useEffect(() => {
|
||||
const fetchAccount = async () => {
|
||||
if (!user || !isOpen) return;
|
||||
try {
|
||||
const response = await getUserAccount();
|
||||
if (response.success) {
|
||||
setUserAccount(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账户失败:', error);
|
||||
}
|
||||
};
|
||||
fetchAccount();
|
||||
}, [user, isOpen]);
|
||||
|
||||
// 重置状态
|
||||
useEffect(() => {
|
||||
@@ -79,15 +92,22 @@ const TradeModal = ({ isOpen, onClose, topic, mode = 'buy', onTradeSuccess }) =>
|
||||
let avgPrice = 0;
|
||||
|
||||
if (mode === 'buy') {
|
||||
cost = calculateBuyCost(selectedSide.total_shares, otherSide.total_shares, shares);
|
||||
tax = calculateTax(cost);
|
||||
totalCost = cost + tax;
|
||||
avgPrice = cost / shares;
|
||||
const costData = calculateBuyCost(
|
||||
selectedOption === 'yes' ? selectedSide.total_shares : otherSide.total_shares,
|
||||
selectedOption === 'yes' ? otherSide.total_shares : selectedSide.total_shares,
|
||||
shares
|
||||
);
|
||||
cost = costData.amount;
|
||||
tax = costData.tax;
|
||||
totalCost = costData.total;
|
||||
avgPrice = costData.avgPrice;
|
||||
} else {
|
||||
cost = calculateSellRevenue(selectedSide.total_shares, otherSide.total_shares, shares);
|
||||
// 卖出功能暂未实现,使用简化计算
|
||||
const currentPrice = selectedSide.current_price || MARKET_CONFIG.BASE_PRICE;
|
||||
cost = currentPrice * shares;
|
||||
tax = calculateTax(cost);
|
||||
totalCost = cost - tax;
|
||||
avgPrice = cost / shares;
|
||||
avgPrice = currentPrice;
|
||||
}
|
||||
|
||||
// 获取用户在该方向的持仓
|
||||
@@ -128,43 +148,49 @@ const TradeModal = ({ isOpen, onClose, topic, mode = 'buy', onTradeSuccess }) =>
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
let result;
|
||||
if (mode === 'buy') {
|
||||
result = buyPosition({
|
||||
user_id: user.id,
|
||||
user_name: user.name || user.username,
|
||||
user_avatar: user.avatar,
|
||||
// 调用买入 API
|
||||
const response = await buyShares({
|
||||
topic_id: topic.id,
|
||||
option_id: selectedOption,
|
||||
direction: selectedOption,
|
||||
shares,
|
||||
});
|
||||
} else {
|
||||
result = sellPosition({
|
||||
user_id: user.id,
|
||||
topic_id: topic.id,
|
||||
option_id: selectedOption,
|
||||
shares,
|
||||
});
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
toast({
|
||||
title: mode === 'buy' ? '购买成功!' : '卖出成功!',
|
||||
description: mode === 'buy' ? `花费${totalCost}积分` : `获得${totalCost}积分`,
|
||||
title: '购买成功!',
|
||||
description: `花费${totalCost}积分,剩余 ${response.data.new_balance} 积分`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
// 刷新账户数据
|
||||
const accountResponse = await getUserAccount();
|
||||
if (accountResponse.success) {
|
||||
setUserAccount(accountResponse.data);
|
||||
}
|
||||
|
||||
// 通知父组件刷新
|
||||
if (onTradeSuccess) {
|
||||
onTradeSuccess(result);
|
||||
onTradeSuccess(response.data);
|
||||
}
|
||||
|
||||
onClose();
|
||||
}
|
||||
} else {
|
||||
// 卖出功能暂未实现
|
||||
toast({
|
||||
title: '功能暂未开放',
|
||||
description: '卖出功能正在开发中,敬请期待',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('交易失败:', error);
|
||||
toast({
|
||||
title: '交易失败',
|
||||
description: error.message,
|
||||
description: error.response?.data?.message || error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ import { Search, PenSquare, TrendingUp, Clock, Heart, Zap, HelpCircle } from 'lu
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import { getPosts, searchPosts } from '@services/elasticsearchService';
|
||||
import { getTopics } from '@services/predictionMarketService';
|
||||
import { getTopics } from '@services/predictionMarketService.api';
|
||||
import PostCard from './components/PostCard';
|
||||
import PredictionTopicCard from './components/PredictionTopicCard';
|
||||
import CreatePostModal from './components/CreatePostModal';
|
||||
@@ -93,11 +93,13 @@ const ValueForum = () => {
|
||||
};
|
||||
|
||||
// 获取预测话题列表
|
||||
const fetchPredictionTopics = () => {
|
||||
const fetchPredictionTopics = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const topics = getTopics({ status: 'active', sortBy });
|
||||
setPredictionTopics(topics);
|
||||
const response = await getTopics({ status: 'active', sort_by: sortBy });
|
||||
if (response.success) {
|
||||
setPredictionTopics(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预测话题失败:', error);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user