diff --git a/src/services/predictionMarketService.api.js b/src/services/predictionMarketService.api.js new file mode 100644 index 00000000..067c4f39 --- /dev/null +++ b/src/services/predictionMarketService.api.js @@ -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, +}; diff --git a/src/views/ValueForum/PredictionTopicDetail.js b/src/views/ValueForum/PredictionTopicDetail.js index 3c031869..aafea33d 100644 --- a/src/views/ValueForum/PredictionTopicDetail.js +++ b/src/views/ValueForum/PredictionTopicDetail.js @@ -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,13 +62,24 @@ const PredictionTopicDetail = () => { // 加载话题数据 useEffect(() => { - const loadTopic = () => { - const topicData = getTopic(topicId); - if (topicData) { - setTopic(topicData); - } else { + const loadTopic = async () => { + try { + const response = await getTopicDetail(topicId); + if (response.success) { + setTopic(response.data); + } else { + toast({ + title: '话题不存在', + status: 'error', + duration: 3000, + }); + navigate('/value-forum'); + } + } catch (error) { + console.error('获取话题详情失败:', error); toast({ - title: '话题不存在', + title: '加载失败', + description: error.message, status: 'error', duration: 3000, }); @@ -77,12 +87,21 @@ const PredictionTopicDetail = () => { } }; - loadTopic(); + const loadAccount = async () => { + if (!user) return; + try { + const response = await getUserAccount(); + if (response.success) { + setUserAccount(response.data); + } + } catch (error) { + console.error('获取账户失败:', error); + } + }; - if (user) { - setUserAccount(getUserAccount(user.id)); - } - }, [topicId, user]); + loadTopic(); + 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) { diff --git a/src/views/ValueForum/components/CreatePredictionModal.js b/src/views/ValueForum/components/CreatePredictionModal.js index 4fd3ee6c..eb39663d 100644 --- a/src/views/ValueForum/components/CreatePredictionModal.js +++ b/src/views/ValueForum/components/CreatePredictionModal.js @@ -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,42 +108,43 @@ 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(), }); - toast({ - title: '创建成功!', - description: `话题已发布,扣除${CREDIT_CONFIG.CREATE_TOPIC_COST}积分`, - status: 'success', - duration: 3000, - }); + if (response.success) { + toast({ + title: '创建成功!', + description: `话题已发布,剩余 ${response.data.new_balance} 积分`, + status: 'success', + duration: 3000, + }); - // 重置表单 - setFormData({ - title: '', - description: '', - category: 'stock', - deadline_days: 7, - }); + // 重置表单 + setFormData({ + title: '', + description: '', + category: 'stock', + deadline_days: 7, + }); - // 通知父组件 - if (onTopicCreated) { - onTopicCreated(newTopic); + // 通知父组件 + if (onTopicCreated) { + onTopicCreated(response.data); + } + + onClose(); + + // 刷新账户数据 + const accountResponse = await getUserAccount(); + if (accountResponse.success) { + setUserAccount(accountResponse.data); + } } - - onClose(); } catch (error) { console.error('创建话题失败:', error); toast({ diff --git a/src/views/ValueForum/components/TradeModal.js b/src/views/ValueForum/components/TradeModal.js index 14389c35..9e475c57 100644 --- a/src/views/ValueForum/components/TradeModal.js +++ b/src/views/ValueForum/components/TradeModal.js @@ -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, }); + + if (response.success) { + toast({ + title: '购买成功!', + description: `花费${totalCost}积分,剩余 ${response.data.new_balance} 积分`, + status: 'success', + duration: 3000, + }); + + // 刷新账户数据 + const accountResponse = await getUserAccount(); + if (accountResponse.success) { + setUserAccount(accountResponse.data); + } + + // 通知父组件刷新 + if (onTradeSuccess) { + onTradeSuccess(response.data); + } + + onClose(); + } } else { - result = sellPosition({ - user_id: user.id, - topic_id: topic.id, - option_id: selectedOption, - shares, + // 卖出功能暂未实现 + toast({ + title: '功能暂未开放', + description: '卖出功能正在开发中,敬请期待', + status: 'warning', + duration: 3000, }); } - - toast({ - title: mode === 'buy' ? '购买成功!' : '卖出成功!', - description: mode === 'buy' ? `花费${totalCost}积分` : `获得${totalCost}积分`, - status: 'success', - duration: 3000, - }); - - // 通知父组件刷新 - if (onTradeSuccess) { - onTradeSuccess(result); - } - - onClose(); } catch (error) { console.error('交易失败:', error); toast({ title: '交易失败', - description: error.message, + description: error.response?.data?.message || error.message, status: 'error', duration: 3000, }); diff --git a/src/views/ValueForum/index.js b/src/views/ValueForum/index.js index 3d5444f7..b0e748c7 100644 --- a/src/views/ValueForum/index.js +++ b/src/views/ValueForum/index.js @@ -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 {