/** * Elasticsearch 服务层 * 用于价值论坛的帖子、评论存储和搜索 */ import axios from 'axios'; // Elasticsearch 配置 // 使用 Nginx 代理路径避免 Mixed Content 问题 const ES_CONFIG = { baseURL: process.env.NODE_ENV === 'production' ? '/es-api' // 生产环境使用 Nginx 代理 : 'http://222.128.1.157:19200', // 开发环境直连 timeout: 10000, headers: { 'Content-Type': 'application/json', }, }; // 创建 axios 实例 const esClient = axios.create(ES_CONFIG); // 索引名称 const INDICES = { POSTS: 'forum_posts', COMMENTS: 'forum_comments', EVENTS: 'forum_events', }; /** * 初始化索引(创建索引和映射) */ export const initializeIndices = async () => { try { // 创建帖子索引 await esClient.put(`/${INDICES.POSTS}`, { mappings: { properties: { id: { type: 'keyword' }, author_id: { type: 'keyword' }, author_name: { type: 'text' }, author_avatar: { type: 'keyword' }, title: { type: 'text', analyzer: 'ik_max_word' }, content: { type: 'text', analyzer: 'ik_max_word' }, images: { type: 'keyword' }, tags: { type: 'keyword' }, category: { type: 'keyword' }, likes_count: { type: 'integer' }, comments_count: { type: 'integer' }, views_count: { type: 'integer' }, created_at: { type: 'date' }, updated_at: { type: 'date' }, is_pinned: { type: 'boolean' }, status: { type: 'keyword' }, // active, deleted, hidden }, }, }); // 创建评论索引 await esClient.put(`/${INDICES.COMMENTS}`, { mappings: { properties: { id: { type: 'keyword' }, post_id: { type: 'keyword' }, author_id: { type: 'keyword' }, author_name: { type: 'text' }, author_avatar: { type: 'keyword' }, content: { type: 'text', analyzer: 'ik_max_word' }, parent_id: { type: 'keyword' }, // 用于嵌套评论 likes_count: { type: 'integer' }, created_at: { type: 'date' }, status: { type: 'keyword' }, }, }, }); // 创建事件时间轴索引 await esClient.put(`/${INDICES.EVENTS}`, { mappings: { properties: { id: { type: 'keyword' }, post_id: { type: 'keyword' }, event_type: { type: 'keyword' }, // news, price_change, announcement, etc. title: { type: 'text' }, description: { type: 'text', analyzer: 'ik_max_word' }, source: { type: 'keyword' }, source_url: { type: 'keyword' }, related_stocks: { type: 'keyword' }, occurred_at: { type: 'date' }, created_at: { type: 'date' }, importance: { type: 'keyword' }, // high, medium, low }, }, }); console.log('Elasticsearch 索引初始化成功'); } catch (error) { if (error.response?.status === 400 && error.response?.data?.error?.type === 'resource_already_exists_exception') { console.log('索引已存在,跳过创建'); } else { console.error('初始化索引失败:', error); throw error; } } }; // ==================== 帖子相关操作 ==================== /** * 创建新帖子 */ export const createPost = async (postData) => { try { const post = { id: `post_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ...postData, likes_count: 0, comments_count: 0, views_count: 0, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), is_pinned: false, status: 'active', }; const response = await esClient.post(`/${INDICES.POSTS}/_doc/${post.id}`, post); return { ...post, _id: response.data._id }; } catch (error) { console.error('创建帖子失败:', error); throw error; } }; /** * 获取帖子列表(支持分页、排序、筛选) */ export const getPosts = async ({ page = 1, size = 20, sort = 'created_at', order = 'desc', category = null, tags = [] }) => { try { const from = (page - 1) * size; const query = { bool: { must: [{ match: { status: 'active' } }], }, }; if (category) { query.bool.must.push({ term: { category } }); } if (tags.length > 0) { query.bool.must.push({ terms: { tags } }); } const response = await esClient.post(`/${INDICES.POSTS}/_search`, { from, size, query, sort: [ { is_pinned: { order: 'desc' } }, { [sort]: { order } }, ], }); return { total: response.data.hits.total.value, posts: response.data.hits.hits.map((hit) => ({ ...hit._source, _id: hit._id })), page, size, }; } catch (error) { console.error('获取帖子列表失败:', error); throw error; } }; /** * 获取单个帖子详情 */ export const getPostById = async (postId) => { try { const response = await esClient.get(`/${INDICES.POSTS}/_doc/${postId}`); // 增加浏览量 await esClient.post(`/${INDICES.POSTS}/_update/${postId}`, { script: { source: 'ctx._source.views_count += 1', lang: 'painless', }, }); return { ...response.data._source, _id: response.data._id }; } catch (error) { console.error('获取帖子详情失败:', error); throw error; } }; /** * 更新帖子 */ export const updatePost = async (postId, updateData) => { try { const response = await esClient.post(`/${INDICES.POSTS}/_update/${postId}`, { doc: { ...updateData, updated_at: new Date().toISOString(), }, }); return response.data; } catch (error) { console.error('更新帖子失败:', error); throw error; } }; /** * 删除帖子(软删除) */ export const deletePost = async (postId) => { try { await updatePost(postId, { status: 'deleted' }); } catch (error) { console.error('删除帖子失败:', error); throw error; } }; /** * 点赞帖子 */ export const likePost = async (postId) => { try { await esClient.post(`/${INDICES.POSTS}/_update/${postId}`, { script: { source: 'ctx._source.likes_count += 1', lang: 'painless', }, }); } catch (error) { console.error('点赞帖子失败:', error); throw error; } }; /** * 搜索帖子 */ export const searchPosts = async (keyword, { page = 1, size = 20 }) => { try { const from = (page - 1) * size; const response = await esClient.post(`/${INDICES.POSTS}/_search`, { from, size, query: { bool: { must: [ { multi_match: { query: keyword, fields: ['title^3', 'content', 'tags^2'], type: 'best_fields', }, }, { match: { status: 'active' } }, ], }, }, highlight: { fields: { title: {}, content: {}, }, }, }); return { total: response.data.hits.total.value, posts: response.data.hits.hits.map((hit) => ({ ...hit._source, _id: hit._id, highlight: hit.highlight, })), page, size, }; } catch (error) { console.error('搜索帖子失败:', error); throw error; } }; // ==================== 评论相关操作 ==================== /** * 创建评论 */ export const createComment = async (commentData) => { try { const comment = { id: `comment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ...commentData, likes_count: 0, created_at: new Date().toISOString(), status: 'active', }; const response = await esClient.post(`/${INDICES.COMMENTS}/_doc/${comment.id}`, comment); // 增加帖子评论数 await esClient.post(`/${INDICES.POSTS}/_update/${commentData.post_id}`, { script: { source: 'ctx._source.comments_count += 1', lang: 'painless', }, }); return { ...comment, _id: response.data._id }; } catch (error) { console.error('创建评论失败:', error); throw error; } }; /** * 获取帖子的评论列表 */ export const getCommentsByPostId = async (postId, { page = 1, size = 50 }) => { try { const from = (page - 1) * size; const response = await esClient.post(`/${INDICES.COMMENTS}/_search`, { from, size, query: { bool: { must: [ { term: { post_id: postId } }, { match: { status: 'active' } }, ], }, }, sort: [{ created_at: { order: 'asc' } }], }); return { total: response.data.hits.total.value, comments: response.data.hits.hits.map((hit) => ({ ...hit._source, _id: hit._id })), }; } catch (error) { console.error('获取评论列表失败:', error); throw error; } }; /** * 点赞评论 */ export const likeComment = async (commentId) => { try { await esClient.post(`/${INDICES.COMMENTS}/_update/${commentId}`, { script: { source: 'ctx._source.likes_count += 1', lang: 'painless', }, }); } catch (error) { console.error('点赞评论失败:', error); throw error; } }; // ==================== 事件时间轴相关操作 ==================== /** * 创建事件 */ export const createEvent = async (eventData) => { try { const event = { id: `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ...eventData, created_at: new Date().toISOString(), }; const response = await esClient.post(`/${INDICES.EVENTS}/_doc/${event.id}`, event); return { ...event, _id: response.data._id }; } catch (error) { console.error('创建事件失败:', error); throw error; } }; /** * 获取帖子的事件时间轴 */ export const getEventsByPostId = async (postId) => { try { const response = await esClient.post(`/${INDICES.EVENTS}/_search`, { size: 100, query: { term: { post_id: postId }, }, sort: [{ occurred_at: { order: 'desc' } }], }); return response.data.hits.hits.map((hit) => ({ ...hit._source, _id: hit._id })); } catch (error) { console.error('获取事件时间轴失败:', error); throw error; } }; export default { initializeIndices, // 帖子操作 createPost, getPosts, getPostById, updatePost, deletePost, likePost, searchPosts, // 评论操作 createComment, getCommentsByPostId, likeComment, // 事件操作 createEvent, getEventsByPostId, };