add forum
This commit is contained in:
429
src/services/elasticsearchService.js
Normal file
429
src/services/elasticsearchService.js
Normal file
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* Elasticsearch 服务层
|
||||
* 用于价值论坛的帖子、评论存储和搜索
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
// Elasticsearch 配置
|
||||
const ES_CONFIG = {
|
||||
baseURL: '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,
|
||||
};
|
||||
Reference in New Issue
Block a user