blob: 4c89334882873f8a960f5b340842e755b51c245e [file] [log] [blame]
TRM-codingd1cbf672025-06-18 15:15:08 +08001# routes/posts.py
wu90da17b2025-06-19 12:45:29 +08002
TRM-codingd1cbf672025-06-18 15:15:08 +08003from flask import Blueprint, request, jsonify, abort
wu52491532025-06-24 22:47:35 +08004from extensions import db
5from models.post import Post
TRM-codingd1cbf672025-06-18 15:15:08 +08006from models.behavior import Behavior
wu52491532025-06-24 22:47:35 +08007from utils.Fpost import Fpost
trmfaf2af52025-06-27 17:05:44 +00008from sqlalchemy import create_engine
9from sqlalchemy.orm import sessionmaker
10from config import Config
TRM-codingf55d2372025-06-20 16:22:37 +080011import json
TRM-codingd1cbf672025-06-18 15:15:08 +080012
13posts_bp = Blueprint('posts', __name__)
14
trmfaf2af52025-06-27 17:05:44 +000015def safe_log(user_id, log_type, message, ip):
16 user_id=1
17 """安全的日志记录函数,不会影响主业务逻辑"""
18 try:
19 engine = create_engine(Config.SQLURL)
20 SessionLocal = sessionmaker(bind=engine)
21 session = SessionLocal()
22 f = Fpost(session)
23 f.recordlog(user_id, log_type, message, ip)
24 except Exception as e:
25 # 方法一:简单打印错误信息
26 print(f"[safe_log] 记录日志失败:{e}")
27 traceback.print_exc() # 打印完整的堆栈跟踪
28
29 # 方法二:使用 logging 模块(推荐)
30 logger.exception("safe_log: 记录日志时发生异常")
31 finally:
32 # 确保 session 一定关闭
33 try:
34 session.close()
35 except Exception:
36 pass
37
TRM-codingd1cbf672025-06-18 15:15:08 +080038@posts_bp.route('', methods=['POST'])
39def create_post():
trmfaf2af52025-06-27 17:05:44 +000040 user_id = None
TRM-codingf55d2372025-06-20 16:22:37 +080041 try:
wu52491532025-06-24 22:47:35 +080042 # 获取文本字段
43 user_id = request.form.get('user_id')
44 title = request.form.get('title')
45 content = request.form.get('content')
46 status = request.form.get('status', 'published')
47 topic_id = request.form.get('topic_id')
TRM-codingf55d2372025-06-20 16:22:37 +080048 media_count = int(request.form.get('media_count', 0))
wu52491532025-06-24 22:47:35 +080049
TRM-codingf55d2372025-06-20 16:22:37 +080050 if not user_id or not title or not content:
trmfaf2af52025-06-27 17:05:44 +000051 safe_log(int(user_id) if user_id else 0, 'error', '创建帖子失败:缺少必要字段', request.remote_addr)
TRM-codingf55d2372025-06-20 16:22:37 +080052 return jsonify({'error': '缺少必要字段'}), 400
wu52491532025-06-24 22:47:35 +080053
54 # 获取上传的文件
TRM-codingf55d2372025-06-20 16:22:37 +080055 files = []
56 for i in range(media_count):
wu52491532025-06-24 22:47:35 +080057 file_key = f'media_{i}'
58 if file_key in request.files:
59 files.append(request.files[file_key])
60
61 # 使用 Fpost 创建帖子
TRM-codingf55d2372025-06-20 16:22:37 +080062 fpost = Fpost(db.session)
63 new_post = fpost.create_post_with_files(
64 user_id=int(user_id),
65 title=title,
66 content=content,
67 topic_id=int(topic_id) if topic_id else None,
68 status=status,
69 files=files
70 )
wu52491532025-06-24 22:47:35 +080071
trmfaf2af52025-06-27 17:05:44 +000072 safe_log(int(user_id), 'behavior', f'成功创建帖子,帖子ID: {new_post.id},标题: {title}', request.remote_addr)
TRM-codingf55d2372025-06-20 16:22:37 +080073 return jsonify({'id': new_post.id}), 201
wu52491532025-06-24 22:47:35 +080074
TRM-codingf55d2372025-06-20 16:22:37 +080075 except Exception as e:
trmfaf2af52025-06-27 17:05:44 +000076 safe_log(int(user_id) if user_id else 0, 'error', f'创建帖子时发生异常: {str(e)}', request.remote_addr)
TRM-codingf55d2372025-06-20 16:22:37 +080077 return jsonify({'error': str(e)}), 500
TRM-codingd1cbf672025-06-18 15:15:08 +080078
79@posts_bp.route('', methods=['GET'])
80def list_posts():
wu90da17b2025-06-19 12:45:29 +080081 """
wu52491532025-06-24 22:47:35 +080082 获取帖子列表,支持:
83 - GET /posts 返回所有已发布帖子
84 - GET /posts?user_id=123 返回指定用户 user_id 的所有帖子
wu90da17b2025-06-19 12:45:29 +080085 """
trmfaf2af52025-06-27 17:05:44 +000086 user_id = None
87 try:
88 user_id = request.args.get('user_id', type=int)
89 query = Post.query
90 if user_id is not None:
91 query = query.filter_by(user_id=user_id)
92 else:
93 query = query.filter_by(status='published')
wu90da17b2025-06-19 12:45:29 +080094
trmfaf2af52025-06-27 17:05:44 +000095 posts = query.all()
96
97 log_msg = f'获取帖子列表成功,共{len(posts)}个帖子'
98 if user_id:
99 log_msg += f',用户ID: {user_id}'
100 safe_log(user_id if user_id else 0, 'access', log_msg, request.remote_addr)
TRM-codingd1cbf672025-06-18 15:15:08 +0800101
trmfaf2af52025-06-27 17:05:44 +0000102 return jsonify([{
103 'id': p.id,
104 'title': p.title,
105 'status': p.status,
106 'heat': p.heat,
107 'created_at': p.created_at.isoformat()
108 } for p in posts])
109
110 except Exception as e:
111 safe_log(user_id if user_id else 0, 'error', f'获取帖子列表时发生异常: {str(e)}', request.remote_addr)
112 return jsonify({'error': str(e)}), 500
wu5a4acf72025-06-24 15:38:29 +0800113
TRM-codingd1cbf672025-06-18 15:15:08 +0800114@posts_bp.route('/<int:post_id>', methods=['GET'])
115def get_post(post_id):
trmfaf2af52025-06-27 17:05:44 +0000116 try:
117 post = Post.query.get_or_404(post_id)
118 safe_log(0, 'access', f'获取帖子详情成功,帖子ID: {post_id},标题: {post.title}', request.remote_addr)
119
120 return jsonify({
121 'id': post.id,
122 'user_id': post.user_id,
123 'topic_id': post.topic_id,
124 'title': post.title,
125 'content': post.content,
126 'media_urls': post.media_urls,
127 'status': post.status,
128 'heat': post.heat,
129 'created_at': post.created_at.isoformat(),
130 'updated_at': post.updated_at.isoformat()
131 })
132
133 except Exception as e:
134 safe_log(0, 'error', f'获取帖子详情时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
135 return jsonify({'error': str(e)}), 500
TRM-codingd1cbf672025-06-18 15:15:08 +0800136
137@posts_bp.route('/<int:post_id>', methods=['PUT'])
138def update_post(post_id):
139 """
wu52491532025-06-24 22:47:35 +0800140 修改帖子字段(可选字段:title, content, topic_id, media_urls, status)
141 支持FormData和JSON两种格式
TRM-codingd1cbf672025-06-18 15:15:08 +0800142 """
TRM-codingf55d2372025-06-20 16:22:37 +0800143 try:
144 fpost = Fpost(db.session)
wu52491532025-06-24 22:47:35 +0800145
146 # 检查是否是FormData请求
TRM-codingf55d2372025-06-20 16:22:37 +0800147 if request.content_type and 'multipart/form-data' in request.content_type:
wu52491532025-06-24 22:47:35 +0800148 # FormData请求
149 title = request.form.get('title')
150 content = request.form.get('content')
151 status = request.form.get('status')
TRM-codingf55d2372025-06-20 16:22:37 +0800152 topic_id = request.form.get('topic_id')
wu52491532025-06-24 22:47:35 +0800153 media_count = int(request.form.get('media_count', 0))
154 existing_media_urls_str = request.form.get('existing_media_urls')
155
156 # 解析现有媒体URLs
TRM-codingf55d2372025-06-20 16:22:37 +0800157 existing_media_urls = None
wu52491532025-06-24 22:47:35 +0800158 if existing_media_urls_str:
TRM-codingf55d2372025-06-20 16:22:37 +0800159 try:
wu52491532025-06-24 22:47:35 +0800160 existing_media_urls = json.loads(existing_media_urls_str)
TRM-codingf55d2372025-06-20 16:22:37 +0800161 except:
162 existing_media_urls = None
wu52491532025-06-24 22:47:35 +0800163
164 # 获取新上传的文件
TRM-codingf55d2372025-06-20 16:22:37 +0800165 files = []
wu52491532025-06-24 22:47:35 +0800166 for i in range(media_count):
167 file_key = f'media_{i}'
168 if file_key in request.files:
169 files.append(request.files[file_key])
170
171 # 更新帖子
172 updated_post = fpost.update_post_with_files(
TRM-codingf55d2372025-06-20 16:22:37 +0800173 post_id=post_id,
174 title=title,
175 content=content,
176 topic_id=int(topic_id) if topic_id else None,
177 status=status,
wu52491532025-06-24 22:47:35 +0800178 files=files if files else None,
TRM-codingf55d2372025-06-20 16:22:37 +0800179 existing_media_urls=existing_media_urls
180 )
wu52491532025-06-24 22:47:35 +0800181
TRM-codingf55d2372025-06-20 16:22:37 +0800182 else:
wu52491532025-06-24 22:47:35 +0800183 # JSON请求(保持原有逻辑)
TRM-codingf55d2372025-06-20 16:22:37 +0800184 post = Post.query.get_or_404(post_id)
185 data = request.get_json() or {}
wu52491532025-06-24 22:47:35 +0800186 for key in ('title', 'content', 'topic_id', 'media_urls', 'status'):
187 if key in data:
188 setattr(post, key, data[key])
TRM-codingf55d2372025-06-20 16:22:37 +0800189 db.session.commit()
wu52491532025-06-24 22:47:35 +0800190 updated_post = post
191
192 if not updated_post:
trmfaf2af52025-06-27 17:05:44 +0000193 safe_log(0, 'error', f'更新帖子失败,帖子不存在,帖子ID: {post_id}', request.remote_addr)
TRM-codingf55d2372025-06-20 16:22:37 +0800194 return jsonify({'error': '帖子不存在'}), 404
trmfaf2af52025-06-27 17:05:44 +0000195
196 safe_log(0, 'behavior', f'更新帖子成功,帖子ID: {post_id}', request.remote_addr)
TRM-codingf55d2372025-06-20 16:22:37 +0800197 return '', 204
wu52491532025-06-24 22:47:35 +0800198
TRM-codingf55d2372025-06-20 16:22:37 +0800199 except Exception as e:
trmfaf2af52025-06-27 17:05:44 +0000200 safe_log(0, 'error', f'更新帖子时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
TRM-codingf55d2372025-06-20 16:22:37 +0800201 return jsonify({'error': str(e)}), 500
TRM-codingd1cbf672025-06-18 15:15:08 +0800202
203@posts_bp.route('/<int:post_id>', methods=['DELETE'])
204def delete_post(post_id):
trmfaf2af52025-06-27 17:05:44 +0000205 try:
206 post = Post.query.get_or_404(post_id)
207 db.session.delete(post)
208 db.session.commit()
209 safe_log(0, 'behavior', f'删除帖子成功,帖子ID: {post_id}', request.remote_addr)
210 return '', 204
211
212 except Exception as e:
213 safe_log(0, 'error', f'删除帖子时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
214 return jsonify({'error': str(e)}), 500
TRM-codingd1cbf672025-06-18 15:15:08 +0800215
wu52491532025-06-24 22:47:35 +0800216@posts_bp.route('/<int:post_id>/<action>', methods=['POST'])
217def post_action(post_id, action):
218 """
219 支持的 action: like, favorite, view, share
220 对于 like 和 favorite,保证每个用户每帖只做一次。
221 """
trmfaf2af52025-06-27 17:05:44 +0000222 user_id = None
223 try:
224 if action not in ('like', 'favorite', 'view', 'share'):
225 abort(400, 'Invalid action')
TRM-codingd1cbf672025-06-18 15:15:08 +0800226
trmfaf2af52025-06-27 17:05:44 +0000227 data = request.get_json() or {}
228 user_id = data.get('user_id')
229 if not user_id:
230 safe_log(0, 'error', f'执行帖子操作失败,缺少用户ID,帖子ID: {post_id},操作: {action}', request.remote_addr)
231 abort(400, 'user_id required')
wu5934be42025-06-24 11:50:34 +0800232
trmfaf2af52025-06-27 17:05:44 +0000233 # 对 like/favorite 做去重检查
234 if action in ('like', 'favorite'):
235 exists = Behavior.query.filter_by(
236 user_id=user_id,
237 post_id=post_id,
238 type=action
239 ).first()
240 if exists:
241 safe_log(user_id, 'error', f'重复执行帖子操作,帖子ID: {post_id},操作: {action}', request.remote_addr)
242 return jsonify({'error': f'already {action}d'}), 400
wu52491532025-06-24 22:47:35 +0800243
trmfaf2af52025-06-27 17:05:44 +0000244 # 创建行为记录
245 beh = Behavior(user_id=user_id, post_id=post_id, type=action)
246 db.session.add(beh)
wu52491532025-06-24 22:47:35 +0800247
trmfaf2af52025-06-27 17:05:44 +0000248 # 更新热度
249 post = Post.query.get_or_404(post_id)
250 post.heat += 1
wu52491532025-06-24 22:47:35 +0800251
trmfaf2af52025-06-27 17:05:44 +0000252 db.session.commit()
253 safe_log(user_id, 'behavior', f'执行帖子操作成功,帖子ID: {post_id},操作: {action}', request.remote_addr)
254 return '', 201
255
256 except Exception as e:
257 safe_log(user_id if user_id else 0, 'error', f'执行帖子操作时发生异常,帖子ID: {post_id},操作: {action},错误: {str(e)}', request.remote_addr)
258 return jsonify({'error': str(e)}), 500
wu52491532025-06-24 22:47:35 +0800259
260@posts_bp.route('/<int:post_id>/like', methods=['DELETE'])
261def unlike(post_id):
trmfaf2af52025-06-27 17:05:44 +0000262 user_id = None
263 try:
264 user_id = request.get_json(silent=True) and request.get_json().get('user_id')
265 if not user_id:
266 safe_log(0, 'error', f'取消点赞失败,缺少用户ID,帖子ID: {post_id}', request.remote_addr)
267 abort(400, 'user_id required')
268
269 # 查找已有的 like 行为
270 beh = Behavior.query.filter_by(
271 user_id=user_id,
272 post_id=post_id,
273 type='like'
274 ).first()
275 if not beh:
276 safe_log(user_id, 'error', f'取消点赞失败,用户尚未点赞,帖子ID: {post_id}', request.remote_addr)
277 return jsonify({'error': 'not liked yet'}), 400
wu52491532025-06-24 22:47:35 +0800278
trmfaf2af52025-06-27 17:05:44 +0000279 db.session.delete(beh)
280 # 更新热度,确保不降到负数
281 post = Post.query.get_or_404(post_id)
282 post.heat = max(post.heat - 1, 0)
283 db.session.commit()
284 safe_log(user_id, 'behavior', f'取消点赞成功,帖子ID: {post_id}', request.remote_addr)
285 return '', 204
286
287 except Exception as e:
288 safe_log(user_id if user_id else 0, 'error', f'取消点赞时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
289 return jsonify({'error': str(e)}), 500
wu52491532025-06-24 22:47:35 +0800290
291@posts_bp.route('/<int:post_id>/favorite', methods=['DELETE'])
292def unfavorite(post_id):
trmfaf2af52025-06-27 17:05:44 +0000293 user_id = None
294 try:
295 user_id = request.get_json(silent=True) and request.get_json().get('user_id')
296 if not user_id:
297 safe_log(0, 'error', f'取消收藏失败,缺少用户ID,帖子ID: {post_id}', request.remote_addr)
298 abort(400, 'user_id required')
299
300 # 查找已有的 favorite 行为
301 beh = Behavior.query.filter_by(
302 user_id=user_id,
303 post_id=post_id,
304 type='favorite'
305 ).first()
306 if not beh:
307 safe_log(user_id, 'error', f'取消收藏失败,用户尚未收藏,帖子ID: {post_id}', request.remote_addr)
308 return jsonify({'error': 'not favorited yet'}), 400
wu52491532025-06-24 22:47:35 +0800309
trmfaf2af52025-06-27 17:05:44 +0000310 db.session.delete(beh)
311 # 更新热度
312 post = Post.query.get_or_404(post_id)
313 post.heat = max(post.heat - 1, 0)
314 db.session.commit()
315 safe_log(user_id, 'behavior', f'取消收藏成功,帖子ID: {post_id}', request.remote_addr)
316 return '', 204
317
318 except Exception as e:
319 safe_log(user_id if user_id else 0, 'error', f'取消收藏时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
320 return jsonify({'error': str(e)}), 500
wu52491532025-06-24 22:47:35 +0800321
322@posts_bp.route('/<int:post_id>/like/status', methods=['GET'])
wu5934be42025-06-24 11:50:34 +0800323def has_liked(post_id):
324 """
wu52491532025-06-24 22:47:35 +0800325 检查指定 user_id 是否对 post_id 点过赞。
326 GET /posts/<post_id>/like/status?user_id=123
327 返回 { "liked": true } 或 { "liked": false }
wu5934be42025-06-24 11:50:34 +0800328 """
trmfaf2af52025-06-27 17:05:44 +0000329 user_id = None
330 try:
331 user_id = request.args.get('user_id', type=int)
332 if not user_id:
333 safe_log(0, 'error', f'检查点赞状态失败,缺少用户ID,帖子ID: {post_id}', request.remote_addr)
334 abort(400, 'user_id required')
wu5934be42025-06-24 11:50:34 +0800335
trmfaf2af52025-06-27 17:05:44 +0000336 exists = Behavior.query.filter_by(
337 user_id=user_id,
338 post_id=post_id,
339 type='like'
340 ).first() is not None
341
342 safe_log(user_id, 'access', f'检查点赞状态成功,帖子ID: {post_id},结果: {exists}', request.remote_addr)
343 return jsonify({'liked': exists}), 200
344
345 except Exception as e:
346 safe_log(user_id if user_id else 0, 'error', f'检查点赞状态时发生异常,帖子ID: {post_id},错误: {str(e)}', request.remote_addr)
347 return jsonify({'error': str(e)}), 500