blob: 4c89334882873f8a960f5b340842e755b51c245e [file] [log] [blame]
# routes/posts.py
from flask import Blueprint, request, jsonify, abort
from extensions import db
from models.post import Post
from models.behavior import Behavior
from utils.Fpost import Fpost
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from config import Config
import json
posts_bp = Blueprint('posts', __name__)
def safe_log(user_id, log_type, message, ip):
user_id=1
"""安全的日志记录函数,不会影响主业务逻辑"""
try:
engine = create_engine(Config.SQLURL)
SessionLocal = sessionmaker(bind=engine)
session = SessionLocal()
f = Fpost(session)
f.recordlog(user_id, log_type, message, ip)
except Exception as e:
# 方法一:简单打印错误信息
print(f"[safe_log] 记录日志失败:{e}")
traceback.print_exc() # 打印完整的堆栈跟踪
# 方法二:使用 logging 模块(推荐)
logger.exception("safe_log: 记录日志时发生异常")
finally:
# 确保 session 一定关闭
try:
session.close()
except Exception:
pass
@posts_bp.route('', methods=['POST'])
def create_post():
user_id = None
try:
# 获取文本字段
user_id = request.form.get('user_id')
title = request.form.get('title')
content = request.form.get('content')
status = request.form.get('status', 'published')
topic_id = request.form.get('topic_id')
media_count = int(request.form.get('media_count', 0))
if not user_id or not title or not content:
safe_log(int(user_id) if user_id else 0, 'error', '创建帖子失败:缺少必要字段', request.remote_addr)
return jsonify({'error': '缺少必要字段'}), 400
# 获取上传的文件
files = []
for i in range(media_count):
file_key = f'media_{i}'
if file_key in request.files:
files.append(request.files[file_key])
# 使用 Fpost 创建帖子
fpost = Fpost(db.session)
new_post = fpost.create_post_with_files(
user_id=int(user_id),
title=title,
content=content,
topic_id=int(topic_id) if topic_id else None,
status=status,
files=files
)
safe_log(int(user_id), 'behavior', f'成功创建帖子,帖子ID: {new_post.id},标题: {title}', request.remote_addr)
return jsonify({'id': new_post.id}), 201
except Exception as e:
safe_log(int(user_id) if user_id else 0, 'error', f'创建帖子时发生异常: {str(e)}', request.remote_addr)
return jsonify({'error': str(e)}), 500
@posts_bp.route('', methods=['GET'])
def list_posts():
"""
获取帖子列表,支持:
- GET /posts 返回所有已发布帖子
- GET /posts?user_id=123 返回指定用户 user_id 的所有帖子
"""
user_id = None
try:
user_id = request.args.get('user_id', type=int)
query = Post.query
if user_id is not None:
query = query.filter_by(user_id=user_id)
else:
query = query.filter_by(status='published')
posts = query.all()
log_msg = f'获取帖子列表成功,共{len(posts)}个帖子'
if user_id:
log_msg += f',用户ID: {user_id}'
safe_log(user_id if user_id else 0, 'access', log_msg, request.remote_addr)
return jsonify([{
'id': p.id,
'title': p.title,
'status': p.status,
'heat': p.heat,
'created_at': p.created_at.isoformat()
} for p in posts])
except Exception as e:
safe_log(user_id if user_id else 0, 'error', f'获取帖子列表时发生异常: {str(e)}', request.remote_addr)
return jsonify({'error': str(e)}), 500
@posts_bp.route('/<int:post_id>', methods=['GET'])
def get_post(post_id):
try:
post = Post.query.get_or_404(post_id)
safe_log(0, 'access', f'获取帖子详情成功,帖子ID: {post_id},标题: {post.title}', request.remote_addr)
return jsonify({
'id': post.id,
'user_id': post.user_id,
'topic_id': post.topic_id,
'title': post.title,
'content': post.content,
'media_urls': post.media_urls,
'status': post.status,
'heat': post.heat,
'created_at': post.created_at.isoformat(),
'updated_at': post.updated_at.isoformat()
})
except Exception as e:
safe_log(0, 'error', f'获取帖子详情时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
return jsonify({'error': str(e)}), 500
@posts_bp.route('/<int:post_id>', methods=['PUT'])
def update_post(post_id):
"""
修改帖子字段(可选字段:title, content, topic_id, media_urls, status)
支持FormData和JSON两种格式
"""
try:
fpost = Fpost(db.session)
# 检查是否是FormData请求
if request.content_type and 'multipart/form-data' in request.content_type:
# FormData请求
title = request.form.get('title')
content = request.form.get('content')
status = request.form.get('status')
topic_id = request.form.get('topic_id')
media_count = int(request.form.get('media_count', 0))
existing_media_urls_str = request.form.get('existing_media_urls')
# 解析现有媒体URLs
existing_media_urls = None
if existing_media_urls_str:
try:
existing_media_urls = json.loads(existing_media_urls_str)
except:
existing_media_urls = None
# 获取新上传的文件
files = []
for i in range(media_count):
file_key = f'media_{i}'
if file_key in request.files:
files.append(request.files[file_key])
# 更新帖子
updated_post = fpost.update_post_with_files(
post_id=post_id,
title=title,
content=content,
topic_id=int(topic_id) if topic_id else None,
status=status,
files=files if files else None,
existing_media_urls=existing_media_urls
)
else:
# JSON请求(保持原有逻辑)
post = Post.query.get_or_404(post_id)
data = request.get_json() or {}
for key in ('title', 'content', 'topic_id', 'media_urls', 'status'):
if key in data:
setattr(post, key, data[key])
db.session.commit()
updated_post = post
if not updated_post:
safe_log(0, 'error', f'更新帖子失败,帖子不存在,帖子ID: {post_id}', request.remote_addr)
return jsonify({'error': '帖子不存在'}), 404
safe_log(0, 'behavior', f'更新帖子成功,帖子ID: {post_id}', request.remote_addr)
return '', 204
except Exception as e:
safe_log(0, 'error', f'更新帖子时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
return jsonify({'error': str(e)}), 500
@posts_bp.route('/<int:post_id>', methods=['DELETE'])
def delete_post(post_id):
try:
post = Post.query.get_or_404(post_id)
db.session.delete(post)
db.session.commit()
safe_log(0, 'behavior', f'删除帖子成功,帖子ID: {post_id}', request.remote_addr)
return '', 204
except Exception as e:
safe_log(0, 'error', f'删除帖子时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
return jsonify({'error': str(e)}), 500
@posts_bp.route('/<int:post_id>/<action>', methods=['POST'])
def post_action(post_id, action):
"""
支持的 action: like, favorite, view, share
对于 like 和 favorite,保证每个用户每帖只做一次。
"""
user_id = None
try:
if action not in ('like', 'favorite', 'view', 'share'):
abort(400, 'Invalid action')
data = request.get_json() or {}
user_id = data.get('user_id')
if not user_id:
safe_log(0, 'error', f'执行帖子操作失败,缺少用户ID,帖子ID: {post_id},操作: {action}', request.remote_addr)
abort(400, 'user_id required')
# 对 like/favorite 做去重检查
if action in ('like', 'favorite'):
exists = Behavior.query.filter_by(
user_id=user_id,
post_id=post_id,
type=action
).first()
if exists:
safe_log(user_id, 'error', f'重复执行帖子操作,帖子ID: {post_id},操作: {action}', request.remote_addr)
return jsonify({'error': f'already {action}d'}), 400
# 创建行为记录
beh = Behavior(user_id=user_id, post_id=post_id, type=action)
db.session.add(beh)
# 更新热度
post = Post.query.get_or_404(post_id)
post.heat += 1
db.session.commit()
safe_log(user_id, 'behavior', f'执行帖子操作成功,帖子ID: {post_id},操作: {action}', request.remote_addr)
return '', 201
except Exception as e:
safe_log(user_id if user_id else 0, 'error', f'执行帖子操作时发生异常,帖子ID: {post_id},操作: {action},错误: {str(e)}', request.remote_addr)
return jsonify({'error': str(e)}), 500
@posts_bp.route('/<int:post_id>/like', methods=['DELETE'])
def unlike(post_id):
user_id = None
try:
user_id = request.get_json(silent=True) and request.get_json().get('user_id')
if not user_id:
safe_log(0, 'error', f'取消点赞失败,缺少用户ID,帖子ID: {post_id}', request.remote_addr)
abort(400, 'user_id required')
# 查找已有的 like 行为
beh = Behavior.query.filter_by(
user_id=user_id,
post_id=post_id,
type='like'
).first()
if not beh:
safe_log(user_id, 'error', f'取消点赞失败,用户尚未点赞,帖子ID: {post_id}', request.remote_addr)
return jsonify({'error': 'not liked yet'}), 400
db.session.delete(beh)
# 更新热度,确保不降到负数
post = Post.query.get_or_404(post_id)
post.heat = max(post.heat - 1, 0)
db.session.commit()
safe_log(user_id, 'behavior', f'取消点赞成功,帖子ID: {post_id}', request.remote_addr)
return '', 204
except Exception as e:
safe_log(user_id if user_id else 0, 'error', f'取消点赞时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
return jsonify({'error': str(e)}), 500
@posts_bp.route('/<int:post_id>/favorite', methods=['DELETE'])
def unfavorite(post_id):
user_id = None
try:
user_id = request.get_json(silent=True) and request.get_json().get('user_id')
if not user_id:
safe_log(0, 'error', f'取消收藏失败,缺少用户ID,帖子ID: {post_id}', request.remote_addr)
abort(400, 'user_id required')
# 查找已有的 favorite 行为
beh = Behavior.query.filter_by(
user_id=user_id,
post_id=post_id,
type='favorite'
).first()
if not beh:
safe_log(user_id, 'error', f'取消收藏失败,用户尚未收藏,帖子ID: {post_id}', request.remote_addr)
return jsonify({'error': 'not favorited yet'}), 400
db.session.delete(beh)
# 更新热度
post = Post.query.get_or_404(post_id)
post.heat = max(post.heat - 1, 0)
db.session.commit()
safe_log(user_id, 'behavior', f'取消收藏成功,帖子ID: {post_id}', request.remote_addr)
return '', 204
except Exception as e:
safe_log(user_id if user_id else 0, 'error', f'取消收藏时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
return jsonify({'error': str(e)}), 500
@posts_bp.route('/<int:post_id>/like/status', methods=['GET'])
def has_liked(post_id):
"""
检查指定 user_id 是否对 post_id 点过赞。
GET /posts/<post_id>/like/status?user_id=123
返回 { "liked": true } 或 { "liked": false }
"""
user_id = None
try:
user_id = request.args.get('user_id', type=int)
if not user_id:
safe_log(0, 'error', f'检查点赞状态失败,缺少用户ID,帖子ID: {post_id}', request.remote_addr)
abort(400, 'user_id required')
exists = Behavior.query.filter_by(
user_id=user_id,
post_id=post_id,
type='like'
).first() is not None
safe_log(user_id, 'access', f'检查点赞状态成功,帖子ID: {post_id},结果: {exists}', request.remote_addr)
return jsonify({'liked': exists}), 200
except Exception as e:
safe_log(user_id if user_id else 0, 'error', f'检查点赞状态时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
return jsonify({'error': str(e)}), 500