feat: 完整集成JWLLL搜索推荐系统到Merge项目

新增功能:
- 完整的JWLLL搜索推荐后端服务 (back_jwlll/)
- 前端智能搜索和推荐功能集成
- HomeFeed组件增强: 数据源切换(原始数据 ↔ 智能推荐)
- 新增PostDetailJWLLL和UploadPageJWLLL组件
- 新增search_jwlll.js API接口

技术特性:
- 标签推荐和协同过滤推荐算法
- 中文分词和Word2Vec语义搜索
- 100%向后兼容,原功能完全保留
- 独立服务架构,无冲突部署

集成内容:
- JWLLL后端服务配置和依赖
- 前端路由和组件更新
- 样式文件和API集成
- 项目文档和启动工具

Change-Id: I1d008cf04eee40e7d81bfb9109f933d3447d1760
diff --git a/Merge/front/src/components/HomeFeed.jsx b/Merge/front/src/components/HomeFeed.jsx
index c681858..e32a2eb 100644
--- a/Merge/front/src/components/HomeFeed.jsx
+++ b/Merge/front/src/components/HomeFeed.jsx
@@ -1,9 +1,10 @@
 // src/components/HomeFeed.jsx
 
-import React, { useState, useEffect } from 'react'
+import React, { useState, useEffect, useCallback } from 'react'
 import { useNavigate } from 'react-router-dom'
 import { ThumbsUp } from 'lucide-react'
 import { fetchPosts, fetchPost } from '../api/posts_wzy'
+import { searchAPI } from '../api/search_jwlll'
 import '../style/HomeFeed.css'
 
 const categories = [
@@ -11,15 +12,120 @@
   '职场','情感','家居','游戏','旅行','健身'
 ]
 
