TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 1 | # routes/posts.py |
wu | 90da17b | 2025-06-19 12:45:29 +0800 | [diff] [blame] | 2 | |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 3 | from flask import Blueprint, request, jsonify, abort |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 4 | from extensions import db |
| 5 | from models.post import Post |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 6 | from models.behavior import Behavior |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 7 | from utils.Fpost import Fpost |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 8 | import json |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 9 | |
| 10 | posts_bp = Blueprint('posts', __name__) |
| 11 | |
| 12 | @posts_bp.route('', methods=['POST']) |
| 13 | def create_post(): |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 14 | try: |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 15 | user_id = request.form.get('user_id') |
| 16 | title = request.form.get('title') |
| 17 | content = request.form.get('content') |
| 18 | status = request.form.get('status', 'published') |
| 19 | topic_id = request.form.get('topic_id') |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 20 | media_count = int(request.form.get('media_count', 0)) |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 21 | |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 22 | if not user_id or not title or not content: |
| 23 | return jsonify({'error': '缺少必要字段'}), 400 |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 24 | |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 25 | files = [] |
| 26 | for i in range(media_count): |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 27 | key = f'media_{i}' |
| 28 | if key in request.files: |
| 29 | files.append(request.files[key]) |
| 30 | |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 31 | fpost = Fpost(db.session) |
| 32 | new_post = fpost.create_post_with_files( |
| 33 | user_id=int(user_id), |
| 34 | title=title, |
| 35 | content=content, |
| 36 | topic_id=int(topic_id) if topic_id else None, |
| 37 | status=status, |
| 38 | files=files |
| 39 | ) |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 40 | |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 41 | return jsonify({'id': new_post.id}), 201 |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 42 | |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 43 | except Exception as e: |
| 44 | return jsonify({'error': str(e)}), 500 |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 45 | |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 46 | |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 47 | @posts_bp.route('', methods=['GET']) |
| 48 | def list_posts(): |
wu | 90da17b | 2025-06-19 12:45:29 +0800 | [diff] [blame] | 49 | """ |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 50 | GET /posts -> 全部已发布帖子 |
| 51 | GET /posts?user_id=xx -> 指定用户的所有帖子 |
wu | 90da17b | 2025-06-19 12:45:29 +0800 | [diff] [blame] | 52 | """ |
| 53 | user_id = request.args.get('user_id', type=int) |
| 54 | query = Post.query |
| 55 | if user_id is not None: |
| 56 | query = query.filter_by(user_id=user_id) |
| 57 | else: |
| 58 | query = query.filter_by(status='published') |
| 59 | |
| 60 | posts = query.all() |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 61 | return jsonify([{ |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 62 | 'id' : p.id, |
| 63 | 'title' : p.title, |
| 64 | 'status' : p.status, |
| 65 | 'heat' : p.heat, |
| 66 | 'created_at' : p.created_at.isoformat() |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 67 | } for p in posts]) |
| 68 | |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 69 | |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 70 | @posts_bp.route('/<int:post_id>', methods=['GET']) |
| 71 | def get_post(post_id): |
| 72 | post = Post.query.get_or_404(post_id) |
| 73 | return jsonify({ |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 74 | 'id' : post.id, |
| 75 | 'user_id' : post.user_id, |
| 76 | 'topic_id' : post.topic_id, |
| 77 | 'title' : post.title, |
| 78 | 'content' : post.content, |
| 79 | 'media_urls' : post.media_urls, |
| 80 | 'status' : post.status, |
| 81 | 'heat' : post.heat, |
| 82 | 'created_at' : post.created_at.isoformat(), |
| 83 | 'updated_at' : post.updated_at.isoformat() |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 84 | }) |
| 85 | |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 86 | |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 87 | @posts_bp.route('/<int:post_id>', methods=['PUT']) |
| 88 | def update_post(post_id): |
| 89 | """ |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 90 | 支持 FormData 和 JSON 两种格式更新: |
| 91 | - multipart/form-data 时可上传新文件并保留 existing_media_urls |
| 92 | - application/json 时只修改字段 |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 93 | """ |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 94 | try: |
| 95 | fpost = Fpost(db.session) |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 96 | |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 97 | if request.content_type and 'multipart/form-data' in request.content_type: |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 98 | title = request.form.get('title') |
| 99 | content = request.form.get('content') |
| 100 | status = request.form.get('status') |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 101 | topic_id = request.form.get('topic_id') |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 102 | count = int(request.form.get('media_count', 0)) |
| 103 | existing = request.form.get('existing_media_urls') |
| 104 | |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 105 | existing_media_urls = None |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 106 | if existing: |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 107 | try: |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 108 | existing_media_urls = json.loads(existing) |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 109 | except: |
| 110 | existing_media_urls = None |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 111 | |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 112 | files = [] |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 113 | for i in range(count): |
| 114 | key = f'media_{i}' |
| 115 | if key in request.files: |
| 116 | files.append(request.files[key]) |
| 117 | |
| 118 | updated = fpost.update_post_with_files( |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 119 | post_id=post_id, |
| 120 | title=title, |
| 121 | content=content, |
| 122 | topic_id=int(topic_id) if topic_id else None, |
| 123 | status=status, |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 124 | files=files or None, |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 125 | existing_media_urls=existing_media_urls |
| 126 | ) |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 127 | else: |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 128 | post = Post.query.get_or_404(post_id) |
| 129 | data = request.get_json() or {} |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 130 | for field in ('title','content','topic_id','media_urls','status'): |
| 131 | if field in data: |
| 132 | setattr(post, field, data[field]) |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 133 | db.session.commit() |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 134 | updated = post |
| 135 | |
| 136 | if not updated: |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 137 | return jsonify({'error': '帖子不存在'}), 404 |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 138 | |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 139 | return '', 204 |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 140 | |
TRM-coding | f55d237 | 2025-06-20 16:22:37 +0800 | [diff] [blame] | 141 | except Exception as e: |
| 142 | return jsonify({'error': str(e)}), 500 |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 143 | |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 144 | |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 145 | @posts_bp.route('/<int:post_id>', methods=['DELETE']) |
| 146 | def delete_post(post_id): |
| 147 | post = Post.query.get_or_404(post_id) |
| 148 | db.session.delete(post) |
| 149 | db.session.commit() |
| 150 | return '', 204 |
| 151 | |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 152 | |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 153 | # —— 显式的 like/favorite 删除和查询路由,放在泛用 action 路由之前 —— # |
wu | 5934be4 | 2025-06-24 11:50:34 +0800 | [diff] [blame] | 154 | |
| 155 | @posts_bp.route('/<int:post_id>/like', methods=['GET']) |
| 156 | def has_liked(post_id): |
| 157 | """ |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 158 | GET /posts/<post_id>/like?user_id=xx |
| 159 | 返回 { "liked": true/false } |
wu | 5934be4 | 2025-06-24 11:50:34 +0800 | [diff] [blame] | 160 | """ |
| 161 | user_id = request.args.get('user_id', type=int) |
| 162 | if not user_id: |
| 163 | abort(400, 'user_id required') |
| 164 | |
| 165 | exists = Behavior.query.filter_by( |
| 166 | user_id=user_id, |
| 167 | post_id=post_id, |
| 168 | type='like' |
| 169 | ).first() is not None |
| 170 | |
| 171 | return jsonify({'liked': exists}), 200 |
wu | 5a4acf7 | 2025-06-24 15:38:29 +0800 | [diff] [blame^] | 172 | |
| 173 | |
| 174 | @posts_bp.route('/<int:post_id>/like', methods=['DELETE']) |
| 175 | def unlike(post_id): |
| 176 | data = request.get_json(silent=True) or {} |
| 177 | user_id = data.get('user_id') |
| 178 | if not user_id: |
| 179 | abort(400, 'user_id required') |
| 180 | |
| 181 | beh = Behavior.query.filter_by( |
| 182 | user_id=user_id, |
| 183 | post_id=post_id, |
| 184 | type='like' |
| 185 | ).first() |
| 186 | if not beh: |
| 187 | return jsonify({'error': 'not liked yet'}), 400 |
| 188 | |
| 189 | db.session.delete(beh) |
| 190 | post = Post.query.get_or_404(post_id) |
| 191 | post.heat = max(post.heat - 1, 0) |
| 192 | db.session.commit() |
| 193 | return '', 204 |
| 194 | |
| 195 | |
| 196 | @posts_bp.route('/<int:post_id>/favorite', methods=['DELETE']) |
| 197 | def unfavorite(post_id): |
| 198 | data = request.get_json(silent=True) or {} |
| 199 | user_id = data.get('user_id') |
| 200 | if not user_id: |
| 201 | abort(400, 'user_id required') |
| 202 | |
| 203 | beh = Behavior.query.filter_by( |
| 204 | user_id=user_id, |
| 205 | post_id=post_id, |
| 206 | type='favorite' |
| 207 | ).first() |
| 208 | if not beh: |
| 209 | return jsonify({'error': 'not favorited yet'}), 400 |
| 210 | |
| 211 | db.session.delete(beh) |
| 212 | post = Post.query.get_or_404(post_id) |
| 213 | post.heat = max(post.heat - 1, 0) |
| 214 | db.session.commit() |
| 215 | return '', 204 |
| 216 | |
| 217 | |
| 218 | # —— 泛用 action 路由,仅处理 POST /posts/<id>/(like|favorite|view|share) —— # |
| 219 | |
| 220 | @posts_bp.route('/<int:post_id>/<action>', methods=['POST']) |
| 221 | def post_action(post_id, action): |
| 222 | """ |
| 223 | 支持 action: like, favorite, view, share, |
| 224 | 对 like/favorite 做幂等去重检查。 |
| 225 | """ |
| 226 | if action not in ('like','favorite','view','share'): |
| 227 | abort(400, 'Invalid action') |
| 228 | |
| 229 | data = request.get_json() or {} |
| 230 | user_id = data.get('user_id') |
| 231 | if not user_id: |
| 232 | abort(400, 'user_id required') |
| 233 | |
| 234 | # 幂等检查 |
| 235 | if action in ('like','favorite'): |
| 236 | exists = Behavior.query.filter_by( |
| 237 | user_id=user_id, |
| 238 | post_id=post_id, |
| 239 | type=action |
| 240 | ).first() |
| 241 | if exists: |
| 242 | return jsonify({'error': f'already {action}d'}), 400 |
| 243 | |
| 244 | # 记录行为 |
| 245 | beh = Behavior(user_id=user_id, post_id=post_id, type=action) |
| 246 | db.session.add(beh) |
| 247 | |
| 248 | # 更新热度 |
| 249 | post = Post.query.get_or_404(post_id) |
| 250 | post.heat += 1 |
| 251 | |
| 252 | db.session.commit() |
| 253 | return '', 201 |