433 lines
10 KiB
JavaScript
433 lines
10 KiB
JavaScript
/**
|
|
* 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,
|
|
};
|