纯前端修改点赞问题。
Change-Id: I99522d6492beb310ec048ff1191349c4a4436e09
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/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 b0ca84f..47f905f 100644
--- a/Merge/front/src/components/PostDetailJWLLL.jsx
+++ b/Merge/front/src/components/PostDetailJWLLL.jsx
@@ -1,18 +1,10 @@
-import React, { useState, useEffect, useCallback, useMemo } from 'react'
+import React, { useState, useEffect, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
-import {
- ArrowLeft,
- ThumbsUp,
- MessageCircle,
- Share2,
- BookmarkPlus,
- Heart,
- Eye,
-} from 'lucide-react'
+import { ArrowLeft, ThumbsUp, MessageCircle, Share2, BookmarkPlus, Heart, Eye } from 'lucide-react'
import { searchAPI } from '../api/search_jwlll'
import { getUserInfo } from '../utils/auth'
import FollowButton from './FollowButton'
-import postsAPI from '../api/posts_api' // ⇦ 包含 hasLikedPost、likePost、unlikePost
+import postsAPI from '../api/posts_api'
import MediaPreview from './MediaPreview'
import '../style/PostDetail.css'
import dayjs from 'dayjs'
@@ -20,34 +12,24 @@
export default function PostDetail() {
const { id } = useParams()
const navigate = useNavigate()
-
- // ──────────────── 状态 ────────────────
const [post, setPost] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
-
const [liked, setLiked] = useState(false)
const [bookmarked, setBookmarked] = useState(false)
const [likeCount, setLikeCount] = useState(0)
-
const [comments, setComments] = useState([])
const [newComment, setNewComment] = useState('')
const [showComments, setShowComments] = useState(false)
-
const [isFollowing, setIsFollowing] = 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(memo 化,组件整个生命周期只算一次)
- const currentUserId = useMemo(() => {
- const ui = getUserInfo()
- return ui?.id || 'null' // 未登录就给个默认值 3
- }, [])
-
- // ──────────────── 拉取帖子详情 ────────────────
+ const [commentUserMap, setCommentUserMap] = useState({}) // user_id: username
+ 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)
@@ -55,151 +37,153 @@
const data = await searchAPI.getPostDetail(id)
setPost(data)
setLikeCount(data.heat || 0)
- } catch (err) {
- console.error('获取帖子详情失败:', err)
+
+ // 检查当前用户是否已点赞
+ 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('帖子不存在或已被删除')
} finally {
setLoading(false)
}
}, [id])
- // ──────────────── 拉取评论 ────────────────
const fetchComments = useCallback(async () => {
try {
const data = await searchAPI.getComments(id)
setComments(data.comments || [])
- } catch (err) {
- console.error('获取评论失败:', err)
+ } catch (error) {
+ console.error('获取评论失败:', error)
}
}, [id])
- // ──────────────── 组件挂载:帖子详情 + 评论 + 点赞状态 ────────────────
- useEffect(() => {
- fetchPostDetail()
- fetchComments()
-
- // 检查我是否点过赞
- if (currentUserId) {
- ;(async () => {
- try {
- const res = await postsAPI.hasLikedPost(id, currentUserId)
- setLiked(!!res.liked)
- } catch (err) {
- console.error('检查点赞状态失败:', err)
- }
- })()
- }
- }, [fetchPostDetail, fetchComments, id, currentUserId])
-
- // ──────────────── 检查是否关注作者 ────────────────
+ // 检查当前用户是否已关注发帖人
useEffect(() => {
if (post && post.user_id) {
+ // 这里假设有API postsAPI.getUserFollowing
const checkFollow = async () => {
try {
- if (!currentUserId) return
- const res = await postsAPI.getUserFollowing(currentUserId)
+ 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))
+ 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))
+ setIsFollowing(res.following.some(u => u.id === post.user_id))
}
} catch {}
}
checkFollow()
}
- }, [post, currentUserId])
+ }, [post])
- // ──────────────── 作者信息 ────────────────
+ // 拉取发帖人信息
useEffect(() => {
if (post && post.user_id) {
- postsAPI
- .getUser(post.user_id)
- .then((res) => setAuthorInfo(res || {}))
- .catch(() => setAuthorInfo({}))
+ postsAPI.getUser(post.user_id).then(res => setAuthorInfo(res || {})).catch(() => setAuthorInfo({}))
}
}, [post])
- // ──────────────── 拉取评论用户昵称 / 头像 ────────────────
+ // 拉取所有评论用户昵称
const fetchCommentUserNames = async (userIds) => {
const map = {}
- await Promise.all(
- userIds.map(async (uid) => {
- try {
- const user = await postsAPI.getUser(uid)
- map[uid] = user.username || user.nickname || `用户${uid}`
- } catch {
- map[uid] = `用户${uid}`
- }
- })
- )
+ await Promise.all(userIds.map(async uid => {
+ try {
+ const user = await postsAPI.getUser(uid)
+ map[uid] = user.username || user.nickname || `用户${uid}`
+ } catch {
+ map[uid] = `用户${uid}`
+ }
+ }))
setCommentUserMap(map)
}
+ // 拉取所有评论用户头像
const fetchCommentUserAvatars = async (userIds) => {
const map = {}
- await Promise.all(
- userIds.map(async (uid) => {
- try {
- const user = await postsAPI.getUser(uid)
- map[uid] =
- user.avatar && user.avatar.startsWith('http')
- ? user.avatar
- : user.avatar
- ? `http://10.126.59.25:5715/static/${user.avatar}`
- : `https://i.pravatar.cc/40?img=${uid}`
- } catch {
- map[uid] = `https://i.pravatar.cc/40?img=${uid}`
- }
- })
- )
+ await Promise.all(userIds.map(async uid => {
+ try {
+ const user = await postsAPI.getUser(uid)
+ map[uid] = user.avatar && user.avatar.startsWith('http') ? user.avatar : (user.avatar ? `http://10.126.59.25:5715/static/${user.avatar}` : `https://i.pravatar.cc/40?img=${uid}`)
+ } catch {
+ map[uid] = `https://i.pravatar.cc/40?img=${uid}`
+ }
+ }))
setCommentUserAvatarMap(map)
}
useEffect(() => {
+ fetchPostDetail()
+ fetchComments()
+ }, [fetchPostDetail, fetchComments])
+
+ // 评论区用户昵称和头像拉取
+ useEffect(() => {
if (comments.length > 0) {
- const uidSet = new Set(comments.map((c) => c.user_id).filter(Boolean))
- const ids = [...uidSet]
- fetchCommentUserNames(ids)
- fetchCommentUserAvatars(ids)
+ const userIds = [...new Set(comments.map(c => c.user_id).filter(Boolean))]
+ fetchCommentUserNames(userIds)
+ fetchCommentUserAvatars(userIds)
}
}, [comments])
-
- // ──────────────── 交互 handlers ────────────────
- const handleBack = () => navigate(-1)
-
+ const handleBack = () => {
+ navigate(-1)
+ }
const handleLike = async () => {
- if (!currentUserId) {
- alert('请先登录~')
- return
- }
-
+ const currentUserId = getCurrentUserId()
+ const originalLiked = liked
+ const originalLikeCount = likeCount
+
try {
- if (liked) {
- // 取消点赞
- await postsAPI.unlikePost(id, currentUserId)
+ // 先乐观更新UI,提供即时反馈
+ const newLiked = !liked
+ setLiked(newLiked)
+ setLikeCount(prev => newLiked ? prev + 1 : prev - 1)
+
+ // 调用后端API
+ if (newLiked) {
+ await searchAPI.likePost(id, currentUserId)
} else {
- // 点赞
- await postsAPI.likePost(id, currentUserId)
+ await searchAPI.unlikePost(id, currentUserId)
}
-
- // —— 关键:操作成功后重新拉一次帖子详情(里面会 setLikeCount) ——
- await fetchPostDetail()
-
- // —— 同步一下 liked 状态(可选,因为你直接用 fetchPostDetail 重置 likeCount,也可以重新查一下状态) ——
- const { liked: has } = await postsAPI.hasLikedPost(id, currentUserId)
- setLiked(!!has)
-
- } catch (err) {
- console.error('点赞操作失败:', err)
+
+ // API调用成功,状态已经更新,无需再次设置
+ } catch (error) {
+ 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('操作失败,请重试')
}
}
const handleBookmark = () => {
setBookmarked(!bookmarked)
- // TODO: 调后端保存收藏状态
+ // 实际项目中这里应该调用后端API保存收藏状态
}
const handleShare = () => {
+ // 分享功能
if (navigator.share) {
navigator.share({
title: post?.title,
@@ -207,27 +191,44 @@
url: window.location.href,
})
} else {
+ // 复制链接到剪贴板
navigator.clipboard.writeText(window.location.href)
alert('链接已复制到剪贴板')
}
}
-
const handleAddComment = async (e) => {
e.preventDefault()
if (!newComment.trim()) return
+
try {
+ const currentUserId = getCurrentUserId()
await searchAPI.addComment(id, currentUserId, newComment)
setNewComment('')
- fetchComments()
- } catch (err) {
- console.error('添加评论失败:', err)
+ fetchComments() // 刷新评论列表
+ } catch (error) {
+ console.error('添加评论失败:', error)
alert('评论失败,请重试')
}
}
- const handleFollowChange = (followed) => setIsFollowing(followed)
+ // 关注后刷新关注状态
+ 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 {}
+ }
+ }
- // ──────────────── 渲染逻辑 ────────────────
if (loading) {
return (
<div className="post-detail">
@@ -239,6 +240,7 @@
)
}
+ // 优化错误和不存在的判断逻辑
if (error) {
return (
<div className="post-detail">
@@ -254,6 +256,7 @@
)
}
+ // 只有明确为 null 或 undefined 时才显示不存在
if (post === null || post === undefined) {
return (
<div className="post-detail">
@@ -281,8 +284,8 @@
<button onClick={handleShare} className="action-btn">
<Share2 size={20} />
</button>
- <button
- onClick={handleBookmark}
+ <button
+ onClick={handleBookmark}
className={`action-btn ${bookmarked ? 'active' : ''}`}
>
<BookmarkPlus size={20} />
@@ -292,56 +295,31 @@
{/* 主要内容区 */}
<main className="post-content">
- {/* 标题 */}
+ {/* 帖子标题 */}
<h1 className="post-title">{post.title}</h1>
- {/* 作者 & 元数据 */}
+ {/* 作者信息和元数据 */}
<div className="post-meta">
<div className="author-info">
<div className="avatar">
- {authorInfo?.avatar && authorInfo.avatar.startsWith('http') ? (
- <img
- className="avatar"
- src={authorInfo.avatar}
- alt={
- authorInfo.username ||
- authorInfo.nickname ||
- post.author ||
- '用户'
- }
- />
+ {authorInfo && authorInfo.avatar && authorInfo.avatar.startsWith('http') ? (
+ <img className="avatar" src={authorInfo.avatar} alt={authorInfo.username || authorInfo.nickname || post.author || '用户'} />
) : (
- <img
- className="avatar"
- src={`https://i.pravatar.cc/40?img=${post.user_id}`}
- alt={
- authorInfo?.username ||
- authorInfo?.nickname ||
- post.author ||
- '用户'
- }
- />
+ <img className="avatar" src={`https://i.pravatar.cc/40?img=${post.user_id}`} alt={authorInfo?.username || authorInfo?.nickname || post.author || '用户'} />
)}
</div>
<div className="author-details">
- <span className="author-name">
- {authorInfo?.username ||
- authorInfo?.nickname ||
- post.author ||
- '匿名用户'}
- </span>
+ <span className="author-name">{authorInfo?.username || authorInfo?.nickname || post.author || '匿名用户'}</span>
<span className="post-date">
- {post.created_at
- ? dayjs(post.created_at).format('YYYY-MM-DD HH:mm:ss')
- : '未知时间'}
+ {post.created_at ? dayjs(post.created_at).format('YYYY-MM-DD HH:mm:ss') : '未知时间'}
</span>
-
+ {/* 关注按钮 */}
{post.user_id && (
<FollowButton
userId={post.user_id}
isFollowing={isFollowing}
onFollowChange={handleFollowChange}
- style={{ marginLeft: 12 }}
+ style={{marginLeft: 12}}
/>
)}
</div>
@@ -359,40 +337,25 @@
</div>
{/* 标签 */}
- {post.tags?.length > 0 && (
+ {post.tags && post.tags.length > 0 && (
<div className="post-tags">
- {post.tags.map((tag, idx) => (
- <span key={idx} className="tag">
- {tag}
- </span>
+ {post.tags.map((tag, index) => (
+ <span key={index} className="tag">{tag}</span>
))}
</div>
)}
- {/* 媒体 */}
+ {/* 帖子媒体(支持多图/多视频) */}
{Array.isArray(post.media_urls) && post.media_urls.length > 0 && (
- <div
- className="post-media"
- style={{
- display: 'flex',
- gap: 8,
- marginBottom: 16,
- flexWrap: 'wrap',
- }}
- >
+ <div className="post-media" style={{display:'flex',gap:8,marginBottom:16,flexWrap:'wrap'}}>
{post.media_urls.map((url, idx) => (
<MediaPreview
key={idx}
url={url}
- alt={`媒体${idx + 1}`}
+ alt={`媒体${idx+1}`}
onClick={(mediaUrl) => {
- // 图片点击预览
- const lower = mediaUrl.toLowerCase()
- if (
- !lower.includes('video') &&
- !lower.endsWith('.mp4') &&
- !lower.endsWith('.webm')
- ) {
+ // 对于图片,显示预览
+ if (!mediaUrl.toLowerCase().includes('video') && !mediaUrl.includes('.mp4') && !mediaUrl.includes('.webm')) {
setPreviewImg(mediaUrl)
}
}}
@@ -403,42 +366,19 @@
))}
</div>
)}
+ {/* 大图预览弹窗 */}
{previewImg && (
- <div
- className="img-preview-mask"
- style={{
- position: 'fixed',
- zIndex: 9999,
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- background: 'rgba(0,0,0,0.7)',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- }}
- onClick={() => setPreviewImg(null)}
- >
- <img
- src={previewImg}
- alt="大图预览"
- style={{
- maxWidth: '90vw',
- maxHeight: '90vh',
- borderRadius: 12,
- boxShadow: '0 4px 24px #0008',
- }}
- />
+ <div className="img-preview-mask" style={{position:'fixed',zIndex:9999,top:0,left:0,right:0,bottom:0,background:'rgba(0,0,0,0.7)',display:'flex',alignItems:'center',justifyContent:'center'}} onClick={()=>setPreviewImg(null)}>
+ <img src={previewImg} alt="大图预览" style={{maxWidth:'90vw',maxHeight:'90vh',borderRadius:12,boxShadow:'0 4px 24px #0008'}} />
</div>
)}
- {/* 正文 */}
+ {/* 帖子正文 */}
<div className="post-body">
<p>{post.content}</p>
</div>
- {/* 分类 / 类型 */}
+ {/* 类别信息 */}
{(post.category || post.type) && (
<div className="post-category">
{post.category && (
@@ -449,9 +389,7 @@
)}
{post.type && (
<>
- <span className="category-label" style={{ marginLeft: '1em' }}>
- 类型:
- </span>
+ <span className="category-label" style={{marginLeft: '1em'}}>类型:</span>
<span className="category-name">{post.type}</span>
</>
)}
@@ -461,7 +399,7 @@
{/* 评论区 */}
<div className="comments-section">
<div className="comments-header">
- <button
+ <button
onClick={() => setShowComments(!showComments)}
className="comments-toggle"
>
@@ -472,7 +410,7 @@
{showComments && (
<div className="comments-content">
- {/* 新评论 */}
+ {/* 添加评论 */}
<form onSubmit={handleAddComment} className="comment-form">
<textarea
value={newComment}
@@ -491,37 +429,20 @@
{comments.length === 0 ? (
<p className="no-comments">暂无评论</p>
) : (
- comments.map((comment, idx) => (
- <div key={idx} className="comment-item">
+ comments.map((comment, index) => (
+ <div key={index} className="comment-item">
<div className="comment-author">
<div className="comment-avatar">
- <img
- className="avatar"
- src={
- commentUserAvatarMap[comment.user_id] ||
- `https://i.pravatar.cc/40?img=${comment.user_id}`
- }
- alt={
- commentUserMap[comment.user_id] ||
- comment.user_name ||
- '用户'
- }
- />
+ <img className="avatar" src={commentUserAvatarMap[comment.user_id] || `https://i.pravatar.cc/40?img=${comment.user_id}`} alt={commentUserMap[comment.user_id] || comment.user_name || '用户'} />
</div>
- <span className="comment-name">
- {commentUserMap[comment.user_id] ||
- comment.user_name ||
- '匿名用户'}
- </span>
+ <span className="comment-name">{commentUserMap[comment.user_id] || comment.user_name || '匿名用户'}</span>
<span className="comment-time">
- {comment.create_time
- ? new Date(comment.create_time).toLocaleString(
- 'zh-CN'
- )
- : ''}
+ {comment.create_time ? new Date(comment.create_time).toLocaleString('zh-CN') : ''}
</span>
</div>
- <div className="comment-content">{comment.content}</div>
+ <div className="comment-content">
+ {comment.content}
+ </div>
</div>
))
)}
@@ -534,29 +455,29 @@
{/* 底部操作栏 */}
<footer className="post-footer">
<div className="action-bar">
- <button
- onClick={handleLike}
+ <button
+ onClick={handleLike}
className={`action-button ${liked ? 'liked' : ''}`}
>
<ThumbsUp size={20} />
<span>{likeCount}</span>
</button>
-
- <button
+
+ <button
onClick={() => setShowComments(!showComments)}
className="action-button"
>
<MessageCircle size={20} />
<span>评论</span>
</button>
-
+
<button onClick={handleShare} className="action-button">
<Share2 size={20} />
<span>分享</span>
</button>
-
- <button
- onClick={handleBookmark}
+
+ <button
+ onClick={handleBookmark}
className={`action-button ${bookmarked ? 'bookmarked' : ''}`}
>
<BookmarkPlus size={20} />
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