获取概览卡片数据 (/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
|
||||
|
||||
@@ -32,4 +32,19 @@ analyticsRouter.get('/kol-funnel', (c) => analyticsController.getKolFunnel(c));
|
||||
// Add new post performance route
|
||||
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;
|
||||
@@ -83,6 +83,102 @@ export interface PostPerformanceResponse {
|
||||
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
|
||||
*/
|
||||
@@ -887,6 +983,749 @@ export class AnalyticsService {
|
||||
|
||||
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
|
||||
|
||||
@@ -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: {
|
||||
schemas: {
|
||||
|
||||
Reference in New Issue
Block a user