Files
vf_react/prediction_market_routes_to_add.py
2025-11-23 19:44:49 +08:00

837 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ===========================
# 预测市场 API 路由
# 请将此文件内容插入到 app.py 的 `if __name__ == '__main__':` 之前
# ===========================
# --- 积分系统 API ---
@app.route('/api/prediction/credit/account', methods=['GET'])
@login_required
def get_credit_account():
"""获取用户积分账户"""
try:
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
# 如果账户不存在,自动创建
if not account:
account = UserCreditAccount(user_id=current_user.id)
db.session.add(account)
db.session.commit()
return jsonify({
'success': True,
'data': {
'balance': float(account.balance),
'frozen_balance': float(account.frozen_balance),
'available_balance': float(account.balance - account.frozen_balance),
'total_earned': float(account.total_earned),
'total_spent': float(account.total_spent),
'last_daily_bonus_at': account.last_daily_bonus_at.isoformat() if account.last_daily_bonus_at else None
}
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/prediction/credit/daily-bonus', methods=['POST'])
@login_required
def claim_daily_bonus():
"""领取每日奖励100积分"""
try:
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
if not account:
account = UserCreditAccount(user_id=current_user.id)
db.session.add(account)
# 检查是否已领取今日奖励
today = beijing_now().date()
if account.last_daily_bonus_at and account.last_daily_bonus_at.date() == today:
return jsonify({
'success': False,
'error': '今日奖励已领取'
}), 400
# 发放奖励
bonus_amount = 100.0
account.balance += bonus_amount
account.total_earned += bonus_amount
account.last_daily_bonus_at = beijing_now()
# 记录交易
transaction = CreditTransaction(
user_id=current_user.id,
transaction_type='daily_bonus',
amount=bonus_amount,
balance_after=account.balance,
description='每日登录奖励'
)
db.session.add(transaction)
db.session.commit()
return jsonify({
'success': True,
'message': f'领取成功,获得 {bonus_amount} 积分',
'data': {
'bonus_amount': bonus_amount,
'new_balance': float(account.balance)
}
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 500
# --- 预测话题 API ---
@app.route('/api/prediction/topics', methods=['POST'])
@login_required
def create_prediction_topic():
"""创建预测话题消耗100积分"""
try:
data = request.get_json()
title = data.get('title', '').strip()
description = data.get('description', '').strip()
category = data.get('category', 'stock')
deadline_str = data.get('deadline')
# 验证参数
if not title or len(title) < 5:
return jsonify({'success': False, 'error': '标题至少5个字符'}), 400
if not deadline_str:
return jsonify({'success': False, 'error': '请设置截止时间'}), 400
# 解析截止时间
deadline = datetime.fromisoformat(deadline_str.replace('Z', '+00:00'))
if deadline <= beijing_now():
return jsonify({'success': False, 'error': '截止时间必须在未来'}), 400
# 检查积分账户
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
if not account or account.balance < 100:
return jsonify({'success': False, 'error': '积分不足需要100积分'}), 400
# 扣除创建费用
create_cost = 100.0
account.balance -= create_cost
account.total_spent += create_cost
# 创建话题
topic = PredictionTopic(
creator_id=current_user.id,
title=title,
description=description,
category=category,
deadline=deadline
)
db.session.add(topic)
# 记录积分交易
transaction = CreditTransaction(
user_id=current_user.id,
transaction_type='create_topic',
amount=-create_cost,
balance_after=account.balance,
description=f'创建预测话题:{title}'
)
db.session.add(transaction)
db.session.commit()
return jsonify({
'success': True,
'message': '话题创建成功',
'data': {
'topic_id': topic.id,
'title': topic.title,
'new_balance': float(account.balance)
}
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/prediction/topics', methods=['GET'])
def get_prediction_topics():
"""获取预测话题列表"""
try:
status = request.args.get('status', 'active')
category = request.args.get('category')
sort_by = request.args.get('sort_by', 'created_at')
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
# 构建查询
query = PredictionTopic.query
if status:
query = query.filter_by(status=status)
if category:
query = query.filter_by(category=category)
# 排序
if sort_by == 'hot':
query = query.order_by(desc(PredictionTopic.views_count))
elif sort_by == 'participants':
query = query.order_by(desc(PredictionTopic.participants_count))
else:
query = query.order_by(desc(PredictionTopic.created_at))
# 分页
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
topics = pagination.items
# 格式化返回数据
topics_data = []
for topic in topics:
# 计算市场倾向
total_shares = topic.yes_total_shares + topic.no_total_shares
yes_prob = (topic.yes_total_shares / total_shares * 100) if total_shares > 0 else 50.0
topics_data.append({
'id': topic.id,
'title': topic.title,
'description': topic.description,
'category': topic.category,
'status': topic.status,
'yes_price': float(topic.yes_price),
'no_price': float(topic.no_price),
'yes_probability': round(yes_prob, 1),
'total_pool': float(topic.total_pool),
'yes_lord': {
'id': topic.yes_lord.id,
'username': topic.yes_lord.username,
'nickname': topic.yes_lord.nickname or topic.yes_lord.username,
'avatar_url': topic.yes_lord.avatar_url
} if topic.yes_lord else None,
'no_lord': {
'id': topic.no_lord.id,
'username': topic.no_lord.username,
'nickname': topic.no_lord.nickname or topic.no_lord.username,
'avatar_url': topic.no_lord.avatar_url
} if topic.no_lord else None,
'deadline': topic.deadline.isoformat(),
'created_at': topic.created_at.isoformat(),
'views_count': topic.views_count,
'comments_count': topic.comments_count,
'participants_count': topic.participants_count,
'creator': {
'id': topic.creator.id,
'username': topic.creator.username,
'nickname': topic.creator.nickname or topic.creator.username
}
})
return jsonify({
'success': True,
'data': topics_data,
'pagination': {
'page': page,
'per_page': per_page,
'total': pagination.total,
'pages': pagination.pages,
'has_next': pagination.has_next,
'has_prev': pagination.has_prev
}
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/prediction/topics/<int:topic_id>', methods=['GET'])
def get_prediction_topic_detail(topic_id):
"""获取预测话题详情"""
try:
topic = PredictionTopic.query.get_or_404(topic_id)
# 增加浏览量
topic.views_count += 1
db.session.commit()
# 计算市场倾向
total_shares = topic.yes_total_shares + topic.no_total_shares
yes_prob = (topic.yes_total_shares / total_shares * 100) if total_shares > 0 else 50.0
# 获取 TOP 5 持仓YES 和 NO 各5个
yes_top_positions = PredictionPosition.query.filter_by(
topic_id=topic_id,
direction='yes'
).order_by(desc(PredictionPosition.shares)).limit(5).all()
no_top_positions = PredictionPosition.query.filter_by(
topic_id=topic_id,
direction='no'
).order_by(desc(PredictionPosition.shares)).limit(5).all()
def format_position(position):
return {
'user': {
'id': position.user.id,
'username': position.user.username,
'nickname': position.user.nickname or position.user.username,
'avatar_url': position.user.avatar_url
},
'shares': position.shares,
'avg_cost': float(position.avg_cost),
'total_invested': float(position.total_invested),
'is_lord': (topic.yes_lord_id == position.user_id and position.direction == 'yes') or
(topic.no_lord_id == position.user_id and position.direction == 'no')
}
return jsonify({
'success': True,
'data': {
'id': topic.id,
'title': topic.title,
'description': topic.description,
'category': topic.category,
'status': topic.status,
'result': topic.result,
'yes_price': float(topic.yes_price),
'no_price': float(topic.no_price),
'yes_total_shares': topic.yes_total_shares,
'no_total_shares': topic.no_total_shares,
'yes_probability': round(yes_prob, 1),
'no_probability': round(100 - yes_prob, 1),
'total_pool': float(topic.total_pool),
'yes_lord': {
'id': topic.yes_lord.id,
'username': topic.yes_lord.username,
'nickname': topic.yes_lord.nickname or topic.yes_lord.username,
'avatar_url': topic.yes_lord.avatar_url
} if topic.yes_lord else None,
'no_lord': {
'id': topic.no_lord.id,
'username': topic.no_lord.username,
'nickname': topic.no_lord.nickname or topic.no_lord.username,
'avatar_url': topic.no_lord.avatar_url
} if topic.no_lord else None,
'yes_top_positions': [format_position(p) for p in yes_top_positions],
'no_top_positions': [format_position(p) for p in no_top_positions],
'deadline': topic.deadline.isoformat(),
'settled_at': topic.settled_at.isoformat() if topic.settled_at else None,
'created_at': topic.created_at.isoformat(),
'views_count': topic.views_count,
'comments_count': topic.comments_count,
'participants_count': topic.participants_count,
'creator': {
'id': topic.creator.id,
'username': topic.creator.username,
'nickname': topic.creator.nickname or topic.creator.username,
'avatar_url': topic.creator.avatar_url
}
}
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/prediction/topics/<int:topic_id>/settle', methods=['POST'])
@login_required
def settle_prediction_topic(topic_id):
"""结算预测话题(仅创建者可操作)"""
try:
topic = PredictionTopic.query.get_or_404(topic_id)
# 验证权限
if topic.creator_id != current_user.id:
return jsonify({'success': False, 'error': '只有创建者可以结算'}), 403
# 验证状态
if topic.status != 'active':
return jsonify({'success': False, 'error': '话题已结算或已取消'}), 400
# 验证截止时间
if beijing_now() < topic.deadline:
return jsonify({'success': False, 'error': '未到截止时间'}), 400
# 获取结算结果
data = request.get_json()
result = data.get('result') # 'yes', 'no', 'draw'
if result not in ['yes', 'no', 'draw']:
return jsonify({'success': False, 'error': '无效的结算结果'}), 400
# 更新话题状态
topic.status = 'settled'
topic.result = result
topic.settled_at = beijing_now()
# 获取获胜方的所有持仓
if result == 'draw':
# 平局:所有人按投入比例分配奖池
all_positions = PredictionPosition.query.filter_by(topic_id=topic_id).all()
total_invested = sum(p.total_invested for p in all_positions)
for position in all_positions:
if total_invested > 0:
share_ratio = position.total_invested / total_invested
prize = topic.total_pool * share_ratio
# 发放奖金
account = UserCreditAccount.query.filter_by(user_id=position.user_id).first()
if account:
account.balance += prize
account.total_earned += prize
# 记录交易
transaction = CreditTransaction(
user_id=position.user_id,
transaction_type='settle_win',
amount=prize,
balance_after=account.balance,
related_topic_id=topic_id,
description=f'预测平局,获得奖池分红:{topic.title}'
)
db.session.add(transaction)
else:
# YES 或 NO 获胜
winning_direction = result
winning_positions = PredictionPosition.query.filter_by(
topic_id=topic_id,
direction=winning_direction
).all()
if winning_positions:
total_winning_shares = sum(p.shares for p in winning_positions)
for position in winning_positions:
# 按份额比例分配奖池
share_ratio = position.shares / total_winning_shares
prize = topic.total_pool * share_ratio
# 发放奖金
account = UserCreditAccount.query.filter_by(user_id=position.user_id).first()
if account:
account.balance += prize
account.total_earned += prize
# 记录交易
transaction = CreditTransaction(
user_id=position.user_id,
transaction_type='settle_win',
amount=prize,
balance_after=account.balance,
related_topic_id=topic_id,
description=f'预测正确,获得奖金:{topic.title}'
)
db.session.add(transaction)
db.session.commit()
return jsonify({
'success': True,
'message': f'话题已结算,结果为:{result}',
'data': {
'topic_id': topic.id,
'result': result,
'total_pool': float(topic.total_pool),
'settled_at': topic.settled_at.isoformat()
}
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 500
# --- 交易 API ---
@app.route('/api/prediction/trade/buy', methods=['POST'])
@login_required
def buy_prediction_shares():
"""买入预测份额"""
try:
data = request.get_json()
topic_id = data.get('topic_id')
direction = data.get('direction') # 'yes' or 'no'
shares = data.get('shares', 0)
# 验证参数
if not topic_id or direction not in ['yes', 'no'] or shares <= 0:
return jsonify({'success': False, 'error': '参数错误'}), 400
if shares > 1000:
return jsonify({'success': False, 'error': '单次最多买入1000份额'}), 400
# 获取话题
topic = PredictionTopic.query.get_or_404(topic_id)
if topic.status != 'active':
return jsonify({'success': False, 'error': '话题已结算或已取消'}), 400
if beijing_now() >= topic.deadline:
return jsonify({'success': False, 'error': '话题已截止'}), 400
# 获取积分账户
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
if not account:
account = UserCreditAccount(user_id=current_user.id)
db.session.add(account)
db.session.flush()
# 计算价格
current_price = topic.yes_price if direction == 'yes' else topic.no_price
# 简化的AMM定价price = (对应方份额 / 总份额) * 1000
total_shares = topic.yes_total_shares + topic.no_total_shares
if total_shares > 0:
if direction == 'yes':
current_price = (topic.yes_total_shares / total_shares) * 1000
else:
current_price = (topic.no_total_shares / total_shares) * 1000
else:
current_price = 500.0 # 初始价格
# 买入后价格会上涨,使用平均价格
after_total = total_shares + shares
if direction == 'yes':
after_yes_shares = topic.yes_total_shares + shares
after_price = (after_yes_shares / after_total) * 1000
else:
after_no_shares = topic.no_total_shares + shares
after_price = (after_no_shares / after_total) * 1000
avg_price = (current_price + after_price) / 2
# 计算费用
amount = avg_price * shares
tax = amount * 0.02 # 2% 手续费
total_cost = amount + tax
# 检查余额
if account.balance < total_cost:
return jsonify({'success': False, 'error': '积分不足'}), 400
# 扣除费用
account.balance -= total_cost
account.total_spent += total_cost
# 更新话题数据
if direction == 'yes':
topic.yes_total_shares += shares
topic.yes_price = after_price
else:
topic.no_total_shares += shares
topic.no_price = after_price
topic.total_pool += tax # 手续费进入奖池
# 更新或创建持仓
position = PredictionPosition.query.filter_by(
user_id=current_user.id,
topic_id=topic_id,
direction=direction
).first()
if position:
# 更新平均成本
old_cost = position.avg_cost * position.shares
new_cost = avg_price * shares
position.shares += shares
position.avg_cost = (old_cost + new_cost) / position.shares
position.total_invested += total_cost
else:
position = PredictionPosition(
user_id=current_user.id,
topic_id=topic_id,
direction=direction,
shares=shares,
avg_cost=avg_price,
total_invested=total_cost
)
db.session.add(position)
topic.participants_count += 1
# 更新领主
if direction == 'yes':
# 找到YES方持仓最多的用户
top_yes = db.session.query(PredictionPosition).filter_by(
topic_id=topic_id,
direction='yes'
).order_by(desc(PredictionPosition.shares)).first()
if top_yes:
topic.yes_lord_id = top_yes.user_id
else:
# 找到NO方持仓最多的用户
top_no = db.session.query(PredictionPosition).filter_by(
topic_id=topic_id,
direction='no'
).order_by(desc(PredictionPosition.shares)).first()
if top_no:
topic.no_lord_id = top_no.user_id
# 记录交易
transaction = PredictionTransaction(
user_id=current_user.id,
topic_id=topic_id,
transaction_type='buy',
direction=direction,
shares=shares,
price=avg_price,
amount=amount,
tax=tax,
total_cost=total_cost
)
db.session.add(transaction)
# 记录积分交易
credit_transaction = CreditTransaction(
user_id=current_user.id,
transaction_type='prediction_buy',
amount=-total_cost,
balance_after=account.balance,
related_topic_id=topic_id,
related_transaction_id=transaction.id,
description=f'买入 {direction.upper()} 份额:{topic.title}'
)
db.session.add(credit_transaction)
db.session.commit()
return jsonify({
'success': True,
'message': '买入成功',
'data': {
'transaction_id': transaction.id,
'shares': shares,
'price': round(avg_price, 2),
'total_cost': round(total_cost, 2),
'tax': round(tax, 2),
'new_balance': float(account.balance),
'new_position': {
'shares': position.shares,
'avg_cost': float(position.avg_cost)
}
}
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/prediction/positions', methods=['GET'])
@login_required
def get_user_positions():
"""获取用户的所有持仓"""
try:
positions = PredictionPosition.query.filter_by(user_id=current_user.id).all()
positions_data = []
for position in positions:
topic = position.topic
# 计算当前市值(如果话题还在进行中)
current_value = 0
profit = 0
profit_rate = 0
if topic.status == 'active':
current_price = topic.yes_price if position.direction == 'yes' else topic.no_price
current_value = current_price * position.shares
profit = current_value - position.total_invested
profit_rate = (profit / position.total_invested * 100) if position.total_invested > 0 else 0
positions_data.append({
'id': position.id,
'topic': {
'id': topic.id,
'title': topic.title,
'status': topic.status,
'result': topic.result,
'deadline': topic.deadline.isoformat()
},
'direction': position.direction,
'shares': position.shares,
'avg_cost': float(position.avg_cost),
'total_invested': float(position.total_invested),
'current_value': round(current_value, 2),
'profit': round(profit, 2),
'profit_rate': round(profit_rate, 2),
'created_at': position.created_at.isoformat(),
'is_lord': (topic.yes_lord_id == current_user.id and position.direction == 'yes') or
(topic.no_lord_id == current_user.id and position.direction == 'no')
})
return jsonify({
'success': True,
'data': positions_data,
'count': len(positions_data)
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
# --- 评论 API ---
@app.route('/api/prediction/topics/<int:topic_id>/comments', methods=['POST'])
@login_required
def create_topic_comment(topic_id):
"""发表话题评论"""
try:
topic = PredictionTopic.query.get_or_404(topic_id)
data = request.get_json()
content = data.get('content', '').strip()
parent_id = data.get('parent_id')
if not content or len(content) < 2:
return jsonify({'success': False, 'error': '评论内容至少2个字符'}), 400
# 创建评论
comment = TopicComment(
topic_id=topic_id,
user_id=current_user.id,
content=content,
parent_id=parent_id
)
# 如果是领主评论,自动置顶
is_lord = (topic.yes_lord_id == current_user.id) or (topic.no_lord_id == current_user.id)
if is_lord:
comment.is_pinned = True
db.session.add(comment)
# 更新话题评论数
topic.comments_count += 1
db.session.commit()
return jsonify({
'success': True,
'message': '评论成功',
'data': {
'comment_id': comment.id,
'content': comment.content,
'is_pinned': comment.is_pinned,
'created_at': comment.created_at.isoformat()
}
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/prediction/topics/<int:topic_id>/comments', methods=['GET'])
def get_topic_comments(topic_id):
"""获取话题评论列表"""
try:
topic = PredictionTopic.query.get_or_404(topic_id)
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
# 置顶评论在前,然后按时间倒序
query = TopicComment.query.filter_by(
topic_id=topic_id,
status='active',
parent_id=None # 只获取顶级评论
).order_by(
desc(TopicComment.is_pinned),
desc(TopicComment.created_at)
)
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
comments = pagination.items
def format_comment(comment):
# 获取回复
replies = TopicComment.query.filter_by(
parent_id=comment.id,
status='active'
).order_by(TopicComment.created_at).limit(5).all()
return {
'id': comment.id,
'content': comment.content,
'is_pinned': comment.is_pinned,
'likes_count': comment.likes_count,
'created_at': comment.created_at.isoformat(),
'user': {
'id': comment.user.id,
'username': comment.user.username,
'nickname': comment.user.nickname or comment.user.username,
'avatar_url': comment.user.avatar_url
},
'is_lord': (topic.yes_lord_id == comment.user_id) or (topic.no_lord_id == comment.user_id),
'replies': [{
'id': reply.id,
'content': reply.content,
'created_at': reply.created_at.isoformat(),
'user': {
'id': reply.user.id,
'username': reply.user.username,
'nickname': reply.user.nickname or reply.user.username,
'avatar_url': reply.user.avatar_url
}
} for reply in replies]
}
comments_data = [format_comment(comment) for comment in comments]
return jsonify({
'success': True,
'data': comments_data,
'pagination': {
'page': page,
'per_page': per_page,
'total': pagination.total,
'pages': pagination.pages
}
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/prediction/comments/<int:comment_id>/like', methods=['POST'])
@login_required
def like_topic_comment(comment_id):
"""点赞/取消点赞评论"""
try:
comment = TopicComment.query.get_or_404(comment_id)
# 检查是否已点赞
existing_like = TopicCommentLike.query.filter_by(
comment_id=comment_id,
user_id=current_user.id
).first()
if existing_like:
# 取消点赞
db.session.delete(existing_like)
comment.likes_count = max(0, comment.likes_count - 1)
action = 'unliked'
else:
# 点赞
like = TopicCommentLike(
comment_id=comment_id,
user_id=current_user.id
)
db.session.add(like)
comment.likes_count += 1
action = 'liked'
db.session.commit()
return jsonify({
'success': True,
'action': action,
'likes_count': comment.likes_count
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 500