获取概览卡片数据 (/api/analytics/dashboard-cards)
返回包含留言总数、平均互动率和情感分析三个核心指标的卡片数据及环比变化 支持时间范围和项目ID过滤 获取留言趋势数据 (/api/analytics/comment-trend) 返回一段时间内留言数量的变化趋势,用于绘制柱状图 支持时间范围、项目ID和平台过滤 获取平台分布数据 (/api/analytics/platform-distribution) 返回不同社交平台上的评论或互动分布情况 支持时间范围、项目ID和事件类型过滤 获取情感分析详情 (/api/analytics/sentiment-analysis) 返回正面、中性、负面评论的比例和平均情感得分 支持时间范围、项目ID和平台过滤 获取热门文章数据 (/api/analytics/popular-posts) 返回按互动数量或互动率排序的热门帖文列表 支持时间范围、项目ID、平台过滤,以及排序字段和限制返回数量
This commit is contained in:
@@ -401,6 +401,372 @@ export class AnalyticsController {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取概览卡片数据
|
||||||
|
* 返回包含留言总数、平均互动率和情感分析三个卡片数据
|
||||||
|
*
|
||||||
|
* @param c Hono Context
|
||||||
|
* @returns Response with dashboard card data
|
||||||
|
*/
|
||||||
|
async getDashboardCards(c: Context) {
|
||||||
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取查询参数
|
||||||
|
const timeRange = c.req.query('timeRange') || '30'; // 默认30天
|
||||||
|
const projectId = c.req.query('projectId'); // 可选项目过滤
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] Dashboard cards request received`, {
|
||||||
|
timeRange,
|
||||||
|
projectId,
|
||||||
|
userAgent: c.req.header('user-agent'),
|
||||||
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证时间范围
|
||||||
|
if (!['7', '30', '90'].includes(timeRange)) {
|
||||||
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取概览卡片数据
|
||||||
|
const data = await analyticsService.getDashboardCardData(
|
||||||
|
parseInt(timeRange, 10),
|
||||||
|
projectId
|
||||||
|
);
|
||||||
|
|
||||||
|
// 返回成功响应
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info(`[${requestId}] Dashboard cards response sent successfully`, {
|
||||||
|
duration
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 记录错误
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error(`[${requestId}] Error fetching dashboard cards (${duration}ms)`, error);
|
||||||
|
|
||||||
|
// 返回错误响应
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to fetch dashboard card data',
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取留言趋势数据
|
||||||
|
* 返回一段时间内留言数量的变化趋势
|
||||||
|
*
|
||||||
|
* @param c Hono Context
|
||||||
|
* @returns Response with comment trend data
|
||||||
|
*/
|
||||||
|
async getCommentTrend(c: Context) {
|
||||||
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取查询参数
|
||||||
|
const timeRange = c.req.query('timeRange') || '30'; // 默认30天
|
||||||
|
const projectId = c.req.query('projectId'); // 可选项目过滤
|
||||||
|
const platform = c.req.query('platform'); // 可选平台过滤
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] Comment trend request received`, {
|
||||||
|
timeRange,
|
||||||
|
projectId,
|
||||||
|
platform,
|
||||||
|
userAgent: c.req.header('user-agent'),
|
||||||
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证时间范围
|
||||||
|
if (!['7', '30', '90'].includes(timeRange)) {
|
||||||
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取留言趋势数据
|
||||||
|
const data = await analyticsService.getCommentTrend(
|
||||||
|
parseInt(timeRange, 10),
|
||||||
|
projectId,
|
||||||
|
platform
|
||||||
|
);
|
||||||
|
|
||||||
|
// 返回成功响应
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info(`[${requestId}] Comment trend response sent successfully`, {
|
||||||
|
duration,
|
||||||
|
dataPoints: data.data.length,
|
||||||
|
totalComments: data.total_count
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: data.data,
|
||||||
|
metadata: {
|
||||||
|
max_count: data.max_count,
|
||||||
|
total_count: data.total_count
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 记录错误
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error(`[${requestId}] Error fetching comment trend (${duration}ms)`, error);
|
||||||
|
|
||||||
|
// 返回错误响应
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to fetch comment trend data',
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台分布数据
|
||||||
|
* 返回不同社交平台上的事件分布情况
|
||||||
|
*
|
||||||
|
* @param c Hono Context
|
||||||
|
* @returns Response with platform distribution data
|
||||||
|
*/
|
||||||
|
async getPlatformDistribution(c: Context) {
|
||||||
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取查询参数
|
||||||
|
const timeRange = c.req.query('timeRange') || '30'; // 默认30天
|
||||||
|
const projectId = c.req.query('projectId'); // 可选项目过滤
|
||||||
|
const eventType = c.req.query('eventType') || 'comment'; // 默认分析评论事件
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] Platform distribution request received`, {
|
||||||
|
timeRange,
|
||||||
|
projectId,
|
||||||
|
eventType,
|
||||||
|
userAgent: c.req.header('user-agent'),
|
||||||
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证时间范围
|
||||||
|
if (!['7', '30', '90'].includes(timeRange)) {
|
||||||
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证事件类型
|
||||||
|
const validEventTypes = ['comment', 'like', 'view', 'share'];
|
||||||
|
if (!validEventTypes.includes(eventType)) {
|
||||||
|
logger.warn(`[${requestId}] Invalid eventType: ${eventType}`);
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: `Invalid eventType. Must be one of: ${validEventTypes.join(', ')}`
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取平台分布数据
|
||||||
|
const data = await analyticsService.getPlatformDistribution(
|
||||||
|
parseInt(timeRange, 10),
|
||||||
|
projectId,
|
||||||
|
eventType
|
||||||
|
);
|
||||||
|
|
||||||
|
// 返回成功响应
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info(`[${requestId}] Platform distribution response sent successfully`, {
|
||||||
|
duration,
|
||||||
|
platformCount: data.data.length,
|
||||||
|
total: data.total
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: data.data,
|
||||||
|
metadata: {
|
||||||
|
total: data.total,
|
||||||
|
event_type: eventType
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 记录错误
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error(`[${requestId}] Error fetching platform distribution (${duration}ms)`, error);
|
||||||
|
|
||||||
|
// 返回错误响应
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to fetch platform distribution data',
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取情感分析详情数据
|
||||||
|
* 返回正面、中性、负面评论的比例和分析
|
||||||
|
*
|
||||||
|
* @param c Hono Context
|
||||||
|
* @returns Response with sentiment analysis data
|
||||||
|
*/
|
||||||
|
async getSentimentAnalysis(c: Context) {
|
||||||
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取查询参数
|
||||||
|
const timeRange = c.req.query('timeRange') || '30'; // 默认30天
|
||||||
|
const projectId = c.req.query('projectId'); // 可选项目过滤
|
||||||
|
const platform = c.req.query('platform'); // 可选平台过滤
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] Sentiment analysis request received`, {
|
||||||
|
timeRange,
|
||||||
|
projectId,
|
||||||
|
platform,
|
||||||
|
userAgent: c.req.header('user-agent'),
|
||||||
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证时间范围
|
||||||
|
if (!['7', '30', '90'].includes(timeRange)) {
|
||||||
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取情感分析数据
|
||||||
|
const data = await analyticsService.getSentimentAnalysis(
|
||||||
|
parseInt(timeRange, 10),
|
||||||
|
projectId,
|
||||||
|
platform
|
||||||
|
);
|
||||||
|
|
||||||
|
// 返回成功响应
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info(`[${requestId}] Sentiment analysis response sent successfully`, {
|
||||||
|
duration,
|
||||||
|
total: data.total
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 记录错误
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error(`[${requestId}] Error fetching sentiment analysis (${duration}ms)`, error);
|
||||||
|
|
||||||
|
// 返回错误响应
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to fetch sentiment analysis data',
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取热门文章数据
|
||||||
|
* 返回按互动数量或互动率排序的热门帖文
|
||||||
|
*
|
||||||
|
* @param c Hono Context
|
||||||
|
* @returns Response with popular posts data
|
||||||
|
*/
|
||||||
|
async getPopularPosts(c: Context) {
|
||||||
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取查询参数
|
||||||
|
const timeRange = c.req.query('timeRange') || '30'; // 默认30天
|
||||||
|
const projectId = c.req.query('projectId'); // 可选项目过滤
|
||||||
|
const platform = c.req.query('platform'); // 可选平台过滤
|
||||||
|
const sortBy = c.req.query('sortBy') || 'engagement_count'; // 默认按互动数量排序
|
||||||
|
const limit = parseInt(c.req.query('limit') || '10', 10); // 默认返回10个
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] Popular posts request received`, {
|
||||||
|
timeRange,
|
||||||
|
projectId,
|
||||||
|
platform,
|
||||||
|
sortBy,
|
||||||
|
limit,
|
||||||
|
userAgent: c.req.header('user-agent'),
|
||||||
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证时间范围
|
||||||
|
if (!['7', '30', '90'].includes(timeRange)) {
|
||||||
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证排序字段
|
||||||
|
if (!['engagement_count', 'engagement_rate'].includes(sortBy)) {
|
||||||
|
logger.warn(`[${requestId}] Invalid sortBy: ${sortBy}`);
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid sortBy. Must be engagement_count or engagement_rate.'
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取热门文章数据
|
||||||
|
const data = await analyticsService.getPopularPosts(
|
||||||
|
parseInt(timeRange, 10),
|
||||||
|
projectId,
|
||||||
|
platform,
|
||||||
|
sortBy,
|
||||||
|
Math.min(limit, 50) // 最多返回50条
|
||||||
|
);
|
||||||
|
|
||||||
|
// 返回成功响应
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info(`[${requestId}] Popular posts response sent successfully`, {
|
||||||
|
duration,
|
||||||
|
resultCount: data.posts.length,
|
||||||
|
total: data.total
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: data.posts,
|
||||||
|
metadata: {
|
||||||
|
total: data.total,
|
||||||
|
high_engagement_count: data.posts.filter(post => post.is_high_engagement).length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 记录错误
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error(`[${requestId}] Error fetching popular posts (${duration}ms)`, error);
|
||||||
|
|
||||||
|
// 返回错误响应
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to fetch popular posts data',
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
|
|||||||
@@ -32,4 +32,19 @@ analyticsRouter.get('/kol-funnel', (c) => analyticsController.getKolFunnel(c));
|
|||||||
// Add new post performance route
|
// Add new post performance route
|
||||||
analyticsRouter.get('/post-performance', (c) => analyticsController.getPostPerformance(c));
|
analyticsRouter.get('/post-performance', (c) => analyticsController.getPostPerformance(c));
|
||||||
|
|
||||||
|
// Add dashboard cards route
|
||||||
|
analyticsRouter.get('/dashboard-cards', (c) => analyticsController.getDashboardCards(c));
|
||||||
|
|
||||||
|
// Add comment trend route
|
||||||
|
analyticsRouter.get('/comment-trend', (c) => analyticsController.getCommentTrend(c));
|
||||||
|
|
||||||
|
// Add platform distribution route
|
||||||
|
analyticsRouter.get('/platform-distribution', (c) => analyticsController.getPlatformDistribution(c));
|
||||||
|
|
||||||
|
// Add sentiment analysis route
|
||||||
|
analyticsRouter.get('/sentiment-analysis', (c) => analyticsController.getSentimentAnalysis(c));
|
||||||
|
|
||||||
|
// Add popular posts route
|
||||||
|
analyticsRouter.get('/popular-posts', (c) => analyticsController.getPopularPosts(c));
|
||||||
|
|
||||||
export default analyticsRouter;
|
export default analyticsRouter;
|
||||||
@@ -83,6 +83,102 @@ export interface PostPerformanceResponse {
|
|||||||
total: number; // 总数
|
total: number; // 总数
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 概览卡片数据
|
||||||
|
*/
|
||||||
|
export interface DashboardCardData {
|
||||||
|
comments_count: {
|
||||||
|
current: number;
|
||||||
|
change_percentage: number;
|
||||||
|
};
|
||||||
|
engagement_rate: {
|
||||||
|
current: number;
|
||||||
|
change_percentage: number;
|
||||||
|
};
|
||||||
|
sentiment_score: {
|
||||||
|
current: number;
|
||||||
|
change_percentage: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 留言趋势数据点
|
||||||
|
*/
|
||||||
|
export interface CommentTrendPoint {
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 留言趋势响应
|
||||||
|
*/
|
||||||
|
export interface CommentTrendResponse {
|
||||||
|
data: CommentTrendPoint[];
|
||||||
|
max_count: number;
|
||||||
|
total_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台分布数据项
|
||||||
|
*/
|
||||||
|
export interface PlatformDistributionItem {
|
||||||
|
platform: string;
|
||||||
|
count: number;
|
||||||
|
percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台分布响应
|
||||||
|
*/
|
||||||
|
export interface PlatformDistributionResponse {
|
||||||
|
data: PlatformDistributionItem[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 情感分析详情数据
|
||||||
|
*/
|
||||||
|
export interface SentimentAnalysisData {
|
||||||
|
positive: {
|
||||||
|
count: number;
|
||||||
|
percentage: number;
|
||||||
|
};
|
||||||
|
neutral: {
|
||||||
|
count: number;
|
||||||
|
percentage: number;
|
||||||
|
};
|
||||||
|
negative: {
|
||||||
|
count: number;
|
||||||
|
percentage: number;
|
||||||
|
};
|
||||||
|
total: number;
|
||||||
|
average_score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热门文章数据项
|
||||||
|
*/
|
||||||
|
export interface PopularPostItem {
|
||||||
|
post_id: string;
|
||||||
|
title: string;
|
||||||
|
platform: string;
|
||||||
|
influencer_id: string;
|
||||||
|
influencer_name: string;
|
||||||
|
publish_date: string;
|
||||||
|
engagement_count: number;
|
||||||
|
views_count: number;
|
||||||
|
engagement_rate: number;
|
||||||
|
is_high_engagement: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热门文章响应
|
||||||
|
*/
|
||||||
|
export interface PopularPostsResponse {
|
||||||
|
posts: PopularPostItem[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analytics service for KOL performance data
|
* Analytics service for KOL performance data
|
||||||
*/
|
*/
|
||||||
@@ -887,6 +983,749 @@ export class AnalyticsService {
|
|||||||
|
|
||||||
return mockPosts;
|
return mockPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取概览卡片数据
|
||||||
|
* @param timeRange 时间范围(天数)
|
||||||
|
* @param projectId 可选项目ID
|
||||||
|
* @returns 概览卡片数据
|
||||||
|
*/
|
||||||
|
async getDashboardCardData(
|
||||||
|
timeRange: number,
|
||||||
|
projectId?: string
|
||||||
|
): Promise<DashboardCardData> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
logger.info('Fetching dashboard card data', { timeRange, projectId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 计算当前时间范围和比较时间范围
|
||||||
|
const currentDate = new Date();
|
||||||
|
const pastDate = new Date();
|
||||||
|
pastDate.setDate(currentDate.getDate() - timeRange);
|
||||||
|
|
||||||
|
const olderPastDate = new Date(pastDate);
|
||||||
|
olderPastDate.setDate(olderPastDate.getDate() - timeRange);
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const currentDateStr = this.formatDateForClickhouse(currentDate);
|
||||||
|
const pastDateStr = this.formatDateForClickhouse(pastDate);
|
||||||
|
const olderPastDateStr = this.formatDateForClickhouse(olderPastDate);
|
||||||
|
|
||||||
|
// 构建项目过滤条件
|
||||||
|
const projectFilter = projectId ? `AND project_id = '${projectId}'` : '';
|
||||||
|
|
||||||
|
// 查询当前时间段的数据
|
||||||
|
const currentPeriodQuery = `
|
||||||
|
SELECT
|
||||||
|
countIf(event_type = 'comment') AS comments_count,
|
||||||
|
countIf(event_type = 'view') AS views_count,
|
||||||
|
countIf(event_type = 'like' OR event_type = 'comment' OR event_type = 'share') AS interactions_count,
|
||||||
|
avg(CASE
|
||||||
|
WHEN sentiment = 'positive' THEN 1
|
||||||
|
WHEN sentiment = 'neutral' THEN 0
|
||||||
|
WHEN sentiment = 'negative' THEN -1
|
||||||
|
ELSE NULL
|
||||||
|
END) AS avg_sentiment
|
||||||
|
FROM
|
||||||
|
events
|
||||||
|
WHERE
|
||||||
|
date BETWEEN '${pastDateStr}' AND '${currentDateStr}'
|
||||||
|
${projectFilter}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 查询上一时间段的数据(用于计算环比)
|
||||||
|
const previousPeriodQuery = `
|
||||||
|
SELECT
|
||||||
|
countIf(event_type = 'comment') AS comments_count,
|
||||||
|
countIf(event_type = 'view') AS views_count,
|
||||||
|
countIf(event_type = 'like' OR event_type = 'comment' OR event_type = 'share') AS interactions_count,
|
||||||
|
avg(CASE
|
||||||
|
WHEN sentiment = 'positive' THEN 1
|
||||||
|
WHEN sentiment = 'neutral' THEN 0
|
||||||
|
WHEN sentiment = 'negative' THEN -1
|
||||||
|
ELSE NULL
|
||||||
|
END) AS avg_sentiment
|
||||||
|
FROM
|
||||||
|
events
|
||||||
|
WHERE
|
||||||
|
date BETWEEN '${olderPastDateStr}' AND '${pastDateStr}'
|
||||||
|
${projectFilter}
|
||||||
|
`;
|
||||||
|
|
||||||
|
logger.debug('Executing ClickHouse queries for dashboard cards', {
|
||||||
|
currentPeriodQuery: currentPeriodQuery.replace(/\n\s+/g, ' ').trim(),
|
||||||
|
previousPeriodQuery: previousPeriodQuery.replace(/\n\s+/g, ' ').trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
const [currentData, previousData] = await Promise.all([
|
||||||
|
this.executeClickhouseQuery(currentPeriodQuery),
|
||||||
|
this.executeClickhouseQuery(previousPeriodQuery)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 解析结果
|
||||||
|
const current = currentData[0] || {};
|
||||||
|
const previous = previousData[0] || {};
|
||||||
|
|
||||||
|
// 计算当前值
|
||||||
|
const commentsCount = Number(current.comments_count || 0);
|
||||||
|
const viewsCount = Number(current.views_count || 0);
|
||||||
|
const interactionsCount = Number(current.interactions_count || 0);
|
||||||
|
const avgSentiment = Number(current.avg_sentiment || 0);
|
||||||
|
|
||||||
|
// 计算环比变化
|
||||||
|
const prevCommentsCount = Number(previous.comments_count || 0);
|
||||||
|
const prevViewsCount = Number(previous.views_count || 0);
|
||||||
|
const prevInteractionsCount = Number(previous.interactions_count || 0);
|
||||||
|
const prevAvgSentiment = Number(previous.avg_sentiment || 0);
|
||||||
|
|
||||||
|
// 计算环比百分比变化
|
||||||
|
const calculatePercentageChange = (current: number, previous: number): number => {
|
||||||
|
if (previous === 0) return current > 0 ? 100 : 0;
|
||||||
|
return ((current - previous) / previous) * 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
const commentsChange = calculatePercentageChange(commentsCount, prevCommentsCount);
|
||||||
|
|
||||||
|
// 计算互动率 (interactions / views)
|
||||||
|
const currentEngagementRate = viewsCount > 0 ? (interactionsCount / viewsCount) * 100 : 0;
|
||||||
|
const prevEngagementRate = prevViewsCount > 0 ? (prevInteractionsCount / prevViewsCount) * 100 : 0;
|
||||||
|
const engagementRateChange = calculatePercentageChange(currentEngagementRate, prevEngagementRate);
|
||||||
|
|
||||||
|
// 计算情感得分变化
|
||||||
|
const sentimentChange = calculatePercentageChange(avgSentiment, prevAvgSentiment);
|
||||||
|
|
||||||
|
const result: DashboardCardData = {
|
||||||
|
comments_count: {
|
||||||
|
current: commentsCount,
|
||||||
|
change_percentage: parseFloat(commentsChange.toFixed(2))
|
||||||
|
},
|
||||||
|
engagement_rate: {
|
||||||
|
current: parseFloat(currentEngagementRate.toFixed(2)),
|
||||||
|
change_percentage: parseFloat(engagementRateChange.toFixed(2))
|
||||||
|
},
|
||||||
|
sentiment_score: {
|
||||||
|
current: parseFloat(avgSentiment.toFixed(2)),
|
||||||
|
change_percentage: parseFloat(sentimentChange.toFixed(2))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info('Dashboard card data fetched successfully', {
|
||||||
|
duration,
|
||||||
|
result
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error(`Error in getDashboardCardData (${duration}ms)`, error);
|
||||||
|
|
||||||
|
// 发生错误时返回模拟数据
|
||||||
|
return {
|
||||||
|
comments_count: { current: 0, change_percentage: 0 },
|
||||||
|
engagement_rate: { current: 0, change_percentage: 0 },
|
||||||
|
sentiment_score: { current: 0, change_percentage: 0 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取留言趋势数据
|
||||||
|
* @param timeRange 时间范围(天数)
|
||||||
|
* @param projectId 可选项目ID
|
||||||
|
* @param platform 可选平台
|
||||||
|
* @returns 留言趋势数据
|
||||||
|
*/
|
||||||
|
async getCommentTrend(
|
||||||
|
timeRange: number,
|
||||||
|
projectId?: string,
|
||||||
|
platform?: string
|
||||||
|
): Promise<CommentTrendResponse> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
logger.info('Fetching comment trend data', { timeRange, projectId, platform });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 计算时间范围
|
||||||
|
const currentDate = new Date();
|
||||||
|
const pastDate = new Date();
|
||||||
|
pastDate.setDate(currentDate.getDate() - timeRange);
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const currentDateStr = this.formatDateForClickhouse(currentDate);
|
||||||
|
const pastDateStr = this.formatDateForClickhouse(pastDate);
|
||||||
|
|
||||||
|
// 构建过滤条件
|
||||||
|
const filters: string[] = [];
|
||||||
|
filters.push(`date BETWEEN '${pastDateStr}' AND '${currentDateStr}'`);
|
||||||
|
filters.push(`event_type = 'comment'`);
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
filters.push(`project_id = '${projectId}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform) {
|
||||||
|
filters.push(`platform = '${platform}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterCondition = filters.join(' AND ');
|
||||||
|
|
||||||
|
// 按日期分组查询留言数量
|
||||||
|
const trendQuery = `
|
||||||
|
SELECT
|
||||||
|
toDate(timestamp) AS date,
|
||||||
|
count() AS count
|
||||||
|
FROM
|
||||||
|
events
|
||||||
|
WHERE
|
||||||
|
${filterCondition}
|
||||||
|
GROUP BY
|
||||||
|
date
|
||||||
|
ORDER BY
|
||||||
|
date ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 查询总留言数
|
||||||
|
const totalQuery = `
|
||||||
|
SELECT
|
||||||
|
count() AS total
|
||||||
|
FROM
|
||||||
|
events
|
||||||
|
WHERE
|
||||||
|
${filterCondition}
|
||||||
|
`;
|
||||||
|
|
||||||
|
logger.debug('Executing ClickHouse queries for comment trend', {
|
||||||
|
trendQuery: trendQuery.replace(/\n\s+/g, ' ').trim(),
|
||||||
|
totalQuery: totalQuery.replace(/\n\s+/g, ' ').trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
const [trendData, totalData] = await Promise.all([
|
||||||
|
this.executeClickhouseQuery(trendQuery),
|
||||||
|
this.executeClickhouseQuery(totalQuery)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 解析结果
|
||||||
|
const total = totalData[0]?.total || 0;
|
||||||
|
|
||||||
|
// 确保每天都有数据点
|
||||||
|
const trendPoints: CommentTrendPoint[] = [];
|
||||||
|
let maxCount = 0;
|
||||||
|
|
||||||
|
// 创建日期范围内的所有日期
|
||||||
|
const dateMap: Record<string, number> = {};
|
||||||
|
const currentDateObj = new Date(pastDate);
|
||||||
|
|
||||||
|
while (currentDateObj <= currentDate) {
|
||||||
|
const dateStr = this.formatDateForClickhouse(currentDateObj);
|
||||||
|
dateMap[dateStr] = 0;
|
||||||
|
currentDateObj.setDate(currentDateObj.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充实际数据
|
||||||
|
if (Array.isArray(trendData)) {
|
||||||
|
trendData.forEach(point => {
|
||||||
|
if (point.date && point.count !== undefined) {
|
||||||
|
const dateStr = point.date.split('T')[0]; // 确保日期格式一致
|
||||||
|
dateMap[dateStr] = Number(point.count);
|
||||||
|
maxCount = Math.max(maxCount, Number(point.count));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为数组
|
||||||
|
Object.entries(dateMap).forEach(([date, count]) => {
|
||||||
|
trendPoints.push({
|
||||||
|
date,
|
||||||
|
count: Number(count)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按日期排序
|
||||||
|
trendPoints.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
|
||||||
|
const result: CommentTrendResponse = {
|
||||||
|
data: trendPoints,
|
||||||
|
max_count: maxCount,
|
||||||
|
total_count: Number(total)
|
||||||
|
};
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info('Comment trend data fetched successfully', {
|
||||||
|
duration,
|
||||||
|
dataPoints: trendPoints.length,
|
||||||
|
total
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error(`Error in getCommentTrend (${duration}ms)`, error);
|
||||||
|
|
||||||
|
// 发生错误时返回空数据
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
max_count: 0,
|
||||||
|
total_count: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台分布数据
|
||||||
|
* @param timeRange 时间范围(天数)
|
||||||
|
* @param projectId 可选项目ID
|
||||||
|
* @param eventType 可选事件类型
|
||||||
|
* @returns 平台分布数据
|
||||||
|
*/
|
||||||
|
async getPlatformDistribution(
|
||||||
|
timeRange: number,
|
||||||
|
projectId?: string,
|
||||||
|
eventType: string = 'comment'
|
||||||
|
): Promise<PlatformDistributionResponse> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
logger.info('Fetching platform distribution data', { timeRange, projectId, eventType });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 计算时间范围
|
||||||
|
const currentDate = new Date();
|
||||||
|
const pastDate = new Date();
|
||||||
|
pastDate.setDate(currentDate.getDate() - timeRange);
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const currentDateStr = this.formatDateForClickhouse(currentDate);
|
||||||
|
const pastDateStr = this.formatDateForClickhouse(pastDate);
|
||||||
|
|
||||||
|
// 构建过滤条件
|
||||||
|
const filters: string[] = [];
|
||||||
|
filters.push(`date BETWEEN '${pastDateStr}' AND '${currentDateStr}'`);
|
||||||
|
filters.push(`event_type = '${eventType}'`);
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
filters.push(`project_id = '${projectId}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterCondition = filters.join(' AND ');
|
||||||
|
|
||||||
|
// 按平台分组查询事件数量
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
platform,
|
||||||
|
count() AS count
|
||||||
|
FROM
|
||||||
|
events
|
||||||
|
WHERE
|
||||||
|
${filterCondition}
|
||||||
|
GROUP BY
|
||||||
|
platform
|
||||||
|
ORDER BY
|
||||||
|
count DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
logger.debug('Executing ClickHouse query for platform distribution', {
|
||||||
|
query: query.replace(/\n\s+/g, ' ').trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
const result = await this.executeClickhouseQuery(query);
|
||||||
|
|
||||||
|
// 处理结果
|
||||||
|
const platforms: PlatformDistributionItem[] = [];
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
result.forEach(item => {
|
||||||
|
if (item.platform && item.count !== undefined) {
|
||||||
|
const count = Number(item.count);
|
||||||
|
total += count;
|
||||||
|
platforms.push({
|
||||||
|
platform: item.platform,
|
||||||
|
count,
|
||||||
|
percentage: 0 // 先初始化为0,稍后计算
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算百分比
|
||||||
|
if (total > 0) {
|
||||||
|
platforms.forEach(item => {
|
||||||
|
item.percentage = parseFloat(((item.count / total) * 100).toFixed(2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保结果包含主要平台,即使没有数据
|
||||||
|
const mainPlatforms = ['Instagram', 'TikTok', 'Twitter', 'Facebook', 'YouTube'];
|
||||||
|
mainPlatforms.forEach(platform => {
|
||||||
|
if (!platforms.some(item => item.platform.toLowerCase() === platform.toLowerCase())) {
|
||||||
|
platforms.push({
|
||||||
|
platform,
|
||||||
|
count: 0,
|
||||||
|
percentage: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按数量降序排序
|
||||||
|
platforms.sort((a, b) => b.count - a.count);
|
||||||
|
|
||||||
|
const result_data: PlatformDistributionResponse = {
|
||||||
|
data: platforms,
|
||||||
|
total
|
||||||
|
};
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info('Platform distribution data fetched successfully', {
|
||||||
|
duration,
|
||||||
|
platformCount: platforms.length,
|
||||||
|
total
|
||||||
|
});
|
||||||
|
|
||||||
|
return result_data;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error(`Error in getPlatformDistribution (${duration}ms)`, error);
|
||||||
|
|
||||||
|
// 发生错误时返回空数据
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
total: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取情感分析详情数据
|
||||||
|
* @param timeRange 时间范围(天数)
|
||||||
|
* @param projectId 可选项目ID
|
||||||
|
* @param platform 可选平台
|
||||||
|
* @returns 情感分析详情数据
|
||||||
|
*/
|
||||||
|
async getSentimentAnalysis(
|
||||||
|
timeRange: number,
|
||||||
|
projectId?: string,
|
||||||
|
platform?: string
|
||||||
|
): Promise<SentimentAnalysisData> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
logger.info('Fetching sentiment analysis data', { timeRange, projectId, platform });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 计算时间范围
|
||||||
|
const currentDate = new Date();
|
||||||
|
const pastDate = new Date();
|
||||||
|
pastDate.setDate(currentDate.getDate() - timeRange);
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const currentDateStr = this.formatDateForClickhouse(currentDate);
|
||||||
|
const pastDateStr = this.formatDateForClickhouse(pastDate);
|
||||||
|
|
||||||
|
// 构建过滤条件
|
||||||
|
const filters: string[] = [];
|
||||||
|
filters.push(`date BETWEEN '${pastDateStr}' AND '${currentDateStr}'`);
|
||||||
|
filters.push(`sentiment IN ('positive', 'neutral', 'negative')`);
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
filters.push(`project_id = '${projectId}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform) {
|
||||||
|
filters.push(`platform = '${platform}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterCondition = filters.join(' AND ');
|
||||||
|
|
||||||
|
// 查询情感分布
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
sentiment,
|
||||||
|
count() AS count
|
||||||
|
FROM
|
||||||
|
events
|
||||||
|
WHERE
|
||||||
|
${filterCondition}
|
||||||
|
GROUP BY
|
||||||
|
sentiment
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 查询情感得分平均值
|
||||||
|
const averageQuery = `
|
||||||
|
SELECT
|
||||||
|
avg(CASE
|
||||||
|
WHEN sentiment = 'positive' THEN 1
|
||||||
|
WHEN sentiment = 'neutral' THEN 0
|
||||||
|
WHEN sentiment = 'negative' THEN -1
|
||||||
|
ELSE NULL
|
||||||
|
END) AS avg_score
|
||||||
|
FROM
|
||||||
|
events
|
||||||
|
WHERE
|
||||||
|
${filterCondition}
|
||||||
|
`;
|
||||||
|
|
||||||
|
logger.debug('Executing ClickHouse queries for sentiment analysis', {
|
||||||
|
query: query.replace(/\n\s+/g, ' ').trim(),
|
||||||
|
averageQuery: averageQuery.replace(/\n\s+/g, ' ').trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
const [sentimentData, averageData] = await Promise.all([
|
||||||
|
this.executeClickhouseQuery(query),
|
||||||
|
this.executeClickhouseQuery(averageQuery)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 初始化结果
|
||||||
|
let positiveCount = 0;
|
||||||
|
let neutralCount = 0;
|
||||||
|
let negativeCount = 0;
|
||||||
|
|
||||||
|
// 处理情感分布数据
|
||||||
|
if (Array.isArray(sentimentData)) {
|
||||||
|
sentimentData.forEach(item => {
|
||||||
|
if (item.sentiment && item.count !== undefined) {
|
||||||
|
const count = Number(item.count);
|
||||||
|
if (item.sentiment === 'positive') {
|
||||||
|
positiveCount = count;
|
||||||
|
} else if (item.sentiment === 'neutral') {
|
||||||
|
neutralCount = count;
|
||||||
|
} else if (item.sentiment === 'negative') {
|
||||||
|
negativeCount = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总数和百分比
|
||||||
|
const total = positiveCount + neutralCount + negativeCount;
|
||||||
|
const positivePercentage = total > 0 ? (positiveCount / total) * 100 : 0;
|
||||||
|
const neutralPercentage = total > 0 ? (neutralCount / total) * 100 : 0;
|
||||||
|
const negativePercentage = total > 0 ? (negativeCount / total) * 100 : 0;
|
||||||
|
|
||||||
|
// 获取平均得分
|
||||||
|
const avgScore = Array.isArray(averageData) && averageData.length > 0
|
||||||
|
? Number(averageData[0].avg_score || 0)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const result: SentimentAnalysisData = {
|
||||||
|
positive: {
|
||||||
|
count: positiveCount,
|
||||||
|
percentage: parseFloat(positivePercentage.toFixed(2))
|
||||||
|
},
|
||||||
|
neutral: {
|
||||||
|
count: neutralCount,
|
||||||
|
percentage: parseFloat(neutralPercentage.toFixed(2))
|
||||||
|
},
|
||||||
|
negative: {
|
||||||
|
count: negativeCount,
|
||||||
|
percentage: parseFloat(negativePercentage.toFixed(2))
|
||||||
|
},
|
||||||
|
total,
|
||||||
|
average_score: parseFloat(avgScore.toFixed(2))
|
||||||
|
};
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info('Sentiment analysis data fetched successfully', {
|
||||||
|
duration,
|
||||||
|
total,
|
||||||
|
avgScore
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error(`Error in getSentimentAnalysis (${duration}ms)`, error);
|
||||||
|
|
||||||
|
// 发生错误时返回空数据
|
||||||
|
return {
|
||||||
|
positive: { count: 0, percentage: 0 },
|
||||||
|
neutral: { count: 0, percentage: 0 },
|
||||||
|
negative: { count: 0, percentage: 0 },
|
||||||
|
total: 0,
|
||||||
|
average_score: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取热门文章数据
|
||||||
|
* @param timeRange 时间范围(天数)
|
||||||
|
* @param projectId 可选项目ID
|
||||||
|
* @param platform 可选平台
|
||||||
|
* @param sortBy 排序字段
|
||||||
|
* @param limit 返回数量
|
||||||
|
* @returns 热门文章数据
|
||||||
|
*/
|
||||||
|
async getPopularPosts(
|
||||||
|
timeRange: number,
|
||||||
|
projectId?: string,
|
||||||
|
platform?: string,
|
||||||
|
sortBy: string = 'engagement_count',
|
||||||
|
limit: number = 10
|
||||||
|
): Promise<PopularPostsResponse> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
logger.info('Fetching popular posts data', {
|
||||||
|
timeRange,
|
||||||
|
projectId,
|
||||||
|
platform,
|
||||||
|
sortBy,
|
||||||
|
limit
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 计算时间范围
|
||||||
|
const currentDate = new Date();
|
||||||
|
const pastDate = new Date();
|
||||||
|
pastDate.setDate(currentDate.getDate() - timeRange);
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const currentDateStr = this.formatDateForClickhouse(currentDate);
|
||||||
|
const pastDateStr = this.formatDateForClickhouse(pastDate);
|
||||||
|
|
||||||
|
// 构建帖文过滤条件
|
||||||
|
const postFilters: string[] = [];
|
||||||
|
|
||||||
|
if (platform) {
|
||||||
|
postFilters.push(`p.platform = '${platform}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
postFilters.push(`p.project_id = '${projectId}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
postFilters.push(`p.date BETWEEN '${pastDateStr}' AND '${currentDateStr}'`);
|
||||||
|
|
||||||
|
const postFilterCondition = postFilters.length > 0
|
||||||
|
? 'WHERE ' + postFilters.join(' AND ')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// 获取帖文和互动数据
|
||||||
|
const query = `
|
||||||
|
WITH post_metrics AS (
|
||||||
|
SELECT
|
||||||
|
content_id AS post_id,
|
||||||
|
countIf(event_type = 'view') AS views_count,
|
||||||
|
countIf(event_type = 'like' OR event_type = 'comment' OR event_type = 'share') AS engagement_count
|
||||||
|
FROM
|
||||||
|
events
|
||||||
|
WHERE
|
||||||
|
date BETWEEN '${pastDateStr}' AND '${currentDateStr}'
|
||||||
|
AND content_id IS NOT NULL
|
||||||
|
${projectId ? `AND project_id = '${projectId}'` : ''}
|
||||||
|
${platform ? `AND platform = '${platform}'` : ''}
|
||||||
|
GROUP BY
|
||||||
|
post_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
p.post_id,
|
||||||
|
p.title,
|
||||||
|
p.platform,
|
||||||
|
p.influencer_id,
|
||||||
|
i.name AS influencer_name,
|
||||||
|
toString(p.created_at) AS publish_date,
|
||||||
|
pm.engagement_count,
|
||||||
|
pm.views_count,
|
||||||
|
multiIf(pm.views_count > 0, pm.engagement_count / pm.views_count, 0) AS engagement_rate
|
||||||
|
FROM
|
||||||
|
posts p
|
||||||
|
LEFT JOIN
|
||||||
|
influencers i ON p.influencer_id = i.influencer_id
|
||||||
|
LEFT JOIN
|
||||||
|
post_metrics pm ON p.post_id = pm.post_id
|
||||||
|
${postFilterCondition}
|
||||||
|
ORDER BY
|
||||||
|
${sortBy === 'engagement_rate' ? 'engagement_rate' : 'engagement_count'} DESC
|
||||||
|
LIMIT ${limit}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 查询符合条件的帖文总数
|
||||||
|
const countQuery = `
|
||||||
|
SELECT
|
||||||
|
count() AS total
|
||||||
|
FROM
|
||||||
|
posts p
|
||||||
|
${postFilterCondition}
|
||||||
|
`;
|
||||||
|
|
||||||
|
logger.debug('Executing ClickHouse queries for popular posts', {
|
||||||
|
query: query.replace(/\n\s+/g, ' ').trim(),
|
||||||
|
countQuery: countQuery.replace(/\n\s+/g, ' ').trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
const [postsData, countData] = await Promise.all([
|
||||||
|
this.executeClickhouseQuery(query),
|
||||||
|
this.executeClickhouseQuery(countQuery)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 计算总数
|
||||||
|
const total = Array.isArray(countData) && countData.length > 0
|
||||||
|
? Number(countData[0].total || 0)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// 计算高互动率阈值(互动率大于帖文平均互动率的150%)
|
||||||
|
let averageEngagementRate = 0;
|
||||||
|
let engagementRates: number[] = [];
|
||||||
|
|
||||||
|
if (Array.isArray(postsData)) {
|
||||||
|
engagementRates = postsData
|
||||||
|
.map(post => Number(post.engagement_rate || 0))
|
||||||
|
.filter(rate => rate > 0);
|
||||||
|
|
||||||
|
if (engagementRates.length > 0) {
|
||||||
|
averageEngagementRate = engagementRates.reduce((a, b) => a + b, 0) / engagementRates.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const highEngagementThreshold = averageEngagementRate * 1.5;
|
||||||
|
|
||||||
|
// 转换结果
|
||||||
|
const posts: PopularPostItem[] = [];
|
||||||
|
|
||||||
|
if (Array.isArray(postsData)) {
|
||||||
|
postsData.forEach(post => {
|
||||||
|
const engagementCount = Number(post.engagement_count || 0);
|
||||||
|
const viewsCount = Number(post.views_count || 0);
|
||||||
|
const engagementRate = viewsCount > 0 ? engagementCount / viewsCount : 0;
|
||||||
|
|
||||||
|
posts.push({
|
||||||
|
post_id: post.post_id,
|
||||||
|
title: post.title || '无标题',
|
||||||
|
platform: post.platform || 'unknown',
|
||||||
|
influencer_id: post.influencer_id,
|
||||||
|
influencer_name: post.influencer_name || '未知KOL',
|
||||||
|
publish_date: post.publish_date || '',
|
||||||
|
engagement_count: engagementCount,
|
||||||
|
views_count: viewsCount,
|
||||||
|
engagement_rate: parseFloat(engagementRate.toFixed(4)),
|
||||||
|
is_high_engagement: engagementRate > highEngagementThreshold
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: PopularPostsResponse = {
|
||||||
|
posts,
|
||||||
|
total
|
||||||
|
};
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info('Popular posts data fetched successfully', {
|
||||||
|
duration,
|
||||||
|
resultCount: posts.length,
|
||||||
|
total
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error(`Error in getPopularPosts (${duration}ms)`, error);
|
||||||
|
|
||||||
|
// 发生错误时返回空数据
|
||||||
|
return {
|
||||||
|
posts: [],
|
||||||
|
total: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
|
|||||||
@@ -2529,6 +2529,532 @@ export const openAPISpec = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'/api/analytics/dashboard-cards': {
|
||||||
|
get: {
|
||||||
|
summary: '获取概览卡片数据',
|
||||||
|
description: '返回包含留言总数、平均互动率和情感分析三个核心指标的卡片数据及环比变化',
|
||||||
|
tags: ['Analytics'],
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'timeRange',
|
||||||
|
in: 'query',
|
||||||
|
description: '时间范围(天)',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['7', '30', '90'],
|
||||||
|
default: '30'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'projectId',
|
||||||
|
in: 'query',
|
||||||
|
description: '项目ID过滤',
|
||||||
|
schema: {
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses: {
|
||||||
|
'200': {
|
||||||
|
description: '成功响应',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: true },
|
||||||
|
data: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
comments_count: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
current: { type: 'number', example: 256 },
|
||||||
|
change_percentage: { type: 'number', example: 12.5 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
engagement_rate: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
current: { type: 'number', example: 5.8 },
|
||||||
|
change_percentage: { type: 'number', example: -2.3 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sentiment_score: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
current: { type: 'number', example: 0.75 },
|
||||||
|
change_percentage: { type: 'number', example: 8.7 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'400': {
|
||||||
|
description: '参数错误',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: false },
|
||||||
|
error: { type: 'string', example: 'Invalid timeRange. Must be 7, 30, or 90.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'500': {
|
||||||
|
description: '服务器错误',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: false },
|
||||||
|
error: { type: 'string', example: 'Failed to fetch dashboard card data' },
|
||||||
|
message: { type: 'string', example: 'Internal server error' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'/api/analytics/comment-trend': {
|
||||||
|
get: {
|
||||||
|
summary: '获取留言趋势数据',
|
||||||
|
description: '返回一段时间内留言数量的变化趋势,用于绘制柱状图',
|
||||||
|
tags: ['Analytics'],
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'timeRange',
|
||||||
|
in: 'query',
|
||||||
|
description: '时间范围(天)',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['7', '30', '90'],
|
||||||
|
default: '30'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'projectId',
|
||||||
|
in: 'query',
|
||||||
|
description: '项目ID过滤',
|
||||||
|
schema: {
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'platform',
|
||||||
|
in: 'query',
|
||||||
|
description: '平台过滤',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['Twitter', 'Instagram', 'TikTok', 'Facebook', 'YouTube']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses: {
|
||||||
|
'200': {
|
||||||
|
description: '成功响应',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: true },
|
||||||
|
data: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
date: { type: 'string', format: 'date', example: '2025-03-01' },
|
||||||
|
count: { type: 'number', example: 32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
max_count: { type: 'number', example: 58 },
|
||||||
|
total_count: { type: 'number', example: 256 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'400': {
|
||||||
|
description: '参数错误',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: false },
|
||||||
|
error: { type: 'string', example: 'Invalid timeRange. Must be 7, 30, or 90.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'500': {
|
||||||
|
description: '服务器错误',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: false },
|
||||||
|
error: { type: 'string', example: 'Failed to fetch comment trend data' },
|
||||||
|
message: { type: 'string', example: 'Internal server error' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'/api/analytics/platform-distribution': {
|
||||||
|
get: {
|
||||||
|
summary: '获取平台分布数据',
|
||||||
|
description: '返回不同社交平台上的评论或互动分布情况,用于绘制柱状图',
|
||||||
|
tags: ['Analytics'],
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'timeRange',
|
||||||
|
in: 'query',
|
||||||
|
description: '时间范围(天)',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['7', '30', '90'],
|
||||||
|
default: '30'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'projectId',
|
||||||
|
in: 'query',
|
||||||
|
description: '项目ID过滤',
|
||||||
|
schema: {
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'eventType',
|
||||||
|
in: 'query',
|
||||||
|
description: '事件类型',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['comment', 'like', 'view', 'share'],
|
||||||
|
default: 'comment'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses: {
|
||||||
|
'200': {
|
||||||
|
description: '成功响应',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: true },
|
||||||
|
data: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
platform: { type: 'string', example: 'Instagram' },
|
||||||
|
count: { type: 'number', example: 128 },
|
||||||
|
percentage: { type: 'number', example: 42.5 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
total: { type: 'number', example: 312 },
|
||||||
|
event_type: { type: 'string', example: 'comment' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'400': {
|
||||||
|
description: '参数错误',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: false },
|
||||||
|
error: { type: 'string', example: 'Invalid eventType. Must be one of: comment, like, view, share' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'500': {
|
||||||
|
description: '服务器错误',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: false },
|
||||||
|
error: { type: 'string', example: 'Failed to fetch platform distribution data' },
|
||||||
|
message: { type: 'string', example: 'Internal server error' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'/api/analytics/sentiment-analysis': {
|
||||||
|
get: {
|
||||||
|
summary: '获取情感分析详情',
|
||||||
|
description: '返回正面、中性、负面评论的比例和平均情感得分',
|
||||||
|
tags: ['Analytics'],
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'timeRange',
|
||||||
|
in: 'query',
|
||||||
|
description: '时间范围(天)',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['7', '30', '90'],
|
||||||
|
default: '30'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'projectId',
|
||||||
|
in: 'query',
|
||||||
|
description: '项目ID过滤',
|
||||||
|
schema: {
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'platform',
|
||||||
|
in: 'query',
|
||||||
|
description: '平台过滤',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['Twitter', 'Instagram', 'TikTok', 'Facebook', 'YouTube']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses: {
|
||||||
|
'200': {
|
||||||
|
description: '成功响应',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: true },
|
||||||
|
data: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
positive: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
count: { type: 'number', example: 156 },
|
||||||
|
percentage: { type: 'number', example: 65.2 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
neutral: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
count: { type: 'number', example: 45 },
|
||||||
|
percentage: { type: 'number', example: 20.8 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
negative: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
count: { type: 'number', example: 28 },
|
||||||
|
percentage: { type: 'number', example: 14.0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
total: { type: 'number', example: 229 },
|
||||||
|
average_score: { type: 'number', example: 0.68 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'400': {
|
||||||
|
description: '参数错误',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: false },
|
||||||
|
error: { type: 'string', example: 'Invalid timeRange. Must be 7, 30, or 90.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'500': {
|
||||||
|
description: '服务器错误',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: false },
|
||||||
|
error: { type: 'string', example: 'Failed to fetch sentiment analysis data' },
|
||||||
|
message: { type: 'string', example: 'Internal server error' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'/api/analytics/popular-posts': {
|
||||||
|
get: {
|
||||||
|
summary: '获取热门文章数据',
|
||||||
|
description: '返回按互动数量或互动率排序的热门帖文列表',
|
||||||
|
tags: ['Analytics'],
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'timeRange',
|
||||||
|
in: 'query',
|
||||||
|
description: '时间范围(天)',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['7', '30', '90'],
|
||||||
|
default: '30'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'projectId',
|
||||||
|
in: 'query',
|
||||||
|
description: '项目ID过滤',
|
||||||
|
schema: {
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'platform',
|
||||||
|
in: 'query',
|
||||||
|
description: '平台过滤',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['Twitter', 'Instagram', 'TikTok', 'Facebook', 'YouTube']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sortBy',
|
||||||
|
in: 'query',
|
||||||
|
description: '排序字段',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['engagement_count', 'engagement_rate'],
|
||||||
|
default: 'engagement_count'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'limit',
|
||||||
|
in: 'query',
|
||||||
|
description: '返回数量',
|
||||||
|
schema: {
|
||||||
|
type: 'integer',
|
||||||
|
default: 10,
|
||||||
|
maximum: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses: {
|
||||||
|
'200': {
|
||||||
|
description: '成功响应',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: true },
|
||||||
|
data: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
post_id: { type: 'string', example: 'post_123' },
|
||||||
|
title: { type: 'string', example: '新产品发布' },
|
||||||
|
platform: { type: 'string', example: 'Instagram' },
|
||||||
|
influencer_id: { type: 'string', example: 'inf_456' },
|
||||||
|
influencer_name: { type: 'string', example: '时尚KOL' },
|
||||||
|
publish_date: { type: 'string', example: '2025-03-10 10:30:00' },
|
||||||
|
engagement_count: { type: 'number', example: 2350 },
|
||||||
|
views_count: { type: 'number', example: 15600 },
|
||||||
|
engagement_rate: { type: 'number', example: 0.1506 },
|
||||||
|
is_high_engagement: { type: 'boolean', example: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
total: { type: 'number', example: 45 },
|
||||||
|
high_engagement_count: { type: 'number', example: 8 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'400': {
|
||||||
|
description: '参数错误',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: false },
|
||||||
|
error: { type: 'string', example: 'Invalid sortBy. Must be engagement_count or engagement_rate.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'500': {
|
||||||
|
description: '服务器错误',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: false },
|
||||||
|
error: { type: 'string', example: 'Failed to fetch popular posts data' },
|
||||||
|
message: { type: 'string', example: 'Internal server error' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
|||||||
Reference in New Issue
Block a user