Compare commits
2 Commits
bd1a5ce384
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4056bac3ab | |||
| d0f2ab0620 |
@@ -825,6 +825,166 @@ export class AnalyticsController {
|
|||||||
}, 500);
|
}, 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
|
// Export singleton instance
|
||||||
|
|||||||
@@ -53,4 +53,10 @@ analyticsRouter.get('/moderation-status', (c) => analyticsController.getModerati
|
|||||||
// Add hot keywords route
|
// Add hot keywords route
|
||||||
analyticsRouter.get('/hot-keywords', (c) => analyticsController.getHotKeywords(c));
|
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;
|
export default analyticsRouter;
|
||||||
@@ -225,6 +225,62 @@ export interface HotKeywordsResponse {
|
|||||||
total: number; // 总数
|
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
|
* Analytics service for KOL performance data
|
||||||
*/
|
*/
|
||||||
@@ -1911,6 +1967,493 @@ export class AnalyticsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户互动时间分析数据
|
||||||
|
* @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
|
// Export singleton instance
|
||||||
|
|||||||
@@ -3320,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: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
|||||||
Reference in New Issue
Block a user