add forum

This commit is contained in:
2025-11-15 09:10:26 +08:00
parent 2753fbc37f
commit 05b497de29
13 changed files with 2693 additions and 11 deletions

View 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,
};