""" 预测市场 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/', 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//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//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//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//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//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//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//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//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//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