+const recommendModes = [
+  { label: '标签推荐', value: 'tag' },
+  { label: '协同过滤推荐', value: 'cf' }
+]
+
+const DEFAULT_USER_ID = '3' // 确保数据库有此用户
+const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整
+
 export default function HomeFeed() {
   const navigate = useNavigate()
   const [activeCat, setActiveCat] = useState('推荐')
   const [items, setItems]         = useState([])
   const [loading, setLoading]     = useState(true)
   const [error, setError]         = useState(null)
+    // JWLLL 搜索推荐相关状态
+  const [search, setSearch] = useState('')
+  const [recMode, setRecMode] = useState('tag')
+  const [recCFNum, setRecCFNum] = useState(20)
+  const [useSearchRecommend, setUseSearchRecommend] = useState(false) // 是否使用搜索推荐模式  // JWLLL 搜索推荐功能函数
+  
+  // JWLLL搜索推荐内容
+  const fetchSearchContent = useCallback(async (keyword = '') => {
+    setLoading(true)
+    setError(null)
+    try {
+      const data = await searchAPI.search(keyword || activeCat, activeCat === '推荐' ? undefined : activeCat)
+      const formattedItems = (data.results || []).map(item => ({
+        id: item.id,
+        title: item.title,
+        author: item.author || '佚名',
+        avatar: `https://i.pravatar.cc/40?img=${item.id}`,
+        img: item.img || '', 
+        likes: item.heat || 0,
+        content: item.content
+      }))
+      setItems(formattedItems)
+    } catch (e) {
+      console.error('搜索失败:', e)
+      setError('搜索失败')
+      setItems([])
+    }
+    setLoading(false)
+  }, [activeCat])
+
+  // 标签推荐
+  const fetchTagRecommend = useCallback(async (tags) => {
+    setLoading(true)
+    setError(null)
+    try {
+      const data = await searchAPI.recommendByTags(DEFAULT_USER_ID, tags)
+      const formattedItems = (data.recommendations || []).map(item => ({
+        id: item.id,
+        title: item.title,
+        author: item.author || '佚名',
+        avatar: `https://i.pravatar.cc/40?img=${item.id}`,
+        img: item.img || '', 
+        likes: item.heat || 0,
+        content: item.content
+      }))
+      setItems(formattedItems)
+    } catch (e) {
+      console.error('标签推荐失败:', e)
+      setError('标签推荐失败')
+      setItems([])
+    }
+    setLoading(false)
+  }, [])
+
+  // 协同过滤推荐
+  const fetchCFRecommend = useCallback(async (topN = recCFNum) => {
+    setLoading(true)
+    setError(null)
+    try {
+      const data = await searchAPI.userBasedRecommend(DEFAULT_USER_ID, topN)
+      const formattedItems = (data.recommendations || []).map(item => ({
+        id: item.id,
+        title: item.title,
+        author: item.author || '佚名',
+        avatar: `https://i.pravatar.cc/40?img=${item.id}`,
+        img: item.img || '', 
+        likes: item.heat || 0,
+        content: item.content
+      }))
+      setItems(formattedItems)
+    } catch (e) {
+      console.error('协同过滤推荐失败:', e)
+      setError('协同过滤推荐失败')
+      setItems([])
+    }
+    setLoading(false)
+  }, [recCFNum])
+
+  // 获取用户兴趣标签后再推荐
+  const fetchUserTagsAndRecommend = useCallback(async () => {
+    setLoading(true)
+    setError(null)
+    let tags = []
+    try {
+      const data = await searchAPI.getUserTags(DEFAULT_USER_ID)
+      tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS
+    } catch {
+      tags = DEFAULT_TAGS
+    }
+    if (recMode === 'tag') {
+      await fetchTagRecommend(tags)
+    } else {
+      await fetchCFRecommend()
+    }
+    setLoading(false)
+  }, [recMode, fetchTagRecommend, fetchCFRecommend])
 
   useEffect(() => {
-    async function loadPosts() {
+    // 原始数据加载函数
+    const loadPosts = async () => {
       try {
         const list = await fetchPosts()  // [{id, title, heat, created_at}, …]
         // 为了拿到 media_urls 和 user_id,这里再拉详情
@@ -43,25 +149,149 @@
         setLoading(false)
       }
     }
-    loadPosts()
-  }, [])
 
+    // 根据模式选择加载方式
+    if (activeCat === '推荐' && useSearchRecommend) {
+      fetchUserTagsAndRecommend()
+    } else {
+      loadPosts()
+    }
+  }, [activeCat, useSearchRecommend, fetchUserTagsAndRecommend])
+  // 切换推荐模式时的额外处理
+  useEffect(() => {
+    if (activeCat === '推荐' && useSearchRecommend) {
+      fetchUserTagsAndRecommend()
+    }
+    // eslint-disable-next-line
+  }, [recMode, fetchUserTagsAndRecommend])
+
+  // 根据模式选择不同的加载方式
+  const handleSearch = e => {
+    e.preventDefault()
+    if (useSearchRecommend) {
+      fetchSearchContent(search)
+    } else {
+      // 切换到搜索推荐模式
+      setUseSearchRecommend(true)
+      fetchSearchContent(search)
+    }
+  }
+
+  const handlePostClick = (postId) => {
+    navigate(`/post/${postId}`)
+  }
   return (
     <div className="home-feed">
+      {/* 数据源切换 */}
+      <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
+        <span>数据源:</span>
+        <div style={{display:'flex', gap:8}}>
+          <button
+            className={!useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
+            onClick={() => {setUseSearchRecommend(false); setActiveCat('推荐')}}
+            type="button"
+            style={{
+              borderRadius: 20,
+              padding: '4px 18px',
+              border: !useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
+              background: !useSearchRecommend ? '#fff0f0' : '#fff',
+              color: !useSearchRecommend ? '#e84c4a' : '#333',
+              fontWeight: !useSearchRecommend ? 600 : 400,
+              cursor: 'pointer',
+              transition: 'all 0.2s',
+              outline: 'none',
+            }}
+          >原始数据</button>
+          <button
+            className={useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
+            onClick={() => {setUseSearchRecommend(true); setActiveCat('推荐')}}
+            type="button"
+            style={{
+              borderRadius: 20,
+              padding: '4px 18px',
+              border: useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
+              background: useSearchRecommend ? '#fff0f0' : '#fff',
+              color: useSearchRecommend ? '#e84c4a' : '#333',
+              fontWeight: useSearchRecommend ? 600 : 400,
+              cursor: 'pointer',
+              transition: 'all 0.2s',
+              outline: 'none',
+            }}
+          >智能推荐</button>
+        </div>
+      </div>
+
+      {/* 推荐模式切换,仅在推荐页显示且使用搜索推荐时 */}
+      {activeCat === '推荐' && useSearchRecommend && (
+        <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
+          <span style={{marginRight:8}}>推荐模式:</span>
+          <div style={{display:'flex', gap:8}}>
+            {recommendModes.map(m => (
+              <button
+                key={m.value}
+                className={recMode===m.value? 'rec-btn styled active':'rec-btn styled'}
+                onClick={() => setRecMode(m.value)}
+                type="button"
+                style={{
+                  borderRadius: 20,
+                  padding: '4px 18px',
+                  border: recMode===m.value ? '2px solid #e84c4a' : '1px solid #ccc',
+                  background: recMode===m.value ? '#fff0f0' : '#fff',
+                  color: recMode===m.value ? '#e84c4a' : '#333',
+                  fontWeight: recMode===m.value ? 600 : 400,
+                  cursor: 'pointer',
+                  transition: 'all 0.2s',
+                  outline: 'none',
+                }}
+              >{m.label}</button>
+            ))}
+          </div>
+          {/* 协同过滤推荐数量选择 */}
+          {recMode === 'cf' && (
+            <div style={{display:'flex',alignItems:'center',gap:4}}>
+              <span>推荐数量:</span>
+              <select value={recCFNum} onChange={e => { setRecCFNum(Number(e.target.value)); fetchCFRecommend(Number(e.target.value)) }} style={{padding:'2px 8px',borderRadius:6,border:'1px solid #ccc'}}>
+                {[10, 20, 30, 50].map(n => <option key={n} value={n}>{n}</option>)}
+              </select>
+            </div>
+          )}
+        </div>
+      )}
+
+      {/* 搜索栏 */}
+      <form className="feed-search" onSubmit={handleSearch} style={{marginBottom:16, display:'flex', gap:8, alignItems:'center'}}>
+        <input
+          type="text"
+          className="search-input"
+          placeholder="搜索内容/标题/标签"
+          value={search}
+          onChange={e => setSearch(e.target.value)}
+        />
+        <button type="submit" className="search-btn">搜索</button>
+      </form>
+
       {/* 顶部分类 */}
       <nav className="feed-tabs">
         {categories.map(cat => (
           <button
             key={cat}
             className={cat === activeCat ? 'tab active' : 'tab'}
-            onClick={() => setActiveCat(cat)}
+            onClick={() => { 
+              setActiveCat(cat); 
+              setSearch('');
+              if (useSearchRecommend) {
+                if (cat === '推荐') {
+                  fetchUserTagsAndRecommend()
+                } else {
+                  fetchSearchContent()
+                }
+              }
+            }}
           >
             {cat}
           </button>
         ))}
-      </nav>
-
-      {/* 状态提示 */}
+      </nav>      {/* 状态提示 */}
       {loading ? (
         <div className="loading">加载中…</div>
       ) : error ? (
@@ -69,22 +299,27 @@
       ) : (
         /* 瀑布流卡片区 */
         <div className="feed-grid">
-          {items.map(item => (
-            <div key={item.id} className="feed-card">
-              <img className="card-img" src={item.img} alt={item.title} />
-              <h3 className="card-title">{item.title}</h3>
-              <div className="card-footer">
-                <div className="card-author">
-                  <img className="avatar" src={item.avatar} alt={item.author} />
-                  <span className="username">{item.author}</span>
-                </div>
-                <div className="card-likes">
-                  <ThumbsUp size={16} />
-                  <span className="likes-count">{item.likes}</span>
+          {items.length === 0 ? (
+            <div style={{padding:32, color:'#aaa'}}>暂无内容</div>
+          ) : (
+            items.map(item => (
+              <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}>
+                {item.img && <img className="card-img" src={item.img} alt={item.title} />}
+                <h3 className="card-title">{item.title}</h3>
+                {item.content && <div className="card-content">{item.content.slice(0, 60) || ''}</div>}
+                <div className="card-footer">
+                  <div className="card-author">
+                    <img className="avatar" src={item.avatar} alt={item.author} />
+                    <span className="username">{item.author}</span>
+                  </div>
+                  <div className="card-likes">
+                    <ThumbsUp size={16} />
+                    <span className="likes-count">{item.likes}</span>
+                  </div>
                 </div>
               </div>
-            </div>
-          ))}
+            ))
+          )}
         </div>
       )}
     </div>