Files
vf_react/src/services/eventService.js
zdl d37cc720ef fix: 添加删除帖子的 mock handler
- 支持 DELETE /api/posts/:postId 请求
- 从内存存储中正确删除评论
- 修复 mock 模式下删除评论失败的问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-15 14:10:44 +08:00

498 lines
18 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/services/eventService.js
import { logger } from '../utils/logger';
import { getApiBase } from '../utils/apiConfig';
/**
* 格式化股票代码,确保包含交易所后缀
* @param {string} code - 股票代码(可能带或不带后缀)
* @returns {string} 带后缀的股票代码
*/
const formatStockCode = (code) => {
if (!code) return code;
// 如果已经有后缀,直接返回
if (code.includes('.')) {
return code;
}
// 根据股票代码规则添加后缀
// 6开头 -> 上海 .SH
// 688开头 -> 科创板(上海).SH
// 0、3开头 -> 深圳 .SZ
// 8、9、4开头 -> 北交所 .BJ
const firstChar = code.charAt(0);
const prefix = code.substring(0, 3);
if (firstChar === '6' || prefix === '688') {
return `${code}.SH`;
} else if (firstChar === '0' || firstChar === '3') {
return `${code}.SZ`;
} else if (firstChar === '8' || firstChar === '9' || firstChar === '4') {
// 北交所股票
return `${code}.BJ`;
}
// 默认返回原代码(可能是指数或其他)
return code;
};
const apiRequest = async (url, options = {}) => {
const method = options.method || 'GET';
const requestData = options.body ? JSON.parse(options.body) : null;
const fullUrl = `${getApiBase()}${url}`;
logger.api.request(method, fullUrl, requestData);
try {
const response = await fetch(fullUrl, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
credentials: 'include',
});
if (!response.ok) {
const errorText = await response.text();
const error = new Error(`HTTP error! status: ${response.status}`);
logger.api.error(method, fullUrl, error, { errorText, ...requestData });
throw error;
}
const data = await response.json();
logger.api.response(method, fullUrl, response.status, data);
return data;
} catch (error) {
logger.api.error(method, fullUrl, error, requestData);
throw error;
}
};
export const eventService = {
getEvents: (params = {}) => {
// Filter out null, undefined, and empty strings (but keep 0 and false)
const cleanParams = Object.fromEntries(
Object.entries(params).filter(([_, v]) => v !== null && v !== undefined && v !== '')
);
const query = new URLSearchParams(cleanParams).toString();
return apiRequest(`/api/events?${query}`);
},
getHotEvents: (params = {}) => {
const query = new URLSearchParams(params).toString();
return apiRequest(`/api/events/hot?${query}`);
},
getPopularKeywords: (limit = 20) => {
return apiRequest(`/api/events/keywords/popular?limit=${limit}`);
},
calendar: {
/**
* Fetches event counts for a given month, for display on the calendar grid.
* @param {number} year - The year to fetch counts for.
* @param {number} month - The month to fetch counts for (1-12).
* @returns {Promise<object>} A list of objects with date, count, and className.
*/
getEventCounts: (year, month) => {
// Note: The backend route is `/api/v1/calendar/event-counts`, not `/api/calendar-event-counts`
return apiRequest(`/api/v1/calendar/event-counts?year=${year}&month=${month}`);
},
/**
* Fetches a list of all events for a specific date.
* @param {string} date - The date in 'YYYY-MM-DD' format.
* @param {string} [type='all'] - The type of events to fetch ('event', 'data', or 'all').
* @returns {Promise<object>} A list of event objects for the given date.
*/
getEventsForDate: (date, type = 'all') => {
return apiRequest(`/api/v1/calendar/events?date=${date}&type=${type}`);
},
/**
* Fetches the full details of a single calendar event.
* @param {number} eventId - The ID of the calendar event.
* @returns {Promise<object>} The detailed calendar event object.
*/
getEventDetail: (eventId) => {
return apiRequest(`/api/v1/calendar/events/${eventId}`);
},
/**
* 切换未来事件的关注状态
* @param {number} eventId - 未来事件ID
* @returns {Promise<object>} 返回关注状态
*/
toggleFollow: async (eventId) => {
return await apiRequest(`/api/v1/calendar/events/${eventId}/follow`, {
method: 'POST'
});
},
/**
* 获取用户关注的所有未来事件
* @returns {Promise<object>} 返回关注的未来事件列表
*/
getFollowingEvents: async () => {
return await apiRequest(`/api/account/future-events/following`);
},
},
getEventDetail: async (eventId) => {
return await apiRequest(`/api/events/${eventId}`);
},
getRelatedStocks: async (eventId) => {
return await apiRequest(`/api/events/${eventId}/stocks`);
},
addRelatedStock: async (eventId, stockData) => {
return await apiRequest(`/api/events/${eventId}/stocks`, {
method: 'POST',
body: JSON.stringify(stockData),
});
},
deleteRelatedStock: async (stockId) => {
return await apiRequest(`/api/stocks/${stockId}`, {
method: 'DELETE',
});
},
getRelatedConcepts: async (eventId) => {
return await apiRequest(`/api/events/${eventId}/concepts`);
},
getHistoricalEvents: async (eventId) => {
return await apiRequest(`/api/events/${eventId}/historical`);
},
getExpectationScore: async (eventId) => {
return await apiRequest(`/api/events/${eventId}/expectation-score`);
},
getTransmissionChainAnalysis: async (eventId) => {
// This API is optimized: it filters isolated nodes and provides a clean data structure.
return await apiRequest(`/api/events/${eventId}/transmission`);
},
getChainNodeDetail: async (eventId, nodeId) => {
return await apiRequest(`/api/events/${eventId}/chain-node/${nodeId}`);
},
getSankeyData: async (eventId) => {
return await apiRequest(`/api/events/${eventId}/sankey-data`);
},
getSankeyNodeDetail: async (eventId, nodeInfo) => {
const params = new URLSearchParams({
node_name: nodeInfo.name,
node_type: nodeInfo.type,
});
if (nodeInfo.level !== undefined && nodeInfo.level !== null) {
params.append('node_level', nodeInfo.level);
}
const url = `/api/events/${eventId}/sankey-data?${params.toString()}`;
return await apiRequest(url);
},
toggleFollow: async (eventId, isFollowing) => {
return await apiRequest(`/api/events/${eventId}/follow`, {
method: 'POST',
body: JSON.stringify({ is_following: isFollowing }),
});
},
// 帖子相关API
getPosts: async (eventId, sortType = 'latest', page = 1, perPage = 20) => {
try {
const result = await apiRequest(`/api/events/${eventId}/posts?sort=${sortType}&page=${page}&per_page=${perPage}`);
// ⚡ 数据转换:将后端的 user 字段映射为前端期望的 author 字段
if (result.success && Array.isArray(result.data)) {
result.data = result.data.map(post => ({
...post,
author: post.user ? {
id: post.user.id,
// 与导航区保持一致:优先显示昵称
username: post.user.nickname || post.user.username,
avatar: post.user.avatar_url || post.user.avatar // 兼容 avatar_url 和 avatar
} : {
id: 'anonymous',
username: 'Anonymous',
avatar: null
}
// 保留原始的 user 字段(如果其他地方需要)
// user: post.user
}));
}
return result;
} catch (error) {
logger.error('eventService', 'getPosts', error, { eventId, sortType, page });
return { success: false, data: [], pagination: {} };
}
},
createPost: async (eventId, postData) => {
try {
return await apiRequest(`/api/events/${eventId}/posts`, {
method: 'POST',
body: JSON.stringify(postData)
});
} catch (error) {
logger.error('eventService', 'createPost', error, { eventId });
return { success: false, message: '创建帖子失败' };
}
},
deletePost: async (postId) => {
try {
return await apiRequest(`/api/posts/${postId}`, {
method: 'DELETE'
});
} catch (error) {
logger.error('eventService', 'deletePost', error, { postId });
return { success: false, message: '删除帖子失败' };
}
},
likePost: async (postId) => {
try {
return await apiRequest(`/api/posts/${postId}/like`, {
method: 'POST'
});
} catch (error) {
logger.error('eventService', 'likePost', error, { postId });
return { success: false, message: '点赞失败' };
}
},
// 评论相关API
getPostComments: async (postId, sortType = 'latest') => {
try {
return await apiRequest(`/api/posts/${postId}/comments?sort=${sortType}`);
} catch (error) {
logger.error('eventService', 'getPostComments', error, { postId, sortType });
return { success: false, data: [] };
}
},
addPostComment: async (postId, commentData) => {
try {
return await apiRequest(`/api/posts/${postId}/comments`, {
method: 'POST',
body: JSON.stringify(commentData)
});
} catch (error) {
logger.error('eventService', 'addPostComment', error, { postId });
return { success: false, message: '添加评论失败' };
}
},
deleteComment: async (commentId) => {
try {
return await apiRequest(`/api/comments/${commentId}`, {
method: 'DELETE'
});
} catch (error) {
logger.error('eventService', 'deleteComment', error, { commentId });
return { success: false, message: '删除评论失败' };
}
},
likeComment: async (commentId) => {
try {
return await apiRequest(`/api/comments/${commentId}/like`, {
method: 'POST'
});
} catch (error) {
logger.error('eventService', 'likeComment', error, { commentId });
return { success: false, message: '点赞失败' };
}
},
// 兼容旧版本的评论API
getComments: async (eventId, sortType = 'latest') => {
logger.warn('eventService', 'getComments 已废弃,请使用 getPosts');
try {
return await apiRequest(`/api/events/${eventId}/posts?sort=${sortType}&page=1&per_page=20`);
} catch (error) {
logger.error('eventService', 'getComments', error, { eventId, sortType });
return { success: false, data: [], pagination: {} };
}
},
addComment: async (eventId, commentData) => {
logger.warn('eventService', 'addComment 已废弃,请使用 createPost');
try {
return await apiRequest(`/api/events/${eventId}/posts`, {
method: 'POST',
body: JSON.stringify(commentData)
});
} catch (error) {
logger.error('eventService', 'addComment', error, { eventId });
return { success: false, message: '创建帖子失败' };
}
},
};
export const stockService = {
getQuotes: async (codes, eventTime = null) => {
try {
const requestBody = {
codes: codes
};
if (eventTime) {
requestBody.event_time = eventTime;
}
logger.debug('stockService', '获取股票报价', requestBody);
const response = await apiRequest(`/api/stock/quotes`, {
method: 'POST',
body: JSON.stringify(requestBody)
});
logger.debug('stockService', '股票报价响应', {
success: response.success,
dataKeys: response.data ? Object.keys(response.data) : []
});
if (response.success && response.data) {
return response.data;
} else {
logger.warn('stockService', '股票报价响应格式异常', response);
return {};
}
} catch (error) {
logger.error('stockService', 'getQuotes', error, { codes, eventTime });
throw error;
}
},
getForecastReport: async (stockCode) => {
return await apiRequest(`/api/stock/${stockCode}/forecast-report`);
},
getKlineData: async (stockCode, chartType = 'timeline', eventTime = null) => {
try {
const params = new URLSearchParams();
params.append('type', chartType);
if (eventTime) {
params.append('event_time', eventTime);
}
// 格式化股票代码,确保带交易所后缀
const formattedCode = formatStockCode(stockCode);
const url = `/api/stock/${formattedCode}/kline?${params.toString()}`;
logger.debug('stockService', '获取K线数据', { stockCode: formattedCode, chartType, eventTime });
const response = await apiRequest(url);
if (response.error) {
logger.warn('stockService', 'K线数据响应包含错误', response.error);
return response;
}
return response;
} catch (error) {
logger.error('stockService', 'getKlineData', error, { stockCode, chartType });
throw error;
}
},
/**
* 批量获取多只股票的K线数据
* @param {string[]} stockCodes - 股票代码数组
* @param {string} chartType - 图表类型 (timeline/daily)
* @param {string} eventTime - 事件时间
* @param {Object} options - 额外选项
* @param {number} options.days_before - 查询事件日期前多少天的数据默认60最大365
* @param {string} options.end_date - 分页加载时指定结束日期(用于加载更早的数据)
* @returns {Promise<Object>} { success, data: { [stockCode]: data[] }, has_more, query_start_date, query_end_date }
*/
getBatchKlineData: async (stockCodes, chartType = 'timeline', eventTime = null, options = {}) => {
try {
// 格式化所有股票代码,确保带交易所后缀
const formattedCodes = stockCodes.map(code => formatStockCode(code));
const requestBody = {
codes: formattedCodes,
type: chartType
};
if (eventTime) {
requestBody.event_time = eventTime;
}
// 添加分页参数
if (options.days_before) {
requestBody.days_before = options.days_before;
}
if (options.end_date) {
requestBody.end_date = options.end_date;
}
logger.debug('stockService', '批量获取K线数据', { stockCount: stockCodes.length, chartType, eventTime, options });
const response = await apiRequest('/api/stock/batch-kline', {
method: 'POST',
body: JSON.stringify(requestBody)
});
return response;
} catch (error) {
logger.error('stockService', 'getBatchKlineData', error, { stockCodes, chartType });
throw error;
}
},
getTransmissionChainAnalysis: async (eventId) => {
return await apiRequest(`/api/events/${eventId}/transmission`);
},
async getSankeyNodeDetail(eventId, nodeInfo) {
try {
const params = new URLSearchParams({
node_name: nodeInfo.name,
node_type: nodeInfo.type,
node_level: nodeInfo.level
});
const url = `/api/events/${eventId}/sankey-node-detail?${params}`;
return await apiRequest(url);
} catch (error) {
logger.error('stockService', 'getSankeyNodeDetail', error, { eventId, nodeInfo });
throw error;
}
},
getChainNodeDetail: async (eventId, nodeId) => {
return await apiRequest(`/api/events/${eventId}/transmission/node/${nodeId}`);
},
getHistoricalEventStocks: async (eventId) => {
return await apiRequest(`/api/historical-events/${eventId}/stocks`);
},
};
export const indexService = {
getKlineData: async (indexCode, chartType = 'timeline', eventTime = null) => {
try {
const params = new URLSearchParams();
params.append('type', chartType);
if (eventTime) {
params.append('event_time', eventTime);
}
const url = `/api/index/${indexCode}/kline?${params.toString()}`;
logger.debug('indexService', '获取指数K线数据', { indexCode, chartType, eventTime });
const response = await apiRequest(url);
return response;
} catch (error) {
logger.error('indexService', 'getKlineData', error, { indexCode, chartType });
throw error;
}
},
};
export default {
eventService,
stockService,
indexService,
};