Compare commits

..

6 Commits

Author SHA1 Message Date
4056bac3ab content performance 2025-03-14 23:23:47 +08:00
d0f2ab0620 interaction api 2025-03-14 22:31:45 +08:00
bd1a5ce384 hotkeyword 2025-03-14 22:02:10 +08:00
942fb592b5 morte change 2025-03-14 21:10:50 +08:00
5a03323c69 add comment table 2025-03-14 20:39:38 +08:00
853ce79e16 rename ch create table sql name 2025-03-14 20:12:34 +08:00
7 changed files with 2405 additions and 266 deletions

View File

@@ -56,7 +56,7 @@
- 条形长度直观反映各平台占比
- 帮助团队了解哪些平台效果更好
# 审核状态分布 [先不做]
# 审核状态分布 [已实现]
- 环形图展示内容审核状态的分布情况
- 包括三种状态:已核准、待审核、已拒绝

View File

@@ -134,7 +134,7 @@ CREATE TABLE IF NOT EXISTS posts (
PRIMARY KEY (post_id)
) ENGINE = MergeTree();
CREATE TABLE IF NOT EXISTS promote.sync_logs (
CREATE TABLE IF NOT EXISTS sync_logs (
timestamp DateTime DEFAULT now(),
duration_ms UInt64,
posts_synced UInt32,
@@ -145,4 +145,64 @@ CREATE TABLE IF NOT EXISTS promote.sync_logs (
error_messages String
) ENGINE = MergeTree()
ORDER BY
(timestamp)
(timestamp);
-- 创建专门的comments表存储评论数据
CREATE TABLE IF NOT EXISTS comments (
-- 基本标识信息
comment_id String,
-- 评论唯一ID
post_id String,
-- 关联的帖子ID
user_id String,
-- 发表评论的用户ID
-- 时间信息
created_at DateTime DEFAULT now(),
-- 评论创建时间
date Date DEFAULT toDate(created_at),
-- 日期(用于分区)
-- 评论内容信息
content String,
-- 评论内容
sentiment_score Float64,
-- 情感分数(-1到1)
sentiment Enum8(
-- 情感分类
'positive' = 1,
'neutral' = 2,
'negative' = 3
),
-- 关联信息
project_id String,
-- 项目ID
influencer_id String,
-- 网红ID
platform String,
-- 评论所在平台
-- 互动信息
likes_count UInt32 DEFAULT 0,
-- 点赞数量
replies_count UInt32 DEFAULT 0,
-- 回复数量
-- 元数据
parent_comment_id String DEFAULT '',
-- 父评论ID(用于回复)
is_reply UInt8 DEFAULT 0,
-- 是否为回复(0=否1=是)
-- 同步信息
is_synced UInt8 DEFAULT 1,
-- 是否已同步(0=否1=是)
last_updated DateTime DEFAULT now(),
-- 最后更新时间
-- 分析信息
keywords Array(String),
-- 提取的关键词
topics Array(String),
-- 关联的话题
-- 内部处理信息
is_active UInt8 DEFAULT 1,
-- 是否活跃(0=已删除/隐藏1=活跃)
is_spam UInt8 DEFAULT 0 -- 是否为垃圾评论(0=否1=是)
) ENGINE = MergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(post_id, created_at, comment_id) SETTINGS index_granularity = 8192;

View File

@@ -678,6 +678,313 @@ export class AnalyticsController {
}, 500);
}
}
/**
* 获取内容审核状态分布数据
* 返回已批准、待审核和已拒绝内容的数量和比例
*
* @param c Hono Context
* @returns Response with moderation status distribution data
*/
async getModerationStatus(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 contentType = c.req.query('contentType') || 'all'; // 内容类型post, comment, all
logger.info(`[${requestId}] Moderation status distribution request received`, {
timeRange,
projectId,
contentType,
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 (!['post', 'comment', 'all'].includes(contentType)) {
logger.warn(`[${requestId}] Invalid contentType: ${contentType}`);
return c.json({
success: false,
error: 'Invalid contentType. Must be post, comment, or all.'
}, 400);
}
// 获取审核状态分布数据
const data = await analyticsService.getModerationStatusDistribution(
parseInt(timeRange, 10),
projectId,
contentType
);
// 返回成功响应
const duration = Date.now() - startTime;
logger.info(`[${requestId}] Moderation status distribution response sent successfully`, {
duration,
statuses: Object.keys(data.statuses),
total: data.total
});
return c.json({
success: true,
data: data
});
} catch (error) {
// 记录错误
const duration = Date.now() - startTime;
logger.error(`[${requestId}] Error fetching moderation status distribution (${duration}ms)`, error);
// 返回错误响应
return c.json({
success: false,
error: 'Failed to fetch moderation status distribution data',
message: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
}
/**
* 获取热门关键字数据
* 返回按出现频率排序的热门关键字列表
*
* @param c Hono Context
* @returns Response with hot keywords data
*/
async getHotKeywords(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 limit = parseInt(c.req.query('limit') || '20', 10); // 默认返回20个关键字
logger.info(`[${requestId}] Hot keywords request received`, {
timeRange,
projectId,
platform,
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);
}
// 调用服务获取热门关键词数据
const hotKeywordsData = await analyticsService.getHotKeywords(
parseInt(timeRange, 10),
projectId,
platform,
limit
);
const response = {
success: true,
data: hotKeywordsData.data,
metadata: {
total: hotKeywordsData.total,
is_mock_data: false
}
};
const duration = Date.now() - startTime;
logger.info(`[${requestId}] Hot keywords request completed in ${duration}ms`);
return c.json(response);
} catch (error) {
const duration = Date.now() - startTime;
logger.error(`[${requestId}] Error getting hot keywords: ${error}`, {
error: String(error),
duration
});
return c.json({
success: false,
error: 'Failed to fetch hot keywords data'
}, 500);
}
}
/**
* 获取用户互动时间分析数据
* 返回按小时统计的用户互动数量和分布
*
* @param c Hono Context
* @returns Response with interaction time analysis data
*/
async getInteractionTimeAnalysis(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 eventType = c.req.query('eventType') || 'all'; // 可选事件类型过滤
logger.info(`[${requestId}] Interaction time analysis request received`, {
timeRange,
projectId,
platform,
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 = ['all', 'comment', 'like', 'view', 'share', 'follow'];
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.getInteractionTimeAnalysis(
parseInt(timeRange, 10),
projectId,
platform,
eventType
);
// 返回成功响应
const duration = Date.now() - startTime;
logger.info(`[${requestId}] Interaction time analysis response sent successfully`, {
duration,
total: data.total,
peak_hour: data.peak_hour
});
return c.json({
success: true,
data: data.data,
metadata: {
total: data.total,
peak_hour: data.peak_hour,
lowest_hour: data.lowest_hour
}
});
} catch (error) {
// 记录错误
const duration = Date.now() - startTime;
logger.error(`[${requestId}] Error fetching interaction time analysis (${duration}ms)`, error);
// 返回错误响应
return c.json({
success: false,
error: 'Failed to fetch interaction time analysis data',
message: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
}
/**
* 获取内容表现分析数据
* 提供内容覆盖量、互动率、互动量等散点图数据,用于四象限分析
*
* @param c Hono Context
* @returns Response with content performance data
*/
async getContentPerformance(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 kolId = c.req.query('kolId'); // 可选KOL过滤
const contentType = c.req.query('contentType') || 'all'; // 内容类型过滤post、video、article或all
const limit = parseInt(c.req.query('limit') || '100', 10); // 默认返回100条内容
logger.info(`[${requestId}] Content performance analysis request received`, {
timeRange,
projectId,
platform,
kolId,
contentType,
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);
}
// 调用服务获取内容表现分析数据
const contentPerformanceData = await analyticsService.getContentPerformance(
parseInt(timeRange, 10),
projectId,
platform,
kolId,
contentType,
limit
);
// 记录请求完成
const duration = Date.now() - startTime;
logger.info(`[${requestId}] Content performance analysis request completed in ${duration}ms`, {
itemCount: contentPerformanceData.data.length,
metadata: contentPerformanceData.metadata
});
return c.json({
success: true,
data: contentPerformanceData.data,
metadata: contentPerformanceData.metadata
});
} catch (error) {
// 记录错误
const duration = Date.now() - startTime;
logger.error(`[${requestId}] Error fetching content performance data (${duration}ms)`, error);
return c.json({
success: false,
error: 'Failed to fetch content performance data',
message: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
}
}
// Export singleton instance

View File

@@ -47,4 +47,16 @@ analyticsRouter.get('/sentiment-analysis', (c) => analyticsController.getSentime
// Add popular posts route
analyticsRouter.get('/popular-posts', (c) => analyticsController.getPopularPosts(c));
// Add moderation status distribution route
analyticsRouter.get('/moderation-status', (c) => analyticsController.getModerationStatus(c));
// Add hot keywords route
analyticsRouter.get('/hot-keywords', (c) => analyticsController.getHotKeywords(c));
// Add interaction time analysis route
analyticsRouter.get('/interaction-time', (c) => analyticsController.getInteractionTimeAnalysis(c));
// Add content performance analysis route
analyticsRouter.get('/content-performance', (c) => analyticsController.getContentPerformance(c));
export default analyticsRouter;

View File

@@ -179,6 +179,108 @@ export interface PopularPostsResponse {
total: number;
}
/**
* Sentiment analysis response
*/
export interface SentimentAnalysisResponse {
positive_percentage: number;
neutral_percentage: number;
negative_percentage: number;
average_score: number;
total: number;
}
/**
* Moderation status response
*/
export interface ModerationStatusResponse {
statuses: {
approved: number;
pending: number;
rejected: number;
};
percentages: {
approved: number;
pending: number;
rejected: number;
};
total: number;
}
/**
* Represents a hot keyword item with count, percentage and sentiment score
*/
export interface HotKeywordItem {
keyword: string; // 关键词
count: number; // 出现次数
percentage: number; // 占比(%)
sentiment_score: number; // 情感分数
}
/**
* Response structure for hot keywords
*/
export interface HotKeywordsResponse {
data: HotKeywordItem[]; // 热门关键词数据
total: number; // 总数
}
/**
* 用户互动时间分布数据项
*/
export interface InteractionTimeItem {
hour: number; // 小时 (0-23)
count: number; // 互动数量
percentage: number; // 占总数百分比
}
/**
* 用户互动时间分析响应
*/
export interface InteractionTimeResponse {
data: InteractionTimeItem[]; // 按小时统计的互动数据
peak_hour: number; // 峰值时段
lowest_hour: number; // 最低时段
total: number; // 总互动数
}
/**
* 内容表现分析项目数据
*/
export interface ContentPerformanceItem {
content_id: string;
title: string;
platform: string;
content_type: string;
influencer_name: string;
publish_date: string;
coverage: number; // 内容覆盖量(如阅读量/浏览量)
interaction_rate: number; // 互动率(互动总数/覆盖量)
interaction_count: number; // 互动总量(点赞+评论+分享)
likes: number;
comments: number;
shares: number;
quadrant: 'high_value' | 'high_coverage' | 'high_engagement' | 'low_performance'; // 四象限分类
}
/**
* 内容表现分析响应数据
*/
export interface ContentPerformanceResponse {
data: ContentPerformanceItem[];
metadata: {
total: number;
average_coverage: number;
average_interaction_rate: number;
quadrant_counts: {
high_value: number;
high_coverage: number;
high_engagement: number;
low_performance: number;
}
};
}
/**
* Analytics service for KOL performance data
*/
@@ -1652,6 +1754,706 @@ export class AnalyticsService {
};
}
}
/**
* 获取内容审核状态分布数据
* @param timeRange 时间范围(天数)
* @param projectId 可选项目ID过滤
* @param contentType 内容类型过滤post, comment, all
* @returns 审核状态分布数据
*/
async getModerationStatusDistribution(
timeRange: number,
projectId?: string,
contentType: string = 'all'
): Promise<ModerationStatusResponse> {
const startTime = Date.now();
logger.info('Fetching moderation status distribution', { timeRange, projectId, contentType });
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}'`);
if (projectId) {
filters.push(`project_id = '${projectId}'`);
}
// 基于内容类型的过滤
let typeFilter = '';
if (contentType === 'post') {
typeFilter = "AND content_type IN ('video', 'image', 'text', 'story', 'reel', 'live')";
} else if (contentType === 'comment') {
typeFilter = "AND event_type = 'comment'";
}
const filterCondition = filters.join(' AND ');
// 查询审核状态分布
const query = `
SELECT
content_status,
count() as status_count
FROM
events
WHERE
${filterCondition}
AND content_status IN ('approved', 'pending', 'rejected')
${typeFilter}
GROUP BY
content_status
`;
logger.debug('Executing ClickHouse query for moderation status distribution', {
query: query.replace(/\n\s+/g, ' ').trim()
});
// 执行查询
const results = await this.executeClickhouseQuery(query);
// 初始化结果对象
const statusCounts = {
approved: 0,
pending: 0,
rejected: 0
};
// 解析结果
results.forEach(row => {
const status = row.content_status.toLowerCase();
if (status in statusCounts) {
statusCounts[status as keyof typeof statusCounts] = Number(row.status_count);
}
});
// 计算总数和百分比
const total = statusCounts.approved + statusCounts.pending + statusCounts.rejected;
const calculatePercentage = (count: number): number => {
if (total === 0) return 0;
return parseFloat(((count / total) * 100).toFixed(1));
};
const statusPercentages = {
approved: calculatePercentage(statusCounts.approved),
pending: calculatePercentage(statusCounts.pending),
rejected: calculatePercentage(statusCounts.rejected)
};
const result: ModerationStatusResponse = {
statuses: statusCounts,
percentages: statusPercentages,
total
};
const duration = Date.now() - startTime;
logger.info('Moderation status distribution fetched successfully', {
duration,
total,
statusDistribution: statusCounts
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
logger.error(`Error in getModerationStatusDistribution (${duration}ms)`, error);
// 发生错误时返回默认响应
return {
statuses: { approved: 0, pending: 0, rejected: 0 },
percentages: { approved: 0, pending: 0, rejected: 0 },
total: 0
};
}
}
/**
* Get hot keywords from comments with their frequency and sentiment scores
* @param timeRange Time range in days (7, 30, 90)
* @param projectId Optional project ID to filter by
* @param platform Optional platform to filter by
* @param limit Maximum number of keywords to return
* @returns Hot keywords with their counts, percentages and sentiment scores
*/
async getHotKeywords(
timeRange: number,
projectId?: string,
platform?: string,
limit: number = 20
): Promise<HotKeywordsResponse> {
try {
const startDate = new Date();
startDate.setDate(startDate.getDate() - timeRange);
const formattedStartDate = this.formatDateForClickhouse(startDate);
// Build the WHERE clause with filters
const filters = [`event_time >= '${formattedStartDate}'`];
if (projectId) {
filters.push(`project_id = '${projectId}'`);
}
if (platform) {
filters.push(`platform = '${platform}'`);
}
// Add filter for comment events only
filters.push(`event_type = 'comment'`);
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
// Query to extract keywords from comments and count their occurrences
const keywordsQuery = `
WITH extracted_keywords AS (
SELECT
arrayJoin(extractAll(comment_text, '[\\\\p{L}\\\\p{N}]{2,}')) AS keyword,
sentiment_score
FROM events
${whereClause}
WHERE comment_text != ''
)
SELECT
keyword,
count() AS count,
avg(sentiment_score) AS avg_sentiment_score
FROM extracted_keywords
GROUP BY keyword
ORDER BY count DESC
LIMIT ${limit}
`;
// Execute the query
const keywordsData = await this.executeClickhouseQuery(keywordsQuery);
// Calculate total count for percentages
const totalCount = keywordsData.reduce((sum, item) => sum + item.count, 0);
// Format the response
const hotKeywords: HotKeywordItem[] = keywordsData.map(item => ({
keyword: item.keyword,
count: item.count,
percentage: parseFloat(((item.count / totalCount) * 100).toFixed(1)),
sentiment_score: parseFloat(item.avg_sentiment_score.toFixed(1))
}));
return {
data: hotKeywords,
total: totalCount
};
} catch (error) {
logger.error(`Error fetching hot keywords: ${error}`);
// Return mock data in case of error
const mockKeywords: HotKeywordItem[] = [
{ keyword: '价格', count: 45, percentage: 22.5, sentiment_score: 0.2 },
{ keyword: '质量', count: 38, percentage: 19.0, sentiment_score: 0.7 },
{ keyword: '服务', count: 32, percentage: 16.0, sentiment_score: -0.3 },
{ keyword: '快递', count: 28, percentage: 14.0, sentiment_score: 0.1 },
{ keyword: '推荐', count: 24, percentage: 12.0, sentiment_score: 0.8 },
{ keyword: '问题', count: 18, percentage: 9.0, sentiment_score: -0.6 },
{ keyword: '体验', count: 15, percentage: 7.5, sentiment_score: 0.4 }
];
return {
data: mockKeywords,
total: 100
};
}
}
/**
* 获取用户互动时间分析数据
* @param timeRange 时间范围(天数)
* @param projectId 可选项目ID
* @param platform 可选平台
* @param eventType 互动事件类型
* @returns 互动时间分析数据
*/
async getInteractionTimeAnalysis(
timeRange: number,
projectId?: string,
platform?: string,
eventType: string = 'all'
): Promise<InteractionTimeResponse> {
const startTime = Date.now();
logger.info('Fetching interaction time analysis data', { timeRange, projectId, platform, eventType });
try {
// 计算时间范围
const startDate = new Date();
startDate.setDate(startDate.getDate() - timeRange);
const formattedStartDate = this.formatDateForClickhouse(startDate);
// 构建查询条件
const filters = [`event_time >= '${formattedStartDate}'`];
if (projectId) {
filters.push(`project_id = '${projectId}'`);
}
if (platform) {
filters.push(`platform = '${platform}'`);
}
// 根据事件类型过滤
if (eventType !== 'all') {
filters.push(`event_type = '${eventType}'`);
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
// 按小时分组查询互动数量
const query = `
SELECT
toHour(event_time) AS hour,
count() AS count
FROM
events
${whereClause}
GROUP BY
hour
ORDER BY
hour ASC
`;
logger.debug('Executing ClickHouse query for interaction time analysis', {
query: query.replace(/\n\s+/g, ' ').trim()
});
// 执行查询
const result = await this.executeClickhouseQuery(query);
// 处理结果确保所有24小时都有数据
const hourlyData: { [hour: number]: number } = {};
// 初始化所有小时为0
for (let i = 0; i < 24; i++) {
hourlyData[i] = 0;
}
// 填充查询结果
result.forEach(item => {
hourlyData[item.hour] = item.count;
});
// 计算总数和百分比
const total = Object.values(hourlyData).reduce((sum, count) => sum + count, 0);
// 找出峰值和最低时段
let peakHour = 0;
let lowestHour = 0;
let maxCount = 0;
let minCount = Number.MAX_SAFE_INTEGER;
Object.entries(hourlyData).forEach(([hour, count]) => {
const hourNum = parseInt(hour, 10);
if (count > maxCount) {
maxCount = count;
peakHour = hourNum;
}
if (count < minCount) {
minCount = count;
lowestHour = hourNum;
}
});
// 格式化响应数据
const data: InteractionTimeItem[] = Object.entries(hourlyData).map(([hour, count]) => ({
hour: parseInt(hour, 10),
count,
percentage: total > 0 ? parseFloat(((count / total) * 100).toFixed(1)) : 0
}));
const response: InteractionTimeResponse = {
data,
peak_hour: peakHour,
lowest_hour: lowestHour,
total
};
const duration = Date.now() - startTime;
logger.info('Interaction time analysis data fetched successfully', {
duration,
total,
peak_hour: peakHour
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
logger.error(`Error in getInteractionTimeAnalysis (${duration}ms)`, error);
// 发生错误时返回模拟数据
const mockData: InteractionTimeItem[] = Array.from({ length: 24 }, (_, i) => {
// 创建合理的模拟数据分布,早晨和晚上的互动较多
let count = 0;
if (i >= 7 && i <= 10) { // 早晨
count = Math.floor(Math.random() * 100) + 150;
} else if (i >= 17 && i <= 22) { // 晚上
count = Math.floor(Math.random() * 100) + 200;
} else { // 其他时间
count = Math.floor(Math.random() * 100) + 50;
}
return {
hour: i,
count,
percentage: 0 // 暂时设为0稍后计算
};
});
// 计算总数和百分比
const total = mockData.reduce((sum, item) => sum + item.count, 0);
mockData.forEach(item => {
item.percentage = parseFloat(((item.count / total) * 100).toFixed(1));
});
// 找出峰值和最低时段
let peakHour = 0;
let lowestHour = 0;
let maxCount = 0;
let minCount = Number.MAX_SAFE_INTEGER;
mockData.forEach(item => {
if (item.count > maxCount) {
maxCount = item.count;
peakHour = item.hour;
}
if (item.count < minCount) {
minCount = item.count;
lowestHour = item.hour;
}
});
return {
data: mockData,
peak_hour: peakHour,
lowest_hour: lowestHour,
total
};
}
}
/**
* 获取内容表现分析数据
*
* @param timeRange 时间范围(天数)
* @param projectId 可选项目ID
* @param platform 可选平台
* @param kolId 可选KOL ID
* @param contentType 内容类型过滤
* @param limit 最大返回条数
* @returns 内容表现分析数据
*/
async getContentPerformance(
timeRange: number,
projectId?: string,
platform?: string,
kolId?: string,
contentType: string = 'all',
limit: number = 100
): Promise<ContentPerformanceResponse> {
const startTime = Date.now();
logger.info('Fetching content performance data', { timeRange, projectId, platform, kolId, contentType, limit });
try {
// 计算日期范围
const currentDate = new Date();
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - timeRange);
const currentDateStr = this.formatDateForClickhouse(currentDate);
const pastDateStr = this.formatDateForClickhouse(pastDate);
// 构建基础过滤条件
const filters = [
`created_at BETWEEN '${pastDateStr}' AND '${currentDateStr}'`
];
if (projectId) {
filters.push(`project_id = '${projectId}'`);
}
if (platform) {
filters.push(`platform = '${platform}'`);
}
if (kolId) {
filters.push(`influencer_id = '${kolId}'`);
}
if (contentType !== 'all') {
filters.push(`content_type = '${contentType}'`);
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
// 查询内容数据及性能指标
const contentQuery = `
WITH content_metrics AS (
SELECT
c.content_id,
c.title,
c.platform,
c.content_type,
c.influencer_id,
i.name AS influencer_name,
toString(c.created_at) AS publish_date,
c.views AS coverage,
countIf(e.event_type = 'like') AS likes,
countIf(e.event_type = 'comment') AS comments,
countIf(e.event_type = 'share') AS shares,
count() AS total_interactions
FROM
content c
LEFT JOIN
influencers i ON c.influencer_id = i.influencer_id
LEFT JOIN
events e ON c.content_id = e.content_id AND e.event_time BETWEEN '${pastDateStr}' AND '${currentDateStr}'
${whereClause}
GROUP BY
c.content_id, c.title, c.platform, c.content_type, c.influencer_id, i.name, c.created_at, c.views
)
SELECT
content_id,
title,
platform,
content_type,
influencer_id,
influencer_name,
publish_date,
coverage,
likes,
comments,
shares,
total_interactions,
coverage > 0 ? total_interactions / coverage : 0 AS interaction_rate
FROM
content_metrics
WHERE
coverage > 0
ORDER BY
total_interactions DESC
LIMIT ${limit}
`;
// 执行查询
logger.debug('Executing ClickHouse query for content performance', {
query: contentQuery.replace(/\n\s+/g, ' ').trim()
});
const contentData = await this.executeClickhouseQuery(contentQuery);
// 如果没有数据,返回模拟数据
if (!Array.isArray(contentData) || contentData.length === 0) {
return this.generateMockContentPerformance(limit);
}
// 处理结果,计算平均值和四象限分类
const processedData: ContentPerformanceItem[] = [];
let totalCoverage = 0;
let totalInteractionRate = 0;
// 处理每个内容项
contentData.forEach(item => {
const coverage = Number(item.coverage || 0);
const likes = Number(item.likes || 0);
const comments = Number(item.comments || 0);
const shares = Number(item.shares || 0);
const interactionCount = likes + comments + shares;
const interactionRate = coverage > 0 ? interactionCount / coverage : 0;
totalCoverage += coverage;
totalInteractionRate += interactionRate;
processedData.push({
content_id: item.content_id,
title: item.title || '无标题',
platform: item.platform || 'unknown',
content_type: item.content_type || 'post',
influencer_name: item.influencer_name || '未知KOL',
publish_date: item.publish_date || '',
coverage,
interaction_rate: interactionRate,
interaction_count: interactionCount,
likes,
comments,
shares,
quadrant: 'low_performance' // 临时值,稍后更新
});
});
// 计算平均值
const averageCoverage = processedData.length > 0 ? totalCoverage / processedData.length : 0;
const averageInteractionRate = processedData.length > 0 ? totalInteractionRate / processedData.length : 0;
// 根据平均值确定每个内容的四象限分类
const quadrantCounts = {
high_value: 0,
high_coverage: 0,
high_engagement: 0,
low_performance: 0
};
processedData.forEach(item => {
if (item.coverage > averageCoverage && item.interaction_rate > averageInteractionRate) {
// 高覆盖 + 高互动率 = 高价值
item.quadrant = 'high_value';
quadrantCounts.high_value++;
} else if (item.coverage > averageCoverage) {
// 高覆盖 + 低互动率 = 高覆盖
item.quadrant = 'high_coverage';
quadrantCounts.high_coverage++;
} else if (item.interaction_rate > averageInteractionRate) {
// 低覆盖 + 高互动率 = 高互动
item.quadrant = 'high_engagement';
quadrantCounts.high_engagement++;
} else {
// 低覆盖 + 低互动率 = 低表现
item.quadrant = 'low_performance';
quadrantCounts.low_performance++;
}
});
// 构建返回结果
const result: ContentPerformanceResponse = {
data: processedData,
metadata: {
total: processedData.length,
average_coverage: parseFloat(averageCoverage.toFixed(2)),
average_interaction_rate: parseFloat(averageInteractionRate.toFixed(4)),
quadrant_counts: quadrantCounts
}
};
const duration = Date.now() - startTime;
logger.info(`Content performance data fetched successfully in ${duration}ms`, {
itemCount: processedData.length,
averages: { coverage: averageCoverage, interactionRate: averageInteractionRate },
quadrantCounts
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
logger.error(`Error in getContentPerformance (${duration}ms)`, error);
// 发生错误时返回模拟数据
return this.generateMockContentPerformance(limit);
}
}
/**
* 生成模拟内容表现分析数据
* 当实际数据获取失败时使用
*/
private generateMockContentPerformance(limit: number = 100): ContentPerformanceResponse {
const platforms = ['微博', '微信', '抖音', '小红书', '知乎'];
const contentTypes = ['post', 'video', 'article'];
const titles = [
'为什么越来越多的年轻人选择使用我们的产品',
'产品新功能发布,颠覆你的使用体验',
'用户分享:我是如何通过这款产品提升效率的',
'揭秘:产品背后的设计理念',
'五分钟了解产品如何解决你的痛点问题',
'实用指南:产品高级功能详解',
'权威评测产品vs竞品全方位对比',
'独家:产品未来发展方向预测',
'创始人专访:产品诞生的故事',
'用户案例:产品如何改变工作方式'
];
const mockData: ContentPerformanceItem[] = [];
let totalCoverage = 0;
let totalInteractionRate = 0;
const randomDate = (start: Date, end: Date) => {
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
};
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 90);
for (let i = 0; i < limit; i++) {
// 生成随机数据
const coverage = Math.floor(Math.random() * 50000) + 1000;
const likes = Math.floor(Math.random() * coverage * 0.2);
const comments = Math.floor(Math.random() * coverage * 0.05);
const shares = Math.floor(Math.random() * coverage * 0.03);
const interactionCount = likes + comments + shares;
const interactionRate = interactionCount / coverage;
totalCoverage += coverage;
totalInteractionRate += interactionRate;
mockData.push({
content_id: `content_${i + 1}`,
title: titles[i % titles.length],
platform: platforms[Math.floor(Math.random() * platforms.length)],
content_type: contentTypes[Math.floor(Math.random() * contentTypes.length)],
influencer_name: `KOL ${i % 10 + 1}`,
publish_date: randomDate(startDate, endDate).toISOString(),
coverage,
interaction_rate: interactionRate,
interaction_count: interactionCount,
likes,
comments,
shares,
quadrant: 'low_performance' // 临时值,稍后更新
});
}
// 计算平均值
const averageCoverage = mockData.length > 0 ? totalCoverage / mockData.length : 0;
const averageInteractionRate = mockData.length > 0 ? totalInteractionRate / mockData.length : 0;
// 根据平均值确定每个内容的四象限分类
const quadrantCounts = {
high_value: 0,
high_coverage: 0,
high_engagement: 0,
low_performance: 0
};
mockData.forEach(item => {
if (item.coverage > averageCoverage && item.interaction_rate > averageInteractionRate) {
// 高覆盖 + 高互动率 = 高价值
item.quadrant = 'high_value';
quadrantCounts.high_value++;
} else if (item.coverage > averageCoverage) {
// 高覆盖 + 低互动率 = 高覆盖
item.quadrant = 'high_coverage';
quadrantCounts.high_coverage++;
} else if (item.interaction_rate > averageInteractionRate) {
// 低覆盖 + 高互动率 = 高互动
item.quadrant = 'high_engagement';
quadrantCounts.high_engagement++;
} else {
// 低覆盖 + 低互动率 = 低表现
item.quadrant = 'low_performance';
quadrantCounts.low_performance++;
}
});
// 构建返回结果
return {
data: mockData,
metadata: {
total: mockData.length,
average_coverage: parseFloat(averageCoverage.toFixed(2)),
average_interaction_rate: parseFloat(averageInteractionRate.toFixed(4)),
quadrant_counts: quadrantCounts
}
};
}
}
// Export singleton instance

View File

@@ -2923,14 +2923,14 @@ export const openAPISpec = {
},
'/api/analytics/popular-posts': {
get: {
summary: '获取热门文数据',
description: '返回按互动数量或互动率排序的热门帖文列表',
summary: '获取热门文数据',
description: '返回按互动数量或互动率排序的热门帖文',
tags: ['Analytics'],
parameters: [
{
name: 'timeRange',
in: 'query',
description: '时间范围(天)',
description: '时间范围(天)',
schema: {
type: 'string',
enum: ['7', '30', '90'],
@@ -2940,7 +2940,7 @@ export const openAPISpec = {
{
name: 'projectId',
in: 'query',
description: '项目ID过滤',
description: '项目ID',
schema: {
type: 'string'
}
@@ -2948,10 +2948,9 @@ export const openAPISpec = {
{
name: 'platform',
in: 'query',
description: '平台过滤',
description: '平台',
schema: {
type: 'string',
enum: ['Twitter', 'Instagram', 'TikTok', 'Facebook', 'YouTube']
type: 'string'
}
},
{
@@ -2967,7 +2966,7 @@ export const openAPISpec = {
{
name: 'limit',
in: 'query',
description: '返回数量',
description: '返回数量限制',
schema: {
type: 'integer',
default: 10,
@@ -2977,36 +2976,58 @@ export const openAPISpec = {
],
responses: {
'200': {
description: '成功响应',
description: '热门帖文数据',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
success: {
type: 'boolean'
},
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 }
title: {
type: 'string'
},
platform: {
type: 'string'
},
influencer_name: {
type: 'string'
},
publish_date: {
type: 'string',
format: 'date-time'
},
engagement_count: {
type: 'integer'
},
views_count: {
type: 'integer'
},
engagement_rate: {
type: 'number',
format: 'float'
},
is_high_engagement: {
type: 'boolean'
}
}
}
},
metadata: {
type: 'object',
properties: {
total: { type: 'number', example: 45 },
high_engagement_count: { type: 'number', example: 8 }
total: {
type: 'integer'
},
high_engagement_count: {
type: 'integer'
}
}
}
}
@@ -3015,14 +3036,262 @@ export const openAPISpec = {
}
},
'400': {
description: '参数错误',
description: '请求参数错误',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
},
'500': {
description: '服务器错误',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
}
}
}
},
'/api/analytics/moderation-status': {
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: 'contentType',
in: 'query',
description: '内容类型',
schema: {
type: 'string',
enum: ['post', 'comment', 'all'],
default: 'all'
}
}
],
responses: {
'200': {
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.' }
success: {
type: 'boolean'
},
data: {
type: 'object',
properties: {
statuses: {
type: 'object',
properties: {
approved: {
type: 'integer',
description: '已批准内容数量'
},
pending: {
type: 'integer',
description: '待审核内容数量'
},
rejected: {
type: 'integer',
description: '已拒绝内容数量'
}
}
},
percentages: {
type: 'object',
properties: {
approved: {
type: 'number',
format: 'float',
description: '已批准内容百分比'
},
pending: {
type: 'number',
format: 'float',
description: '待审核内容百分比'
},
rejected: {
type: 'number',
format: 'float',
description: '已拒绝内容百分比'
}
}
},
total: {
type: 'integer',
description: '内容总数'
}
}
}
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
},
'500': {
description: '服务器错误',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
}
}
}
},
'/api/analytics/hot-keywords': {
get: {
summary: '获取热门关键词',
description: '返回按出现频率排序的热门关键词列表,包含关键词、出现次数、占比和情感分数',
tags: ['Analytics'],
parameters: [
{
name: 'timeRange',
in: 'query',
description: '时间范围(天)',
schema: {
type: 'integer',
enum: [7, 30, 90],
default: 30
}
},
{
name: 'projectId',
in: 'query',
description: '项目ID可选',
schema: {
type: 'string'
}
},
{
name: 'platform',
in: 'query',
description: '平台(可选)',
schema: {
type: 'string',
enum: ['weibo', 'xiaohongshu', 'douyin', 'bilibili']
}
},
{
name: 'limit',
in: 'query',
description: '返回关键词数量上限',
schema: {
type: 'integer',
default: 20
}
}
],
responses: {
'200': {
description: '成功获取热门关键词',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: {
type: 'boolean',
example: true
},
data: {
type: 'array',
items: {
type: 'object',
properties: {
keyword: {
type: 'string',
example: '质量'
},
count: {
type: 'integer',
example: 38
},
percentage: {
type: 'number',
format: 'float',
example: 19.0
},
sentiment_score: {
type: 'number',
format: 'float',
example: 0.7
}
}
}
},
metadata: {
type: 'object',
properties: {
total: {
type: 'integer',
example: 100
},
is_mock_data: {
type: 'boolean',
example: false
}
}
}
}
}
}
}
},
'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.'
}
}
}
}
@@ -3035,9 +3304,14 @@ export const openAPISpec = {
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' }
success: {
type: 'boolean',
example: false
},
error: {
type: 'string',
example: 'Failed to fetch hot keywords data'
}
}
}
}
@@ -3046,6 +3320,349 @@ export const openAPISpec = {
}
}
},
'/api/analytics/interaction-time': {
get: {
summary: '获取用户互动时间分析',
description: '返回按小时统计的用户互动数量和分布,帮助了解用户最活跃的时间段',
tags: ['Analytics'],
parameters: [
{
name: 'timeRange',
in: 'query',
description: '时间范围(天)',
schema: {
type: 'integer',
enum: [7, 30, 90],
default: 30
}
},
{
name: 'projectId',
in: 'query',
description: '项目ID可选',
schema: {
type: 'string'
}
},
{
name: 'platform',
in: 'query',
description: '平台(可选)',
schema: {
type: 'string',
enum: ['weibo', 'xiaohongshu', 'douyin', 'bilibili']
}
},
{
name: 'eventType',
in: 'query',
description: '互动事件类型',
schema: {
type: 'string',
enum: ['all', 'comment', 'like', 'view', 'share', 'follow'],
default: 'all'
}
}
],
responses: {
'200': {
description: '成功获取互动时间分析数据',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: {
type: 'boolean',
example: true
},
data: {
type: 'array',
items: {
type: 'object',
properties: {
hour: {
type: 'integer',
example: 20
},
count: {
type: 'integer',
example: 256
},
percentage: {
type: 'number',
format: 'float',
example: 15.2
}
}
}
},
metadata: {
type: 'object',
properties: {
total: {
type: 'integer',
example: 1680
},
peak_hour: {
type: 'integer',
example: 20
},
lowest_hour: {
type: 'integer',
example: 4
}
}
}
}
}
}
}
},
'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 interaction time analysis data'
}
}
}
}
}
}
}
}
},
'/api/analytics/content-performance': {
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'
}
},
{
name: 'kolId',
in: 'query',
description: 'KOL ID',
schema: {
type: 'string'
}
},
{
name: 'contentType',
in: 'query',
description: '内容类型',
schema: {
type: 'string',
enum: ['post', 'video', 'article', 'all'],
default: 'all'
}
},
{
name: 'limit',
in: 'query',
description: '最大返回条数',
schema: {
type: 'integer',
default: 100
}
}
],
responses: {
'200': {
description: '内容表现分析数据',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: {
type: 'boolean'
},
data: {
type: 'array',
items: {
type: 'object',
properties: {
content_id: {
type: 'string',
description: '内容ID'
},
title: {
type: 'string',
description: '内容标题'
},
platform: {
type: 'string',
description: '平台'
},
content_type: {
type: 'string',
description: '内容类型'
},
influencer_name: {
type: 'string',
description: 'KOL名称'
},
publish_date: {
type: 'string',
description: '发布日期'
},
coverage: {
type: 'integer',
description: '内容覆盖量(阅读量/浏览量)'
},
interaction_rate: {
type: 'number',
format: 'float',
description: '互动率(互动总数/覆盖量)'
},
interaction_count: {
type: 'integer',
description: '互动总量(点赞+评论+分享)'
},
likes: {
type: 'integer',
description: '点赞数'
},
comments: {
type: 'integer',
description: '评论数'
},
shares: {
type: 'integer',
description: '分享数'
},
quadrant: {
type: 'string',
enum: ['high_value', 'high_coverage', 'high_engagement', 'low_performance'],
description: '四象限分类'
}
}
}
},
metadata: {
type: 'object',
properties: {
total: {
type: 'integer',
description: '内容总数'
},
average_coverage: {
type: 'number',
format: 'float',
description: '平均覆盖量'
},
average_interaction_rate: {
type: 'number',
format: 'float',
description: '平均互动率'
},
quadrant_counts: {
type: 'object',
properties: {
high_value: {
type: 'integer',
description: '高价值内容数'
},
high_coverage: {
type: 'integer',
description: '高覆盖内容数'
},
high_engagement: {
type: 'integer',
description: '高互动内容数'
},
low_performance: {
type: 'integer',
description: '低表现内容数'
}
}
}
}
}
}
}
}
}
},
'400': {
description: '无效请求',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
},
'500': {
description: '服务器错误',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
}
}
}
},
},
components: {
schemas: {

File diff suppressed because it is too large Load Diff