diff --git a/app.py b/app.py index dc0e112c..9bc770d2 100755 --- a/app.py +++ b/app.py @@ -4672,6 +4672,13 @@ class TopicComment(db.Model): # 统计 likes_count = db.Column(db.Integer, default=0, nullable=False) + # 观点IPO 相关 + total_investment = db.Column(db.Integer, default=0, nullable=False) # 总投资额 + investor_count = db.Column(db.Integer, default=0, nullable=False) # 投资人数 + is_verified = db.Column(db.Boolean, default=False, nullable=False) # 是否已验证 + verification_result = db.Column(db.String(20)) # 验证结果:correct/incorrect/null + position_rank = db.Column(db.Integer) # 评论位置排名(用于首发权拍卖) + # 时间 created_at = db.Column(db.DateTime, default=beijing_now, nullable=False) updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now) @@ -4704,6 +4711,148 @@ class TopicCommentLike(db.Model): return f'' +class CommentInvestment(db.Model): + """评论投资记录(观点IPO)""" + __tablename__ = 'comment_investment' + + id = db.Column(db.Integer, primary_key=True) + comment_id = db.Column(db.Integer, db.ForeignKey('topic_comment.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + # 投资数据 + shares = db.Column(db.Integer, nullable=False) # 投资份额 + amount = db.Column(db.Integer, nullable=False) # 投资金额 + avg_price = db.Column(db.Float, nullable=False) # 平均价格 + + # 状态 + status = db.Column(db.String(20), default='active', nullable=False) # active/settled + + # 时间 + created_at = db.Column(db.DateTime, default=beijing_now, nullable=False) + + # 关系 + user = db.relationship('User', backref='comment_investments') + comment = db.relationship('TopicComment', backref='investments') + + def __repr__(self): + return f'' + + +class CommentPositionBid(db.Model): + """评论位置竞拍记录(首发权拍卖)""" + __tablename__ = 'comment_position_bid' + + id = db.Column(db.Integer, primary_key=True) + topic_id = db.Column(db.Integer, db.ForeignKey('prediction_topic.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + # 竞拍数据 + position = db.Column(db.Integer, nullable=False) # 位置:1/2/3 + bid_amount = db.Column(db.Integer, nullable=False) # 出价金额 + status = db.Column(db.String(20), default='pending', nullable=False) # pending/won/lost + + # 时间 + created_at = db.Column(db.DateTime, default=beijing_now, nullable=False) + expires_at = db.Column(db.DateTime, nullable=False) # 竞拍截止时间 + + # 关系 + user = db.relationship('User', backref='comment_position_bids') + topic = db.relationship('PredictionTopic', backref='position_bids') + + def __repr__(self): + return f'' + + +class TimeCapsuleTopic(db.Model): + """时间胶囊话题(长期预测)""" + __tablename__ = 'time_capsule_topic' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + # 话题内容 + title = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text) + encrypted_content = db.Column(db.Text) # 加密的预测内容 + encryption_key = db.Column(db.String(500)) # 加密密钥(后端存储) + + # 时间范围 + start_year = db.Column(db.Integer, nullable=False) # 起始年份 + end_year = db.Column(db.Integer, nullable=False) # 结束年份 + + # 状态 + status = db.Column(db.String(20), default='active', nullable=False) # active/settled + is_decrypted = db.Column(db.Boolean, default=False, nullable=False) # 是否已解密 + actual_happened_year = db.Column(db.Integer) # 实际发生年份 + + # 统计 + total_pool = db.Column(db.Integer, default=0, nullable=False) # 总奖池 + + # 时间 + created_at = db.Column(db.DateTime, default=beijing_now, nullable=False) + updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now) + + # 关系 + user = db.relationship('User', backref='time_capsule_topics') + time_slots = db.relationship('TimeCapsuleTimeSlot', backref='topic', lazy='dynamic') + + def __repr__(self): + return f'' + + +class TimeCapsuleTimeSlot(db.Model): + """时间胶囊时间段""" + __tablename__ = 'time_capsule_time_slot' + + id = db.Column(db.Integer, primary_key=True) + topic_id = db.Column(db.Integer, db.ForeignKey('time_capsule_topic.id'), nullable=False) + + # 时间段 + year_start = db.Column(db.Integer, nullable=False) + year_end = db.Column(db.Integer, nullable=False) + + # 竞拍数据 + current_holder_id = db.Column(db.Integer, db.ForeignKey('user.id')) # 当前持有者 + current_price = db.Column(db.Integer, default=100, nullable=False) # 当前价格 + total_bids = db.Column(db.Integer, default=0, nullable=False) # 总竞拍次数 + + # 状态 + status = db.Column(db.String(20), default='active', nullable=False) # active/won/expired + + # 时间 + created_at = db.Column(db.DateTime, default=beijing_now, nullable=False) + updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now) + + # 关系 + current_holder = db.relationship('User', foreign_keys=[current_holder_id]) + bids = db.relationship('TimeSlotBid', backref='time_slot', lazy='dynamic') + + def __repr__(self): + return f'' + + +class TimeSlotBid(db.Model): + """时间段竞拍记录""" + __tablename__ = 'time_slot_bid' + + id = db.Column(db.Integer, primary_key=True) + slot_id = db.Column(db.Integer, db.ForeignKey('time_capsule_time_slot.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + # 竞拍数据 + bid_amount = db.Column(db.Integer, nullable=False) + status = db.Column(db.String(20), default='outbid', nullable=False) # outbid/holding/won + + # 时间 + created_at = db.Column(db.DateTime, default=beijing_now, nullable=False) + + # 关系 + user = db.relationship('User', backref='time_slot_bids') + + def __repr__(self): + return f'' + + class Event(db.Model): """事件模型""" id = db.Column(db.Integer, primary_key=True) @@ -14056,6 +14205,735 @@ def like_topic_comment(comment_id): return jsonify({'success': False, 'error': str(e)}), 500 +# ==================== 观点IPO API ==================== + +@app.route('/api/prediction/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 + + # 计算投资金额(简化:每份100积分基础价格 + 已有投资额/10) + 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, + 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 + + +@app.route('/api/prediction/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 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 + + +@app.route('/api/prediction/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, + 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, + 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 + + +@app.route('/api/prediction/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 += 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 -= 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, + 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 + } + }) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/prediction/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 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 + + +# ==================== 时间胶囊 API ==================== + +@app.route('/api/time-capsule/topics', methods=['POST']) +@login_required +def create_time_capsule_topic(): + """创建时间胶囊话题""" + try: + data = request.json + title = data.get('title') + description = data.get('description', '') + encrypted_content = data.get('encrypted_content') + encryption_key = data.get('encryption_key') + start_year = data.get('start_year') + end_year = data.get('end_year') + + # 验证 + if not title or not encrypted_content or not encryption_key: + return jsonify({'success': False, 'error': '缺少必要参数'}), 400 + + if not start_year or not end_year or end_year <= start_year: + 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 + + # 扣减积分 + account.balance -= 100 + + # 创建话题 + topic = TimeCapsuleTopic( + user_id=current_user.id, + title=title, + description=description, + encrypted_content=encrypted_content, + encryption_key=encryption_key, + start_year=start_year, + end_year=end_year, + total_pool=100 # 创建费用进入奖池 + ) + db.session.add(topic) + db.session.flush() # 获取 topic.id + + # 自动创建时间段(每年一个时间段) + for year in range(start_year, end_year + 1): + slot = TimeCapsuleTimeSlot( + topic_id=topic.id, + year_start=year, + year_end=year + ) + db.session.add(slot) + + # 记录积分交易 + transaction = CreditTransaction( + user_id=current_user.id, + type='time_capsule_create', + amount=-100, + balance_after=account.balance, + description=f'创建时间胶囊话题 #{topic.id}' + ) + db.session.add(transaction) + + db.session.commit() + + return jsonify({ + 'success': True, + 'data': { + 'topic_id': topic.id, + 'title': topic.title, + 'new_balance': account.balance + } + }) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/time-capsule/topics', methods=['GET']) +def get_time_capsule_topics(): + """获取时间胶囊话题列表""" + try: + status = request.args.get('status', 'active') + + query = TimeCapsuleTopic.query.filter_by(status=status) + topics = query.order_by(TimeCapsuleTopic.created_at.desc()).all() + + result = [] + for topic in topics: + # 获取用户信息 + user = User.query.get(topic.user_id) + + # 获取时间段统计 + slots = TimeCapsuleTimeSlot.query.filter_by(topic_id=topic.id).all() + total_slots = len(slots) + active_slots = len([s for s in slots if s.status == 'active']) + + result.append({ + 'id': topic.id, + 'title': topic.title, + 'description': topic.description, + 'start_year': topic.start_year, + 'end_year': topic.end_year, + 'total_pool': topic.total_pool, + 'total_slots': total_slots, + 'active_slots': active_slots, + 'is_decrypted': topic.is_decrypted, + 'status': topic.status, + 'author_id': topic.user_id, + 'author_name': user.username if user else '未知用户', + 'author_avatar': user.avatar if user else None, + 'created_at': topic.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 + + +@app.route('/api/time-capsule/topics/', methods=['GET']) +def get_time_capsule_topic(topic_id): + """获取时间胶囊话题详情""" + try: + topic = TimeCapsuleTopic.query.get_or_404(topic_id) + user = User.query.get(topic.user_id) + + # 获取所有时间段 + slots = TimeCapsuleTimeSlot.query.filter_by(topic_id=topic_id).order_by(TimeCapsuleTimeSlot.year_start).all() + + slots_data = [] + for slot in slots: + holder = User.query.get(slot.current_holder_id) if slot.current_holder_id else None + + slots_data.append({ + 'id': slot.id, + 'year_start': slot.year_start, + 'year_end': slot.year_end, + 'current_price': slot.current_price, + 'total_bids': slot.total_bids, + 'status': slot.status, + 'current_holder_id': slot.current_holder_id, + 'current_holder_name': holder.username if holder else None, + 'current_holder_avatar': holder.avatar if holder else None + }) + + result = { + 'id': topic.id, + 'title': topic.title, + 'description': topic.description, + 'start_year': topic.start_year, + 'end_year': topic.end_year, + 'total_pool': topic.total_pool, + 'is_decrypted': topic.is_decrypted, + 'decrypted_content': topic.encrypted_content if topic.is_decrypted else None, + 'actual_happened_year': topic.actual_happened_year, + 'status': topic.status, + 'author_id': topic.user_id, + 'author_name': user.username if user else '未知用户', + 'author_avatar': user.avatar if user else None, + 'time_slots': slots_data, + 'created_at': topic.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 + + +@app.route('/api/time-capsule/slots//bid', methods=['POST']) +@login_required +def bid_time_slot(slot_id): + """竞拍时间段""" + try: + data = request.json + bid_amount = data.get('bid_amount') + + slot = TimeCapsuleTimeSlot.query.get_or_404(slot_id) + + # 检查时间段是否还在竞拍 + if slot.status != 'active': + return jsonify({'success': False, 'error': '该时间段已结束竞拍'}), 400 + + # 检查出价是否高于当前价格 + min_bid = slot.current_price + 50 # 至少比当前价格高50积分 + if bid_amount < min_bid: + return jsonify({ + 'success': False, + 'error': f'出价必须至少为 {min_bid} 积分' + }), 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 + + # 扣减积分 + account.balance -= bid_amount + + # 如果有前任持有者,退还积分 + if slot.current_holder_id: + prev_holder_account = UserCreditAccount.query.filter_by(user_id=slot.current_holder_id).first() + if prev_holder_account: + prev_holder_account.balance += slot.current_price + + # 更新前任的竞拍记录状态 + prev_bid = TimeSlotBid.query.filter_by( + slot_id=slot_id, + user_id=slot.current_holder_id, + status='holding' + ).first() + if prev_bid: + prev_bid.status = 'outbid' + + # 创建竞拍记录 + bid = TimeSlotBid( + slot_id=slot_id, + user_id=current_user.id, + bid_amount=bid_amount, + status='holding' + ) + db.session.add(bid) + + # 更新时间段 + slot.current_holder_id = current_user.id + slot.current_price = bid_amount + slot.total_bids += 1 + + # 更新话题奖池 + topic = TimeCapsuleTopic.query.get(slot.topic_id) + price_increase = bid_amount - (slot.current_price if slot.current_holder_id else 100) + topic.total_pool += price_increase + + # 记录积分交易 + transaction = CreditTransaction( + user_id=current_user.id, + type='time_slot_bid', + amount=-bid_amount, + balance_after=account.balance, + description=f'竞拍时间段 {slot.year_start}-{slot.year_end}' + ) + db.session.add(transaction) + + db.session.commit() + + return jsonify({ + 'success': True, + 'data': { + 'slot_id': slot_id, + 'bid_amount': bid_amount, + 'new_balance': account.balance, + 'total_pool': topic.total_pool + } + }) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/time-capsule/topics//decrypt', methods=['POST']) +@login_required +def decrypt_time_capsule(topic_id): + """解密时间胶囊(管理员或作者)""" + try: + topic = TimeCapsuleTopic.query.get_or_404(topic_id) + + # 检查权限(管理员或作者) + if current_user.id != 1 and current_user.id != topic.user_id: + return jsonify({'success': False, 'error': '无权限操作'}), 403 + + # 检查是否已解密 + if topic.is_decrypted: + return jsonify({'success': False, 'error': '该话题已解密'}), 400 + + # 解密(前端会用密钥解密内容) + topic.is_decrypted = True + + db.session.commit() + + return jsonify({ + 'success': True, + 'data': { + 'encrypted_content': topic.encrypted_content, + 'encryption_key': topic.encryption_key + } + }) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/time-capsule/topics//settle', methods=['POST']) +@login_required +def settle_time_capsule(topic_id): + """结算时间胶囊话题""" + try: + # 检查管理员权限 + if current_user.id != 1: + return jsonify({'success': False, 'error': '无权限操作'}), 403 + + data = request.json + happened_year = data.get('happened_year') + + topic = TimeCapsuleTopic.query.get_or_404(topic_id) + + # 检查是否已结算 + if topic.status == 'settled': + return jsonify({'success': False, 'error': '该话题已结算'}), 400 + + # 更新话题状态 + topic.status = 'settled' + topic.actual_happened_year = happened_year + + # 找到中奖的时间段 + winning_slot = TimeCapsuleTimeSlot.query.filter_by( + topic_id=topic_id, + year_start=happened_year + ).first() + + if winning_slot and winning_slot.current_holder_id: + # 中奖者获得全部奖池 + winner_account = UserCreditAccount.query.filter_by(user_id=winning_slot.current_holder_id).first() + if winner_account: + winner_account.balance += topic.total_pool + + # 记录积分交易 + transaction = CreditTransaction( + user_id=winning_slot.current_holder_id, + type='time_capsule_win', + amount=topic.total_pool, + balance_after=winner_account.balance, + description=f'时间胶囊中奖 #{topic_id}' + ) + db.session.add(transaction) + + # 更新竞拍记录 + winning_bid = TimeSlotBid.query.filter_by( + slot_id=winning_slot.id, + user_id=winning_slot.current_holder_id, + status='holding' + ).first() + if winning_bid: + winning_bid.status = 'won' + + # 更新时间段状态 + winning_slot.status = 'won' + + # 其他时间段设为过期 + other_slots = TimeCapsuleTimeSlot.query.filter( + TimeCapsuleTimeSlot.topic_id == topic_id, + TimeCapsuleTimeSlot.id != (winning_slot.id if winning_slot else None) + ).all() + + for slot in other_slots: + slot.status = 'expired' + + db.session.commit() + + return jsonify({ + 'success': True, + 'data': { + 'topic_id': topic_id, + 'happened_year': happened_year, + 'winner_id': winning_slot.current_holder_id if winning_slot else None, + 'prize': topic.total_pool + } + }) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'error': str(e)}), 500 + + if __name__ == '__main__': # 创建数据库表 with app.app_context(): diff --git a/src/views/ValueForum/components/PredictionGuideModal.js b/src/views/ValueForum/components/PredictionGuideModal.js index 68b91260..2948a67a 100644 --- a/src/views/ValueForum/components/PredictionGuideModal.js +++ b/src/views/ValueForum/components/PredictionGuideModal.js @@ -54,6 +54,10 @@ import { Wallet, RefreshCw, Gift, + MessageCircle, + Lock, + Lightbulb, + Calendar, } from 'lucide-react'; import { motion } from 'framer-motion'; import { forumColors } from '@theme/forumTheme'; @@ -290,6 +294,24 @@ const PredictionGuideModal = ({ isOpen, onClose }) => { 示例演示 + + + 观点IPO + + + + 时间胶囊 + @@ -681,6 +703,512 @@ const PredictionGuideModal = ({ isOpen, onClose }) => { + + {/* 观点IPO */} + + + + + + + + 观点IPO - 评论即资产 + + + 让有价值的观点直接变现,让小白通过投资"懂王"获利 + + + + + + {/* 核心玩法 */} + + + + + 💡 核心机制 + + + + + + 1. 发表分析 → 他人投资 + + + 在预测话题下发表长篇分析(如"为什么贵州茅台会涨"),其他用户如果认同你的观点,可以对你的评论进行投资(注资)。 + + + + + + 2. 预测正确 → 股东分红 + + + 如果你的预测正确(由管理员验证),该评论获得的所有投资将按比例分给"股东"(投资者)。投资者获得1.5倍收益,评论作者获得20%奖励。 + + + + + + 3. 首发权拍卖 → 流量变现 + + + 在重大事件(如美联储议息)前,可以竞拍"前三楼"评论位置。前排位置流量最大,大V可以通过竞价获得曝光,吸引追随者投资。 + + + + + + {/* 投资定价 */} + + + + + 💰 投资定价规则 + + + + + + 基础价格:100积分/份 + + + + 动态涨价:每获得投资,价格按已有投资额的10%上涨 + + + + 早期投资者成本低,后期投资者成本高 + + + + 不能投资自己的评论 + + + + + {/* 收益分配 */} + + + + + 🎯 收益分配机制 + + + + + + + 预测正确: + 投资者获得总投资额的1.5倍收益,按份额比例分配 + + + + + + 作者奖励: + 评论作者获得总投资额的20%作为奖励 + + + + + + 预测错误: + 投资者损失全部投资,作者无奖励 + + + + + + {/* 首发权拍卖 */} + + + + + 👑 首发权拍卖 + + + + + 玩法: + 重大事件发布前,竞拍评论区前三名位置 + + + 价值: + 前排评论获得最大曝光,吸引更多投资者 + + + 规则: + 最低出价500积分,出价最高者获得位置 + + + 结算: + 竞拍截止后,获胜者积分扣除,其他人退款 + + + + + {/* 示例 */} + + + + + 📖 完整示例 + + + + + 1. 大V"股神小王"在话题"贵州茅台会涨吗?"下发表3000字深度分析 + + + 2. 小明看好这个分析,投资500积分(5份) + + + 3. 小红也跟投300积分(3份),此时价格已上涨到150积分/份 + + + 4. 最终茅台真的上涨,管理员验证"预测正确" + + + 5. 收益分配: + + + • 小明获得:(5/8) × 1200积分 = 750积分,净收益+250积分 + + + • 小红获得:(3/8) × 1200积分 = 450积分,净收益+150积分 + + + • 股神小王获得:800积分 × 20% = 160积分作者奖励 + + + + + + + {/* 时间胶囊 */} + + + + + + + + 时间胶囊 - 长线预测的博弈 + + + 针对长期事件(如"人类何时登陆火星"),竞拍时间段,赌事件发生的年份 + + + + + + {/* 核心机制 */} + + + + + 💡 核心机制 + + + + + + 1. 创建话题 + 加密预测 + + + 发起一个长期预测话题(如"人类何时登陆火星:2025-2050年"),并提交加密的预测内容。需支付100积分创建费用,内容在解密前完全保密。 + + + + + + 2. 时间段切分 + 竞拍 + + + 系统自动将时间范围切分为多个年份时间段(如2025年、2026年...2050年)。用户可以竞拍任意时间段,最高出价者成为该时间段的持有者。 + + + + + + 3. 价格动态变化 + + + 随着时间推移,如果事件没有发生:临近过期的时间段价格会暴跌(如2025年快到了还没发生),而未来的时间段价格会通过竞拍飙升(概率提升)。 + + + + + + 4. 事件发生 + 结算 + + + 当事件真的发生时(如2035年人类登陆火星),管理员解密并结算。持有"2035年"时间段的用户获得全部奖池,其他时间段持有者失去投资。 + + + + + + {/* 加密机制 */} + + + + + 🔐 加密机制 + + + + + + + 前端AES加密: + 提交时在浏览器端加密预测内容 + + + + + + 后端存储密钥: + 加密密钥存储在数据库中,解密前不公开 + + + + + + 防止马后炮: + 证明"我早就说过",无法事后修改预测 + + + + + + 管理员解密: + 事件发生后,管理员或作者解密查看内容 + + + + + + {/* 竞拍规则 */} + + + + + 💰 竞拍规则 + + + + + + 初始价格:每个时间段初始价格100积分 + + + + 最低加价:每次出价至少比当前价格高50积分 + + + + 自动退款:被超越的出价者自动退还积分 + + + + 奖池累积:每次竞拍的价格增量进入话题奖池 + + + + 创建费用:100积分进入奖池 + + + + + {/* 示例 */} + + + + + 📖 完整示例:人类登陆火星 + + + + + 2025年1月: + 小明创建话题"人类何时登陆火星:2025-2050年",支付100积分,提交加密预测 + + + 初始状态: + 系统创建26个时间段(2025-2050年),每个初始价格100积分 + + + 2025年2月: + 小红出价150积分竞拍"2030年"时间段,成为持有者 + + + 2025年3月: + 小李出价200积分超越小红,成为"2030年"新持有者(小红退还150积分) + + + 2026年: + "2025年"时间段过期无人竞拍(事件未发生),价值归零 + + + 2030年: + SpaceX宣布人类成功登陆火星! + + + 结算: + + + • 小李(持有"2030年")获得全部奖池(假设1500积分) + + + • 其他时间段持有者损失投资 + + + • 话题解密,小明的预测内容公开 + + + + + {/* 策略提示 */} + + + + + 🎯 策略提示 + + + + + ✅ 早期竞拍成本低,但等待时间长 + + + ✅ 可以根据科技进展动态调整持有时间段 + + + ✅ 适合极其长线的宏大叙事预测 + + + ✅ 加密机制保证预测的真实性和不可篡改性 + + + + +