Files
vf_react/prediction_api.py

1230 lines
48 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 Blueprint
从 app.py 抽离的预测市场相关接口
"""
from flask import Blueprint, request, jsonify
from flask_login import login_required, current_user
from sqlalchemy import desc
from datetime import datetime
# 创建 Blueprint
prediction_bp = Blueprint('prediction', __name__, url_prefix='/api/prediction')
def init_prediction_api(db, beijing_now):
"""
初始化预测市场 API
Args:
db: SQLAlchemy 数据库实例
beijing_now: 北京时间函数
Returns:
Blueprint: 配置好的 Blueprint
"""
# 延迟导入模型,避免循环导入
from app import (
UserCreditAccount, PredictionTopic, PredictionPosition,
PredictionTransaction, CreditTransaction, TopicComment,
TopicCommentLike, CommentInvestment, CommentPositionBid, User
)
# ==================== 积分 API ====================
@prediction_bp.route('/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
@prediction_bp.route('/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 ====================
@prediction_bp.route('/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'))
# 移除时区信息转换为naive datetime
if deadline.tzinfo is not None:
deadline = deadline.replace(tzinfo=None)
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,
total_pool=create_cost # 创建费用进入奖池
)
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
@prediction_bp.route('/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
# 处理datetime确保移除时区信息
deadline = topic.deadline
if hasattr(deadline, 'replace') and deadline.tzinfo is not None:
deadline = deadline.replace(tzinfo=None)
created_at = topic.created_at
if hasattr(created_at, 'replace') and created_at.tzinfo is not None:
created_at = created_at.replace(tzinfo=None)
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_total_shares': topic.yes_total_shares,
'no_total_shares': topic.no_total_shares,
'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': deadline.isoformat() if deadline else None,
'created_at': created_at.isoformat() if created_at else None,
'views_count': topic.views_count,
'comments_count': topic.comments_count,
'participants_count': topic.participants_count,
# 添加作者信息 - 修复发起者显示问题
'author_id': topic.creator_id,
'author_name': topic.creator.nickname or topic.creator.username if topic.creator else '未知用户',
'author_avatar': topic.creator.avatar_url if topic.creator else None,
'creator': {
'id': topic.creator.id,
'username': topic.creator.username,
'nickname': topic.creator.nickname or topic.creator.username,
'avatar_url': topic.creator.avatar_url
} if topic.creator else None
})
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:
import traceback
print(f"[ERROR] 获取话题列表失败: {str(e)}")
print(traceback.format_exc())
return jsonify({'success': False, 'error': str(e)}), 500
@prediction_bp.route('/topics/<int:topic_id>', methods=['GET'])
def get_prediction_topic_detail(topic_id):
"""获取预测话题详情"""
try:
# 刷新会话,确保获取最新数据
db.session.expire_all()
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,
# 添加作者信息 - 修复发起者显示问题
'author_id': topic.creator_id,
'author_name': topic.creator.nickname or topic.creator.username if topic.creator else '未知用户',
'author_avatar': topic.creator.avatar_url if topic.creator else None,
'creator': {
'id': topic.creator.id,
'username': topic.creator.username,
'nickname': topic.creator.nickname or topic.creator.username,
'avatar_url': topic.creator.avatar_url
} if topic.creator else None
}
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@prediction_bp.route('/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 ====================
@prediction_bp.route('/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
# 不能参与自己创建的话题
if topic.creator_id == current_user.id:
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()
# 计算价格
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':
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:
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,
trade_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
@prediction_bp.route('/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 ====================
@prediction_bp.route('/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
@prediction_bp.route('/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
@prediction_bp.route('/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
# ==================== 观点IPO API ====================
@prediction_bp.route('/comments/<int:comment_id>/invest', methods=['POST'])
@login_required
def invest_comment(comment_id):
"""投资评论观点IPO"""
try:
data = request.json
shares = data.get('shares', 1)
# 获取评论
comment = TopicComment.query.get_or_404(comment_id)
# 检查评论是否已结算
if comment.is_verified:
return jsonify({'success': False, 'error': '该评论已结算,无法继续投资'}), 400
# 检查是否是自己的评论
if comment.user_id == current_user.id:
return jsonify({'success': False, 'error': '不能投资自己的评论'}), 400
# 计算投资金额
base_price = 100
price_increase = comment.total_investment / 10 if comment.total_investment > 0 else 0
price_per_share = base_price + price_increase
amount = int(price_per_share * shares)
# 获取用户积分账户
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
if not account:
return jsonify({'success': False, 'error': '账户不存在'}), 404
# 检查余额
if account.balance < amount:
return jsonify({'success': False, 'error': '积分不足'}), 400
# 扣减积分
account.balance -= amount
# 检查是否已有投资记录
existing_investment = CommentInvestment.query.filter_by(
comment_id=comment_id,
user_id=current_user.id,
status='active'
).first()
if existing_investment:
# 更新投资记录
total_shares = existing_investment.shares + shares
total_amount = existing_investment.amount + amount
existing_investment.shares = total_shares
existing_investment.amount = total_amount
existing_investment.avg_price = total_amount / total_shares
else:
# 创建新投资记录
investment = CommentInvestment(
comment_id=comment_id,
user_id=current_user.id,
shares=shares,
amount=amount,
avg_price=price_per_share
)
db.session.add(investment)
comment.investor_count += 1
# 更新评论统计
comment.total_investment += amount
# 记录积分交易
transaction = CreditTransaction(
user_id=current_user.id,
transaction_type='comment_investment',
amount=-amount,
balance_after=account.balance,
description=f'投资评论 #{comment_id}'
)
db.session.add(transaction)
db.session.commit()
return jsonify({
'success': True,
'data': {
'shares': shares,
'amount': amount,
'price_per_share': price_per_share,
'total_investment': comment.total_investment,
'investor_count': comment.investor_count,
'new_balance': account.balance
}
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 500
@prediction_bp.route('/comments/<int:comment_id>/investments', methods=['GET'])
def get_comment_investments(comment_id):
"""获取评论的投资列表"""
try:
investments = CommentInvestment.query.filter_by(
comment_id=comment_id,
status='active'
).all()
result = []
for inv in investments:
user = User.query.get(inv.user_id)
result.append({
'id': inv.id,
'user_id': inv.user_id,
'user_name': user.username if user else '未知用户',
'user_avatar': user.avatar_url if user else None,
'shares': inv.shares,
'amount': inv.amount,
'avg_price': inv.avg_price,
'created_at': inv.created_at.strftime('%Y-%m-%d %H:%M:%S')
})
return jsonify({
'success': True,
'data': result
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@prediction_bp.route('/comments/<int:comment_id>/verify', methods=['POST'])
@login_required
def verify_comment(comment_id):
"""管理员验证评论预测结果"""
try:
# 检查管理员权限(简化版:假设 user_id=1 是管理员)
if current_user.id != 1:
return jsonify({'success': False, 'error': '无权限操作'}), 403
data = request.json
result = data.get('result') # 'correct' or 'incorrect'
if result not in ['correct', 'incorrect']:
return jsonify({'success': False, 'error': '无效的验证结果'}), 400
comment = TopicComment.query.get_or_404(comment_id)
# 检查是否已验证
if comment.is_verified:
return jsonify({'success': False, 'error': '该评论已验证'}), 400
# 更新验证状态
comment.is_verified = True
comment.verification_result = result
# 如果预测正确,进行收益分配
if result == 'correct' and comment.total_investment > 0:
# 获取所有投资记录
investments = CommentInvestment.query.filter_by(
comment_id=comment_id,
status='active'
).all()
# 计算总收益总投资额的1.5倍)
total_reward = int(comment.total_investment * 1.5)
# 按份额比例分配收益
total_shares = sum([inv.shares for inv in investments])
for inv in investments:
# 计算该投资者的收益
investor_reward = int((inv.shares / total_shares) * total_reward)
# 获取投资者账户
account = UserCreditAccount.query.filter_by(user_id=inv.user_id).first()
if account:
account.balance += investor_reward
# 记录积分交易
transaction = CreditTransaction(
user_id=inv.user_id,
transaction_type='comment_investment_profit',
amount=investor_reward,
balance_after=account.balance,
description=f'评论投资收益 #{comment_id}'
)
db.session.add(transaction)
# 更新投资状态
inv.status = 'settled'
# 评论作者也获得奖励总投资额的20%
author_reward = int(comment.total_investment * 0.2)
author_account = UserCreditAccount.query.filter_by(user_id=comment.user_id).first()
if author_account:
author_account.balance += author_reward
transaction = CreditTransaction(
user_id=comment.user_id,
transaction_type='comment_author_bonus',
amount=author_reward,
balance_after=author_account.balance,
description=f'评论作者奖励 #{comment_id}'
)
db.session.add(transaction)
db.session.commit()
return jsonify({
'success': True,
'data': {
'comment_id': comment_id,
'verification_result': result,
'total_investment': comment.total_investment
}
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 竞拍 API ====================
@prediction_bp.route('/topics/<int:topic_id>/bid-position', methods=['POST'])
@login_required
def bid_comment_position(topic_id):
"""竞拍评论位置(首发权拍卖)"""
try:
data = request.json
position = data.get('position') # 1/2/3
bid_amount = data.get('bid_amount')
if position not in [1, 2, 3]:
return jsonify({'success': False, 'error': '无效的位置'}), 400
if bid_amount < 500:
return jsonify({'success': False, 'error': '最低出价500积分'}), 400
# 获取用户积分账户
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
if not account or account.balance < bid_amount:
return jsonify({'success': False, 'error': '积分不足'}), 400
# 检查该位置的当前最高出价
current_highest = CommentPositionBid.query.filter_by(
topic_id=topic_id,
position=position,
status='pending'
).order_by(CommentPositionBid.bid_amount.desc()).first()
if current_highest and bid_amount <= current_highest.bid_amount:
return jsonify({
'success': False,
'error': f'出价必须高于当前最高价 {current_highest.bid_amount}'
}), 400
# 扣减积分(冻结)
account.balance -= bid_amount
account.frozen_balance += bid_amount
# 如果有之前的出价,退还积分
user_previous_bid = CommentPositionBid.query.filter_by(
topic_id=topic_id,
position=position,
user_id=current_user.id,
status='pending'
).first()
if user_previous_bid:
account.frozen_balance -= user_previous_bid.bid_amount
account.balance += user_previous_bid.bid_amount
user_previous_bid.status = 'lost'
# 创建竞拍记录
topic = PredictionTopic.query.get_or_404(topic_id)
bid = CommentPositionBid(
topic_id=topic_id,
user_id=current_user.id,
position=position,
bid_amount=bid_amount,
expires_at=topic.deadline
)
db.session.add(bid)
# 记录积分交易
transaction = CreditTransaction(
user_id=current_user.id,
transaction_type='position_bid',
amount=-bid_amount,
balance_after=account.balance,
description=f'竞拍评论位置 #{position} (话题#{topic_id})'
)
db.session.add(transaction)
db.session.commit()
return jsonify({
'success': True,
'data': {
'bid_id': bid.id,
'position': position,
'bid_amount': bid_amount,
'new_balance': account.balance,
'frozen': account.frozen_balance
}
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 500
@prediction_bp.route('/topics/<int:topic_id>/position-bids', methods=['GET'])
def get_position_bids(topic_id):
"""获取话题的位置竞拍列表"""
try:
result = {}
for position in [1, 2, 3]:
bids = CommentPositionBid.query.filter_by(
topic_id=topic_id,
position=position,
status='pending'
).order_by(CommentPositionBid.bid_amount.desc()).limit(5).all()
position_bids = []
for bid in bids:
user = User.query.get(bid.user_id)
position_bids.append({
'id': bid.id,
'user_id': bid.user_id,
'user_name': user.username if user else '未知用户',
'user_avatar': user.avatar_url if user else None,
'bid_amount': bid.bid_amount,
'created_at': bid.created_at.strftime('%Y-%m-%d %H:%M:%S')
})
result[f'position_{position}'] = position_bids
return jsonify({
'success': True,
'data': result
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
return prediction_bp