update pay function
This commit is contained in:
878
app.py
878
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'<TopicCommentLike comment_id={self.comment_id} user_id={self.user_id}>'
|
||||
|
||||
|
||||
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'<CommentInvestment id={self.id} comment_id={self.comment_id} user_id={self.user_id} shares={self.shares}>'
|
||||
|
||||
|
||||
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'<CommentPositionBid id={self.id} topic_id={self.topic_id} position={self.position} bid={self.bid_amount}>'
|
||||
|
||||
|
||||
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'<TimeCapsuleTopic id={self.id} title={self.title} years={self.start_year}-{self.end_year}>'
|
||||
|
||||
|
||||
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'<TimeCapsuleTimeSlot id={self.id} years={self.year_start}-{self.year_end} price={self.current_price}>'
|
||||
|
||||
|
||||
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'<TimeSlotBid id={self.id} slot_id={self.slot_id} user_id={self.user_id} bid={self.bid_amount}>'
|
||||
|
||||
|
||||
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/<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
|
||||
|
||||
# 计算投资金额(简化:每份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/<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 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/<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,
|
||||
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/<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 += 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/<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 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/<int:topic_id>', 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/<int:slot_id>/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/<int:topic_id>/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/<int:topic_id>/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():
|
||||
|
||||
@@ -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 }) => {
|
||||
<Icon as={Trophy} boxSize="16px" mr="2" />
|
||||
示例演示
|
||||
</Tab>
|
||||
<Tab
|
||||
_selected={{
|
||||
bg: forumColors.gradients.goldPrimary,
|
||||
color: forumColors.background.main,
|
||||
}}
|
||||
>
|
||||
<Icon as={MessageCircle} boxSize="16px" mr="2" />
|
||||
观点IPO
|
||||
</Tab>
|
||||
<Tab
|
||||
_selected={{
|
||||
bg: forumColors.gradients.goldPrimary,
|
||||
color: forumColors.background.main,
|
||||
}}
|
||||
>
|
||||
<Icon as={Lock} boxSize="16px" mr="2" />
|
||||
时间胶囊
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
@@ -681,6 +703,512 @@ const PredictionGuideModal = ({ isOpen, onClose }) => {
|
||||
</Box>
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
|
||||
{/* 观点IPO */}
|
||||
<TabPanel p="0">
|
||||
<VStack spacing="6" align="stretch">
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
p="6"
|
||||
borderRadius="xl"
|
||||
border="2px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
>
|
||||
<HStack spacing="3" mb="3">
|
||||
<Icon as={MessageCircle} boxSize="32px" color={forumColors.primary[500]} />
|
||||
<VStack align="start" spacing="0">
|
||||
<Text fontSize="2xl" fontWeight="700" color={forumColors.text.primary}>
|
||||
观点IPO - 评论即资产
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
让有价值的观点直接变现,让小白通过投资"懂王"获利
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
{/* 核心玩法 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2" mb="4">
|
||||
<Icon as={Lightbulb} boxSize="20px" color="yellow.400" />
|
||||
<Text fontWeight="700" color={forumColors.text.primary} fontSize="lg">
|
||||
💡 核心机制
|
||||
</Text>
|
||||
</HStack>
|
||||
<VStack spacing="3" align="stretch">
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
p="4"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
>
|
||||
<Text fontWeight="600" color={forumColors.text.primary} mb="2">
|
||||
1. 发表分析 → 他人投资
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
在预测话题下发表长篇分析(如"为什么贵州茅台会涨"),其他用户如果认同你的观点,可以对你的评论进行投资(注资)。
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="4"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<Text fontWeight="600" color={forumColors.text.primary} mb="2">
|
||||
2. 预测正确 → 股东分红
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
如果你的预测正确(由管理员验证),该评论获得的所有投资将按比例分给"股东"(投资者)。投资者获得1.5倍收益,评论作者获得20%奖励。
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="4"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<Text fontWeight="600" color={forumColors.text.primary} mb="2">
|
||||
3. 首发权拍卖 → 流量变现
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
在重大事件(如美联储议息)前,可以竞拍"前三楼"评论位置。前排位置流量最大,大V可以通过竞价获得曝光,吸引追随者投资。
|
||||
</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 投资定价 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2" mb="4">
|
||||
<Icon as={DollarSign} boxSize="20px" color="green.400" />
|
||||
<Text fontWeight="700" color={forumColors.text.primary} fontSize="lg">
|
||||
💰 投资定价规则
|
||||
</Text>
|
||||
</HStack>
|
||||
<List spacing="2" fontSize="sm" color={forumColors.text.secondary}>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
基础价格:100积分/份
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
动态涨价:每获得投资,价格按已有投资额的10%上涨
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
早期投资者成本低,后期投资者成本高
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
不能投资自己的评论
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
{/* 收益分配 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2" mb="4">
|
||||
<Icon as={Trophy} boxSize="20px" color="purple.400" />
|
||||
<Text fontWeight="700" color={forumColors.text.primary} fontSize="lg">
|
||||
🎯 收益分配机制
|
||||
</Text>
|
||||
</HStack>
|
||||
<VStack spacing="3" align="stretch">
|
||||
<HStack spacing="2">
|
||||
<Icon as={ArrowRight} boxSize="14px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
<Text as="span" fontWeight="600" color={forumColors.text.primary}>预测正确:</Text>
|
||||
投资者获得总投资额的1.5倍收益,按份额比例分配
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing="2">
|
||||
<Icon as={ArrowRight} boxSize="14px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
<Text as="span" fontWeight="600" color={forumColors.text.primary}>作者奖励:</Text>
|
||||
评论作者获得总投资额的20%作为奖励
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing="2">
|
||||
<Icon as={ArrowRight} boxSize="14px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
<Text as="span" fontWeight="600" color={forumColors.text.primary}>预测错误:</Text>
|
||||
投资者损失全部投资,作者无奖励
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 首发权拍卖 */}
|
||||
<Box
|
||||
bg="orange.50"
|
||||
border="2px solid"
|
||||
borderColor="orange.400"
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
>
|
||||
<HStack spacing="3" mb="3">
|
||||
<Icon as={Crown} boxSize="24px" color="orange.600" />
|
||||
<Text fontWeight="700" color="orange.700" fontSize="lg">
|
||||
👑 首发权拍卖
|
||||
</Text>
|
||||
</HStack>
|
||||
<VStack spacing="2" align="stretch">
|
||||
<Text fontSize="sm" color="orange.700">
|
||||
<Text as="span" fontWeight="600">玩法:</Text>
|
||||
重大事件发布前,竞拍评论区前三名位置
|
||||
</Text>
|
||||
<Text fontSize="sm" color="orange.700">
|
||||
<Text as="span" fontWeight="600">价值:</Text>
|
||||
前排评论获得最大曝光,吸引更多投资者
|
||||
</Text>
|
||||
<Text fontSize="sm" color="orange.700">
|
||||
<Text as="span" fontWeight="600">规则:</Text>
|
||||
最低出价500积分,出价最高者获得位置
|
||||
</Text>
|
||||
<Text fontSize="sm" color="orange.700">
|
||||
<Text as="span" fontWeight="600">结算:</Text>
|
||||
竞拍截止后,获胜者积分扣除,其他人退款
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 示例 */}
|
||||
<Box
|
||||
bg="blue.50"
|
||||
border="2px solid"
|
||||
borderColor="blue.400"
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
>
|
||||
<HStack spacing="3" mb="3">
|
||||
<Icon as={Gift} boxSize="24px" color="blue.600" />
|
||||
<Text fontWeight="700" color="blue.700" fontSize="lg">
|
||||
📖 完整示例
|
||||
</Text>
|
||||
</HStack>
|
||||
<VStack spacing="2" align="stretch">
|
||||
<Text fontSize="sm" color="blue.700">
|
||||
1. 大V"股神小王"在话题"贵州茅台会涨吗?"下发表3000字深度分析
|
||||
</Text>
|
||||
<Text fontSize="sm" color="blue.700">
|
||||
2. 小明看好这个分析,投资500积分(5份)
|
||||
</Text>
|
||||
<Text fontSize="sm" color="blue.700">
|
||||
3. 小红也跟投300积分(3份),此时价格已上涨到150积分/份
|
||||
</Text>
|
||||
<Text fontSize="sm" color="blue.700">
|
||||
4. 最终茅台真的上涨,管理员验证"预测正确"
|
||||
</Text>
|
||||
<Text fontSize="sm" color="blue.700" fontWeight="600">
|
||||
5. 收益分配:
|
||||
</Text>
|
||||
<Text fontSize="sm" color="blue.700" ml="4">
|
||||
• 小明获得:(5/8) × 1200积分 = 750积分,净收益+250积分
|
||||
</Text>
|
||||
<Text fontSize="sm" color="blue.700" ml="4">
|
||||
• 小红获得:(3/8) × 1200积分 = 450积分,净收益+150积分
|
||||
</Text>
|
||||
<Text fontSize="sm" color="blue.700" ml="4">
|
||||
• 股神小王获得:800积分 × 20% = 160积分作者奖励
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
|
||||
{/* 时间胶囊 */}
|
||||
<TabPanel p="0">
|
||||
<VStack spacing="6" align="stretch">
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
p="6"
|
||||
borderRadius="xl"
|
||||
border="2px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
>
|
||||
<HStack spacing="3" mb="3">
|
||||
<Icon as={Lock} boxSize="32px" color={forumColors.primary[500]} />
|
||||
<VStack align="start" spacing="0">
|
||||
<Text fontSize="2xl" fontWeight="700" color={forumColors.text.primary}>
|
||||
时间胶囊 - 长线预测的博弈
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
针对长期事件(如"人类何时登陆火星"),竞拍时间段,赌事件发生的年份
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
{/* 核心机制 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2" mb="4">
|
||||
<Icon as={Lightbulb} boxSize="20px" color="yellow.400" />
|
||||
<Text fontWeight="700" color={forumColors.text.primary} fontSize="lg">
|
||||
💡 核心机制
|
||||
</Text>
|
||||
</HStack>
|
||||
<VStack spacing="3" align="stretch">
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
p="4"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
>
|
||||
<Text fontWeight="600" color={forumColors.text.primary} mb="2">
|
||||
1. 创建话题 + 加密预测
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
发起一个长期预测话题(如"人类何时登陆火星:2025-2050年"),并提交加密的预测内容。需支付100积分创建费用,内容在解密前完全保密。
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="4"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<Text fontWeight="600" color={forumColors.text.primary} mb="2">
|
||||
2. 时间段切分 + 竞拍
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
系统自动将时间范围切分为多个年份时间段(如2025年、2026年...2050年)。用户可以竞拍任意时间段,最高出价者成为该时间段的持有者。
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="4"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<Text fontWeight="600" color={forumColors.text.primary} mb="2">
|
||||
3. 价格动态变化
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
随着时间推移,如果事件没有发生:临近过期的时间段价格会暴跌(如2025年快到了还没发生),而未来的时间段价格会通过竞拍飙升(概率提升)。
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="4"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<Text fontWeight="600" color={forumColors.text.primary} mb="2">
|
||||
4. 事件发生 + 结算
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
当事件真的发生时(如2035年人类登陆火星),管理员解密并结算。持有"2035年"时间段的用户获得全部奖池,其他时间段持有者失去投资。
|
||||
</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 加密机制 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2" mb="4">
|
||||
<Icon as={Lock} boxSize="20px" color="purple.400" />
|
||||
<Text fontWeight="700" color={forumColors.text.primary} fontSize="lg">
|
||||
🔐 加密机制
|
||||
</Text>
|
||||
</HStack>
|
||||
<VStack spacing="3" align="stretch">
|
||||
<HStack spacing="2">
|
||||
<Icon as={ArrowRight} boxSize="14px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
<Text as="span" fontWeight="600" color={forumColors.text.primary}>前端AES加密:</Text>
|
||||
提交时在浏览器端加密预测内容
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing="2">
|
||||
<Icon as={ArrowRight} boxSize="14px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
<Text as="span" fontWeight="600" color={forumColors.text.primary}>后端存储密钥:</Text>
|
||||
加密密钥存储在数据库中,解密前不公开
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing="2">
|
||||
<Icon as={ArrowRight} boxSize="14px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
<Text as="span" fontWeight="600" color={forumColors.text.primary}>防止马后炮:</Text>
|
||||
证明"我早就说过",无法事后修改预测
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing="2">
|
||||
<Icon as={ArrowRight} boxSize="14px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
<Text as="span" fontWeight="600" color={forumColors.text.primary}>管理员解密:</Text>
|
||||
事件发生后,管理员或作者解密查看内容
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 竞拍规则 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2" mb="4">
|
||||
<Icon as={TrendingUp} boxSize="20px" color="green.400" />
|
||||
<Text fontWeight="700" color={forumColors.text.primary} fontSize="lg">
|
||||
💰 竞拍规则
|
||||
</Text>
|
||||
</HStack>
|
||||
<List spacing="2" fontSize="sm" color={forumColors.text.secondary}>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
初始价格:每个时间段初始价格100积分
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
最低加价:每次出价至少比当前价格高50积分
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
自动退款:被超越的出价者自动退还积分
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
奖池累积:每次竞拍的价格增量进入话题奖池
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
创建费用:100积分进入奖池
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
{/* 示例 */}
|
||||
<Box
|
||||
bg="purple.50"
|
||||
border="2px solid"
|
||||
borderColor="purple.400"
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
>
|
||||
<HStack spacing="3" mb="3">
|
||||
<Icon as={Calendar} boxSize="24px" color="purple.600" />
|
||||
<Text fontWeight="700" color="purple.700" fontSize="lg">
|
||||
📖 完整示例:人类登陆火星
|
||||
</Text>
|
||||
</HStack>
|
||||
<VStack spacing="2" align="stretch">
|
||||
<Text fontSize="sm" color="purple.700">
|
||||
<Text as="span" fontWeight="600">2025年1月:</Text>
|
||||
小明创建话题"人类何时登陆火星:2025-2050年",支付100积分,提交加密预测
|
||||
</Text>
|
||||
<Text fontSize="sm" color="purple.700">
|
||||
<Text as="span" fontWeight="600">初始状态:</Text>
|
||||
系统创建26个时间段(2025-2050年),每个初始价格100积分
|
||||
</Text>
|
||||
<Text fontSize="sm" color="purple.700">
|
||||
<Text as="span" fontWeight="600">2025年2月:</Text>
|
||||
小红出价150积分竞拍"2030年"时间段,成为持有者
|
||||
</Text>
|
||||
<Text fontSize="sm" color="purple.700">
|
||||
<Text as="span" fontWeight="600">2025年3月:</Text>
|
||||
小李出价200积分超越小红,成为"2030年"新持有者(小红退还150积分)
|
||||
</Text>
|
||||
<Text fontSize="sm" color="purple.700">
|
||||
<Text as="span" fontWeight="600">2026年:</Text>
|
||||
"2025年"时间段过期无人竞拍(事件未发生),价值归零
|
||||
</Text>
|
||||
<Text fontSize="sm" color="purple.700">
|
||||
<Text as="span" fontWeight="600">2030年:</Text>
|
||||
SpaceX宣布人类成功登陆火星!
|
||||
</Text>
|
||||
<Text fontSize="sm" color="purple.700" fontWeight="600">
|
||||
<Text as="span" fontWeight="600">结算:</Text>
|
||||
</Text>
|
||||
<Text fontSize="sm" color="purple.700" ml="4">
|
||||
• 小李(持有"2030年")获得全部奖池(假设1500积分)
|
||||
</Text>
|
||||
<Text fontSize="sm" color="purple.700" ml="4">
|
||||
• 其他时间段持有者损失投资
|
||||
</Text>
|
||||
<Text fontSize="sm" color="purple.700" ml="4">
|
||||
• 话题解密,小明的预测内容公开
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 策略提示 */}
|
||||
<Box
|
||||
bg="green.50"
|
||||
border="2px solid"
|
||||
borderColor="green.400"
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
>
|
||||
<HStack spacing="3" mb="3">
|
||||
<Icon as={Target} boxSize="24px" color="green.600" />
|
||||
<Text fontWeight="700" color="green.700" fontSize="lg">
|
||||
🎯 策略提示
|
||||
</Text>
|
||||
</HStack>
|
||||
<VStack spacing="2" align="stretch">
|
||||
<Text fontSize="sm" color="green.700">
|
||||
✅ 早期竞拍成本低,但等待时间长
|
||||
</Text>
|
||||
<Text fontSize="sm" color="green.700">
|
||||
✅ 可以根据科技进展动态调整持有时间段
|
||||
</Text>
|
||||
<Text fontSize="sm" color="green.700">
|
||||
✅ 适合极其长线的宏大叙事预测
|
||||
</Text>
|
||||
<Text fontSize="sm" color="green.700">
|
||||
✅ 加密机制保证预测的真实性和不可篡改性
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</ModalBody>
|
||||
|
||||
Reference in New Issue
Block a user