blob: 4d8be1eb58f7247cff4b65b597cf58ea9a402fb7 [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
wu5a4acf72025-06-24 15:38:29 +08004from extensions import db
5from models.post import Post
TRM-codingd1cbf672025-06-18 15:15:08 +08006from models.behavior import Behavior
wu5a4acf72025-06-24 15:38:29 +08007from utils.Fpost import Fpost
TRM-codingf55d2372025-06-20 16:22:37 +08008import json
TRM-codingd1cbf672025-06-18 15:15:08 +08009
10posts_bp = Blueprint('posts', __name__)
11
12@posts_bp.route('', methods=['POST'])
13def create_post():
TRM-codingf55d2372025-06-20 16:22:37 +080014 try:
wu5a4acf72025-06-24 15:38:29 +080015 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-codingf55d2372025-06-20 16:22:37 +080020 media_count = int(request.form.get('media_count', 0))
wu5a4acf72025-06-24 15:38:29 +080021
TRM-codingf55d2372025-06-20 16:22:37 +080022 if not user_id or not title or not content:
23 return jsonify({'error': '缺少必要字段'}), 400
wu5a4acf72025-06-24 15:38:29 +080024
TRM-codingf55d2372025-06-20 16:22:37 +080025 files = []
26 for i in range(media_count):
wu5a4acf72025-06-24 15:38:29 +080027 key = f'media_{i}'
28 if key in request.files:
29 files.append(request.files[key])
30
TRM-codingf55d2372025-06-20 16:22:37 +080031 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 )
wu5a4acf72025-06-24 15:38:29 +080040
TRM-codingf55d2372025-06-20 16:22:37 +080041 return jsonify({'id': new_post.id}), 201
wu5a4acf72025-06-24 15:38:29 +080042
TRM-codingf55d2372025-06-20 16:22:37 +080043 except Exception as e:
44 return jsonify({'error': str(e)}), 500
TRM-codingd1cbf672025-06-18 15:15:08 +080045
wu5a4acf72025-06-24 15:38:29 +080046
TRM-codingd1cbf672025-06-18 15:15:08 +080047@posts_bp.route('', methods=['GET'])
48def list_posts():
wu90da17b2025-06-19 12:45:29 +080049 """
wu5a4acf72025-06-24 15:38:29 +080050 GET /posts -> 全部已发布帖子
51 GET /posts?user_id=xx -> 指定用户的所有帖子
wu90da17b2025-06-19 12:45:29 +080052 """
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-codingd1cbf672025-06-18 15:15:08 +080061 return jsonify([{
wu5a4acf72025-06-24 15:38:29 +080062 'id' : p.id,
63 'title' : p.title,
64 'status' : p.status,
65 'heat' : p.heat,
66 'created_at' : p.created_at.isoformat()
TRM-codingd1cbf672025-06-18 15:15:08 +080067 } for p in posts])
68
wu5a4acf72025-06-24 15:38:29 +080069
TRM-codingd1cbf672025-06-18 15:15:08 +080070@posts_bp.route('/<int:post_id>', methods=['GET'])
71def get_post(post_id):
72 post = Post.query.get_or_404(post_id)
73 return jsonify({
wu5a4acf72025-06-24 15:38:29 +080074 '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-codingd1cbf672025-06-18 15:15:08 +080084 })
85
wu5a4acf72025-06-24 15:38:29 +080086
TRM-codingd1cbf672025-06-18 15:15:08 +080087@posts_bp.route('/<int:post_id>', methods=['PUT'])
88def update_post(post_id):
89 """
wu5a4acf72025-06-24 15:38:29 +080090 支持 FormData 和 JSON 两种格式更新:
91 - multipart/form-data 时可上传新文件并保留 existing_media_urls
92 - application/json 时只修改字段
TRM-codingd1cbf672025-06-18 15:15:08 +080093 """
TRM-codingf55d2372025-06-20 16:22:37 +080094 try:
95 fpost = Fpost(db.session)
wu5a4acf72025-06-24 15:38:29 +080096
TRM-codingf55d2372025-06-20 16:22:37 +080097 if request.content_type and 'multipart/form-data' in request.content_type:
wu5a4acf72025-06-24 15:38:29 +080098 title = request.form.get('title')
99 content = request.form.get('content')
100 status = request.form.get('status')
TRM-codingf55d2372025-06-20 16:22:37 +0800101 topic_id = request.form.get('topic_id')
wu5a4acf72025-06-24 15:38:29 +0800102 count = int(request.form.get('media_count', 0))
103 existing = request.form.get('existing_media_urls')
104
TRM-codingf55d2372025-06-20 16:22:37 +0800105 existing_media_urls = None
wu5a4acf72025-06-24 15:38:29 +0800106 if existing:
TRM-codingf55d2372025-06-20 16:22:37 +0800107 try:
wu5a4acf72025-06-24 15:38:29 +0800108 existing_media_urls = json.loads(existing)
TRM-codingf55d2372025-06-20 16:22:37 +0800109 except:
110 existing_media_urls = None
wu5a4acf72025-06-24 15:38:29 +0800111
TRM-codingf55d2372025-06-20 16:22:37 +0800112 files = []
wu5a4acf72025-06-24 15:38:29 +0800113 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-codingf55d2372025-06-20 16:22:37 +0800119 post_id=post_id,
120 title=title,
121 content=content,
122 topic_id=int(topic_id) if topic_id else None,
123 status=status,
wu5a4acf72025-06-24 15:38:29 +0800124 files=files or None,
TRM-codingf55d2372025-06-20 16:22:37 +0800125 existing_media_urls=existing_media_urls
126 )
TRM-codingf55d2372025-06-20 16:22:37 +0800127 else:
TRM-codingf55d2372025-06-20 16:22:37 +0800128 post = Post.query.get_or_404(post_id)
129 data = request.get_json() or {}
wu5a4acf72025-06-24 15:38:29 +0800130 for field in ('title','content','topic_id','media_urls','status'):
131 if field in data:
132 setattr(post, field, data[field])
TRM-codingf55d2372025-06-20 16:22:37 +0800133 db.session.commit()
wu5a4acf72025-06-24 15:38:29 +0800134 updated = post
135
136 if not updated:
TRM-codingf55d2372025-06-20 16:22:37 +0800137 return jsonify({'error': '帖子不存在'}), 404
wu5a4acf72025-06-24 15:38:29 +0800138
TRM-codingf55d2372025-06-20 16:22:37 +0800139 return '', 204
wu5a4acf72025-06-24 15:38:29 +0800140
TRM-codingf55d2372025-06-20 16:22:37 +0800141 except Exception as e:
142 return jsonify({'error': str(e)}), 500
TRM-codingd1cbf672025-06-18 15:15:08 +0800143
wu5a4acf72025-06-24 15:38:29 +0800144
TRM-codingd1cbf672025-06-18 15:15:08 +0800145@posts_bp.route('/<int:post_id>', methods=['DELETE'])
146def 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-codingd1cbf672025-06-18 15:15:08 +0800152
wu5a4acf72025-06-24 15:38:29 +0800153# —— 显式的 like/favorite 删除和查询路由,放在泛用 action 路由之前 —— #
wu5934be42025-06-24 11:50:34 +0800154
155@posts_bp.route('/<int:post_id>/like', methods=['GET'])
156def has_liked(post_id):
157 """
wu5a4acf72025-06-24 15:38:29 +0800158 GET /posts/<post_id>/like?user_id=xx
159 返回 { "liked": true/false }
wu5934be42025-06-24 11:50:34 +0800160 """
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
wu5a4acf72025-06-24 15:38:29 +0800172
173
174@posts_bp.route('/<int:post_id>/like', methods=['DELETE'])
175def 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'])
197def 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'])
221def 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