获取概览卡片数据 (/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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user