update pay function

This commit is contained in:
2025-11-23 19:44:49 +08:00
parent 2fb12e0cc7
commit 3fa3e52d65
4 changed files with 2633 additions and 1 deletions

View File

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