修改信息

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