修改信息
Change-Id: Ic5613c897dc716af06503b865fd9895a0614d6bc
diff --git a/Merge/back_wzy/routes/posts.py b/Merge/back_wzy/routes/posts.py
index 4d8be1e..6fd2a89 100644
--- a/Merge/back_wzy/routes/posts.py
+++ b/Merge/back_wzy/routes/posts.py
@@ -1,10 +1,10 @@
# routes/posts.py
from flask import Blueprint, request, jsonify, abort
-from extensions import db
-from models.post import Post
+from extensions import db
+from models.post import Post
from models.behavior import Behavior
-from utils.Fpost import Fpost
+from utils.Fpost import Fpost
import json
posts_bp = Blueprint('posts', __name__)
@@ -12,22 +12,25 @@
@posts_bp.route('', methods=['POST'])
def create_post():
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')
+ # 获取文本字段
+ 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:
return jsonify({'error': '缺少必要字段'}), 400
-
+
+ # 获取上传的文件
files = []
for i in range(media_count):
- key = f'media_{i}'
- if key in request.files:
- files.append(request.files[key])
-
+ 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),
@@ -37,18 +40,18 @@
status=status,
files=files
)
-
+
return jsonify({'id': new_post.id}), 201
-
+
except Exception as e:
return jsonify({'error': str(e)}), 500
-
@posts_bp.route('', methods=['GET'])
def list_posts():
"""
- GET /posts -> 全部已发布帖子
- GET /posts?user_id=xx -> 指定用户的所有帖子
+ 获取帖子列表,支持:
+ - GET /posts 返回所有已发布帖子
+ - GET /posts?user_id=123 返回指定用户 user_id 的所有帖子
"""
user_id = request.args.get('user_id', type=int)
query = Post.query
@@ -58,90 +61,94 @@
query = query.filter_by(status='published')
posts = query.all()
- return jsonify([{
- 'id' : p.id,
- 'title' : p.title,
- 'status' : p.status,
- 'heat' : p.heat,
- 'created_at' : p.created_at.isoformat()
- } for p in posts])
+ return jsonify([{
+ 'id': p.id,
+ 'title': p.title,
+ 'status': p.status, # 新增 status 字段
+ 'heat': p.heat,
+ 'created_at': p.created_at.isoformat()
+ } for p in posts])
@posts_bp.route('/<int:post_id>', methods=['GET'])
def get_post(post_id):
post = Post.query.get_or_404(post_id)
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()
+ '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()
})
-
@posts_bp.route('/<int:post_id>', methods=['PUT'])
def update_post(post_id):
"""
- 支持 FormData 和 JSON 两种格式更新:
- - multipart/form-data 时可上传新文件并保留 existing_media_urls
- - application/json 时只修改字段
+ 修改帖子字段(可选字段: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:
- title = request.form.get('title')
- content = request.form.get('content')
- status = request.form.get('status')
+ # FormData请求
+ title = request.form.get('title')
+ content = request.form.get('content')
+ status = request.form.get('status')
topic_id = request.form.get('topic_id')
- count = int(request.form.get('media_count', 0))
- existing = request.form.get('existing_media_urls')
-
+ 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:
+ if existing_media_urls_str:
try:
- existing_media_urls = json.loads(existing)
+ existing_media_urls = json.loads(existing_media_urls_str)
except:
existing_media_urls = None
-
+
+ # 获取新上传的文件
files = []
- for i in range(count):
- key = f'media_{i}'
- if key in request.files:
- files.append(request.files[key])
-
- updated = fpost.update_post_with_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 or None,
+ 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 field in ('title','content','topic_id','media_urls','status'):
- if field in data:
- setattr(post, field, data[field])
+ for key in ('title', 'content', 'topic_id', 'media_urls', 'status'):
+ if key in data:
+ setattr(post, key, data[key])
db.session.commit()
- updated = post
-
- if not updated:
+ updated_post = post
+
+ if not updated_post:
return jsonify({'error': '帖子不存在'}), 404
-
+
return '', 204
-
+
except Exception as e:
return jsonify({'error': str(e)}), 500
-
@posts_bp.route('/<int:post_id>', methods=['DELETE'])
def delete_post(post_id):
post = Post.query.get_or_404(post_id)
@@ -149,14 +156,89 @@
db.session.commit()
return '', 204
+@posts_bp.route('/<int:post_id>/<action>', methods=['POST'])
+def post_action(post_id, action):
+ """
+ 支持的 action: like, favorite, view, share
+ 对于 like 和 favorite,保证每个用户每帖只做一次。
+ """
+ if action not in ('like', 'favorite', 'view', 'share'):
+ abort(400, 'Invalid action')
-# —— 显式的 like/favorite 删除和查询路由,放在泛用 action 路由之前 —— #
+ data = request.get_json() or {}
+ user_id = data.get('user_id')
+ if not user_id:
+ abort(400, 'user_id required')
-@posts_bp.route('/<int:post_id>/like', methods=['GET'])
+ # 对 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:
+ 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()
+ return '', 201
+
+@posts_bp.route('/<int:post_id>/like', methods=['DELETE'])
+def unlike(post_id):
+ user_id = request.get_json(silent=True) and request.get_json().get('user_id')
+ if not user_id:
+ 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:
+ 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()
+ return '', 204
+
+@posts_bp.route('/<int:post_id>/favorite', methods=['DELETE'])
+def unfavorite(post_id):
+ user_id = request.get_json(silent=True) and request.get_json().get('user_id')
+ if not user_id:
+ 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:
+ 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()
+ return '', 204
+
+@posts_bp.route('/<int:post_id>/like/status', methods=['GET'])
def has_liked(post_id):
"""
- GET /posts/<post_id>/like?user_id=xx
- 返回 { "liked": true/false }
+ 检查指定 user_id 是否对 post_id 点过赞。
+ GET /posts/<post_id>/like/status?user_id=123
+ 返回 { "liked": true } 或 { "liked": false }
"""
user_id = request.args.get('user_id', type=int)
if not user_id:
@@ -169,85 +251,3 @@
).first() is not None
return jsonify({'liked': exists}), 200
-
-
-@posts_bp.route('/<int:post_id>/like', methods=['DELETE'])
-def unlike(post_id):
- data = request.get_json(silent=True) or {}
- user_id = data.get('user_id')
- if not user_id:
- abort(400, 'user_id required')
-
- beh = Behavior.query.filter_by(
- user_id=user_id,
- post_id=post_id,
- type='like'
- ).first()
- if not beh:
- 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()
- return '', 204
-
-
-@posts_bp.route('/<int:post_id>/favorite', methods=['DELETE'])
-def unfavorite(post_id):
- data = request.get_json(silent=True) or {}
- user_id = data.get('user_id')
- if not user_id:
- abort(400, 'user_id required')
-
- beh = Behavior.query.filter_by(
- user_id=user_id,
- post_id=post_id,
- type='favorite'
- ).first()
- if not beh:
- 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()
- return '', 204
-
-
-# —— 泛用 action 路由,仅处理 POST /posts/<id>/(like|favorite|view|share) —— #
-
-@posts_bp.route('/<int:post_id>/<action>', methods=['POST'])
-def post_action(post_id, action):
- """
- 支持 action: like, favorite, view, share,
- 对 like/favorite 做幂等去重检查。
- """
- 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:
- abort(400, 'user_id required')
-
- # 幂等检查
- if action in ('like','favorite'):
- exists = Behavior.query.filter_by(
- user_id=user_id,
- post_id=post_id,
- type=action
- ).first()
- if exists:
- 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()
- return '', 201
diff --git a/Merge/front/package.json b/Merge/front/package.json
index 5f6553c..ffb677d 100644
--- a/Merge/front/package.json
+++ b/Merge/front/package.json
@@ -3,31 +3,31 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@testing-library/dom": "^10.4.0",
"@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.0",
+ "@mui/icons-material": "^7.1.1",
+ "@mui/material": "^7.1.1",
+ "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
- "react": "^19.1.0",
- "react-dom": "^19.1.0",
- "react-router-dom": "^6.14.1",
- "react-scripts": "^5.0.1",
- "web-vitals": "^2.1.4",
- "lucide-react": "^0.468.0",
- "antd": "^4.24.0",
"ajv": "^8.0.0",
"ajv-keywords": "^5.0.0",
+ "antd": "^4.24.0",
+ "axios": "^1.9.0",
"crypto-js": "^4.2.0",
- "recharts": "^2.1.9",
+ "lucide-react": "^0.468.0",
"mui": "^0.0.1",
- "@mui/material": "^7.1.1",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
"react-icons": "^5.5.0",
- "@mui/icons-material": "^7.1.1",
- "@emotion/styled": "^11.14.0",
- "axios": "^1.9.0"
+ "react-router-dom": "^6.14.1",
+ "react-scripts": "^5.0.1",
+ "recharts": "^2.1.9",
+ "web-vitals": "^2.1.4"
},
"scripts": {
- "start": "react-scripts start",
+ "start": "cross-env HOST=0.0.0.0 PORT=3000 react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
@@ -49,5 +49,8 @@
"last 1 firefox version",
"last 1 safari version"
]
+ },
+ "devDependencies": {
+ "cross-env": "^7.0.3"
}
}
diff --git a/Merge/front/src/api/posts_api.js b/Merge/front/src/api/posts_api.js
index 06cf2c7..d42ede2 100644
--- a/Merge/front/src/api/posts_api.js
+++ b/Merge/front/src/api/posts_api.js
@@ -130,7 +130,14 @@
// 获取收藏列表
getFavorites: async (userId) => {
return await request(`${LJC_BASE_URL}/api/user/${userId}/favorites`)
- }
+ },
+
+ // 获取点赞状态
+ // GET /posts/:postId/like/status?user_id=123
+ hasLikedPost: async (postId, userId) => {
+ const url = `${WZY_BASE_URL}/posts/${postId}/like/status?user_id=${userId}`
+ return await request(url)
+ },
}
export default postsAPI
diff --git a/Merge/front/src/api/search_jwlll.js b/Merge/front/src/api/search_jwlll.js
index e18efa3..e75abb0 100644
--- a/Merge/front/src/api/search_jwlll.js
+++ b/Merge/front/src/api/search_jwlll.js
@@ -17,7 +17,25 @@
},
...options
})
- return await response.json()
+
+ // 检查响应状态码
+ if (!response.ok) {
+ let errorData
+ try {
+ errorData = await response.json()
+ } catch {
+ errorData = { error: `HTTP ${response.status}` }
+ }
+ throw new Error(errorData.error || `HTTP ${response.status}`)
+ }
+
+ // 处理空响应(如204 No Content)
+ const contentType = response.headers.get('content-type')
+ if (contentType && contentType.includes('application/json')) {
+ return await response.json()
+ } else {
+ return {} // 返回空对象而不是尝试解析JSON
+ }
} catch (error) {
console.error('API请求错误:', error)
throw error
@@ -62,8 +80,7 @@
// 获取帖子详情
getPostDetail: async (postId) => {
return await request(`${WZY_BASE_URL}/posts/${postId}`)
- },
- // 点赞帖子
+ }, // 点赞帖子
likePost: async (postId, userId) => {
return await request(`${WZY_BASE_URL}/posts/${postId}/like`, {
method: 'POST',
@@ -78,11 +95,10 @@
body: JSON.stringify({ user_id: userId })
})
},
-
// 查看是否点赞
hasLiked: async (postId, userId) => {
const res = await request(
- `${WZY_BASE_URL}/posts/${postId}/like?user_id=${userId}`,
+ `${WZY_BASE_URL}/posts/${postId}/like/status?user_id=${userId}`,
{
method: 'GET'
}
diff --git a/Merge/front/src/components/PostDetailJWLLL.jsx b/Merge/front/src/components/PostDetailJWLLL.jsx
index 01f64b5..417105a 100644
--- a/Merge/front/src/components/PostDetailJWLLL.jsx
+++ b/Merge/front/src/components/PostDetailJWLLL.jsx
@@ -8,6 +8,7 @@
import MediaPreview from './MediaPreview'
import '../style/PostDetail.css'
import dayjs from 'dayjs'
+import { followUser, unfollowUser } from '../api/api_ljc'
export default function PostDetail() {
const { id } = useParams()
@@ -22,16 +23,15 @@
const [newComment, setNewComment] = useState('')
const [showComments, setShowComments] = useState(false)
const [isFollowing, setIsFollowing] = useState(false)
+ const [followLoading, setFollowLoading] = useState(false)
const [authorInfo, setAuthorInfo] = useState(null)
const [previewImg, setPreviewImg] = useState(null)
const [commentUserMap, setCommentUserMap] = useState({}) // user_id: username
- const [commentUserAvatarMap, setCommentUserAvatarMap] = useState({}) // user_id: avatar
- // 获取当前用户ID
+ const [commentUserAvatarMap, setCommentUserAvatarMap] = useState({}) // user_id: avatar // 获取当前用户ID
const getCurrentUserId = () => {
const userInfo = getUserInfo()
return userInfo?.id || '3' // 如果未登录或无用户信息,使用默认值3
}
-
const fetchPostDetail = useCallback(async () => {
setLoading(true)
setError(null)
@@ -39,6 +39,16 @@
const data = await searchAPI.getPostDetail(id)
setPost(data)
setLikeCount(data.heat || 0)
+
+ // 检查当前用户是否已点赞
+ const currentUserId = getCurrentUserId()
+ try {
+ const hasLiked = await searchAPI.hasLiked(id, currentUserId)
+ setLiked(hasLiked)
+ } catch (error) {
+ console.error('检查点赞状态失败:', error)
+ setLiked(false) // 如果检查失败,默认为未点赞
+ }
} catch (error) {
console.error('获取帖子详情失败:', error)
setError('帖子不存在或已被删除')
@@ -124,26 +134,48 @@
fetchCommentUserAvatars(userIds)
}
}, [comments])
-
const handleBack = () => {
navigate(-1)
}
const handleLike = async () => {
+ const currentUserId = getCurrentUserId()
+ const originalLiked = liked
+ const originalLikeCount = likeCount
+
try {
- const currentUserId = getCurrentUserId()
+ // 先乐观更新UI,提供即时反馈
const newLiked = !liked
+ setLiked(newLiked)
+ setLikeCount(prev => newLiked ? prev + 1 : prev - 1)
+
+ // 调用后端API
if (newLiked) {
await searchAPI.likePost(id, currentUserId)
} else {
await searchAPI.unlikePost(id, currentUserId)
}
- setLiked(newLiked)
- setLikeCount(prev => newLiked ? prev + 1 : prev - 1)
+
+ // API调用成功,状态已经更新,无需再次设置
} catch (error) {
- console.error('点赞失败:', error)
- // 回滚状态
- setLiked(!liked)
- setLikeCount(prev => liked ? prev + 1 : prev - 1)
+ console.error('点赞操作失败:', error)
+
+ // 检查是否是可以忽略的错误
+ const errorMessage = error.message || error.toString()
+ const isIgnorableError = errorMessage.includes('already liked') ||
+ errorMessage.includes('not liked yet') ||
+ errorMessage.includes('already favorited') ||
+ errorMessage.includes('not favorited yet')
+
+ if (isIgnorableError) {
+ // 这些错误可以忽略,因为最终状态是正确的
+ console.log('忽略重复操作错误:', errorMessage)
+ return
+ }
+
+ // 发生真正的错误时回滚到原始状态
+ setLiked(originalLiked)
+ setLikeCount(originalLikeCount)
+ alert('操作失败,请重试')
}
}
@@ -182,22 +214,24 @@
}
// 关注后刷新关注状态
- const handleFollowChange = async (followed) => {
- setIsFollowing(followed)
- // 关注/取关后重新拉取一次关注状态,保证和数据库同步
- if (post && post.user_id) {
- try {
- const userInfo = getUserInfo()
- if (!userInfo?.id) return
- const res = await postsAPI.getUserFollowing(userInfo.id)
- if (Array.isArray(res)) {
- setIsFollowing(res.some(u => u.id === post.user_id))
- } else if (Array.isArray(res.following)) {
- setIsFollowing(res.following.some(u => u.id === post.user_id))
- }
- } catch {}
+ const handleFollowAction = async () => {
+ // 添加了加载状态和错误处理
+ setFollowLoading(true);
+ const currentUserId = getCurrentUserId()
+ try {
+ if (isFollowing) {
+ await unfollowUser(currentUserId, post.user_id);
+ } else {
+ await followUser(currentUserId, post.user_id);
}
+ setIsFollowing(!isFollowing);
+ } catch (error) {
+ console.error(isFollowing ? '取消关注失败' : '关注失败', error);
+ alert(`操作失败: ${error.message || '请重试'}`);
+ } finally {
+ setFollowLoading(false);
}
+};
if (loading) {
return (
@@ -285,12 +319,23 @@
</span>
{/* 关注按钮 */}
{post.user_id && (
- <FollowButton
- userId={post.user_id}
- isFollowing={isFollowing}
- onFollowChange={handleFollowChange}
- style={{marginLeft: 12}}
- />
+ <button
+ className={`follow-btn ${isFollowing ? 'following' : ''}`}
+ onClick={handleFollowAction}
+ disabled={followLoading}
+ style={{
+ marginLeft: '12px',
+ padding: '4px 12px',
+ borderRadius: '20px',
+ border: '1px solid #ccc',
+ background: isFollowing ? '#f0f0f0' : '#007bff',
+ color: isFollowing ? '#333' : 'white',
+ cursor: 'pointer',
+ fontSize: '14px'
+ }}
+ >
+ {followLoading ? '处理中...' : (isFollowing ? '已关注' : '关注')}
+ </button>
)}
</div>
</div>
diff --git "a/\346\216\250\350\215\220\347\256\227\346\263\225\346\216\250\347\220\206\350\277\207\347\250\213\347\244\272\344\276\213.md" "b/\346\216\250\350\215\220\347\256\227\346\263\225\346\216\250\347\220\206\350\277\207\347\250\213\347\244\272\344\276\213.md"
new file mode 100644
index 0000000..ef8fcac
--- /dev/null
+++ "b/\346\216\250\350\215\220\347\256\227\346\263\225\346\216\250\347\220\206\350\277\207\347\250\213\347\244\272\344\276\213.md"
@@ -0,0 +1,118 @@
+# 推荐算法推理过程(基于实际数据库数据)
+
+## 一、标签推荐算法
+
+### 步骤1:查用户兴趣标签
+- 查表:`user_tags`、`tags`
+- SQL:
+ ```sql
+ SELECT t.name
+ FROM user_tags ut
+ JOIN tags t ON ut.tag_id = t.id
+ WHERE ut.user_id = 1;
+ ```
+- 结果:用户1的兴趣标签为“科幻”“动画”“美食”“旅行”“穿搭”
+
+### 步骤2:查这些标签下的所有帖子
+- 查表:`post_tags`、`posts`、`tags`
+- SQL:
+ ```sql
+ SELECT p.id, p.title
+ FROM post_tags pt
+ JOIN posts p ON pt.post_id = p.id
+ JOIN tags t ON pt.tag_id = t.id
+ WHERE t.name IN ('科幻', '动画', '美食', '旅行', '穿搭')
+ AND p.status = 'published'
+ AND p.user_id <> 1;
+ ```
+- 结果:id=3(功夫熊猫)、id=25(Fifth Post)、id=29(Ninth Post)
+
+### 步骤3:查用户已互动过的帖子
+- 查表:`behaviors`
+- SQL:
+ ```sql
+ SELECT post_id FROM behaviors WHERE user_id = 1;
+ ```
+- 结果:1、2、3、21
+
+### 步骤4:排除已看过的内容,得到最终推荐
+- 查表:`post_tags`、`posts`、`tags`
+- SQL:
+ ```sql
+ SELECT DISTINCT p.id, p.title
+ FROM post_tags pt
+ JOIN posts p ON pt.post_id = p.id
+ JOIN tags t ON pt.tag_id = t.id
+ WHERE t.name IN ('科幻', '动画', '美食', '旅行', '穿搭')
+ AND p.status = 'published'
+ AND p.user_id <> 1
+ AND p.id NOT IN (1, 2, 3, 21);
+ ```
+- 结果:id=25(Fifth Post)、id=29(Ninth Post)
+
+### 结论
+最终标签推荐给用户1的内容是:Fifth Post、Ninth Post。
+
+---
+
+## 二、协同过滤推荐算法
+
+### 步骤1:查用户1有行为的帖子
+- 查表:`behaviors`
+- SQL:
+ ```sql
+ SELECT post_id FROM behaviors WHERE user_id = 1;
+ ```
+- 结果:1、2、3、21
+
+### 步骤2:查和用户1有重叠行为的其他用户
+- 查表:`behaviors`
+- SQL:
+ ```sql
+ SELECT DISTINCT b2.user_id, b2.post_id
+ FROM behaviors b1
+ JOIN behaviors b2 ON b1.post_id = b2.post_id
+ WHERE b1.user_id = 1 AND b2.user_id <> 1;
+ ```
+- 结果:用户2、3、4、5、33、36、38、39、43等
+
+### 步骤3:查这些相似用户还看过但用户1没看过的帖子
+- 查表:`behaviors`
+- SQL:
+ ```sql
+ SELECT DISTINCT post_id
+ FROM behaviors
+ WHERE user_id IN (2, 3, 4, 5, 33, 36, 38, 39, 43)
+ AND post_id NOT IN (1, 2, 3, 21);
+ ```
+- 结果:29、334、336、338、339
+
+### 步骤4:查这些帖子的详情
+- 查表:`posts`
+- SQL:
+ ```sql
+ SELECT id, title
+ FROM posts
+ WHERE id IN (29, 334, 336, 338, 339)
+ AND status = 'published';
+ ```
+- 结果:
+
+| id | title |
+|-----|--------------|
+| 29 | Ninth Post |
+| 334 | 测试草稿 |
+| 336 | 学生证背面 |
+| 338 | 论文截图1 |
+| 339 | api第二次作业 |
+
+### 结论
+最终协同过滤推荐给用户1的内容是:Ninth Post、测试草稿、学生证背面、论文截图1、api第二次作业。
+
+---
+
+## 总结
+- 标签推荐算法推荐给用户1的内容是:Fifth Post、Ninth Post
+- 协同过滤推荐算法推荐给用户1的内容是:Ninth Post、测试草稿、学生证背面、论文截图1、api第二次作业
+
+每一步都明确了查哪个表、查什么数据,推理过程完全基于你的实际数据库内容。
diff --git "a/\346\216\250\350\215\220\347\256\227\346\263\225\346\216\250\347\220\206\350\277\207\347\250\213\347\244\272\344\276\213.pdf" "b/\346\216\250\350\215\220\347\256\227\346\263\225\346\216\250\347\220\206\350\277\207\347\250\213\347\244\272\344\276\213.pdf"
new file mode 100644
index 0000000..654624f
--- /dev/null
+++ "b/\346\216\250\350\215\220\347\256\227\346\263\225\346\216\250\347\220\206\350\277\207\347\250\213\347\244\272\344\276\213.pdf"
Binary files differ