import { Context } from 'hono'; import { analyticsService } from '../services/analyticsService'; import { logger } from '../utils/logger'; /** * Controller for analytics endpoints */ export class AnalyticsController { /** * Get KOL performance overview * Returns card-style layout showing key performance metrics for each KOL * * @param c Hono Context * @returns Response with KOL performance data */ async getKolOverview(c: Context) { const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; const startTime = Date.now(); try { // Get query parameters for time range filtering const timeRange = c.req.query('timeRange') || '30'; // Default to 30 days const projectId = c.req.query('projectId'); // Optional project filter const sortBy = c.req.query('sortBy') || 'followers_change'; // Default sort by followers change const sortOrder = c.req.query('sortOrder') || 'desc'; // Default to descending order const limit = parseInt(c.req.query('limit') || '20', 10); // Default limit to 20 KOLs const offset = parseInt(c.req.query('offset') || '0', 10); // Default offset to 0 const debug = c.req.query('debug') || 'false'; // Default debug to false logger.info(`[${requestId}] KOL overview request received`, { timeRange, projectId, sortBy, sortOrder, limit, offset, debug, userAgent: c.req.header('user-agent'), ip: c.req.header('x-forwarded-for') || 'unknown' }); // Validate time range 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); } // Validate sort order if (!['asc', 'desc'].includes(sortOrder)) { logger.warn(`[${requestId}] Invalid sortOrder: ${sortOrder}`); return c.json({ success: false, error: 'Invalid sortOrder. Must be asc or desc.' }, 400); } // Validate sort field const validSortFields = ['followers_change', 'likes_change', 'follows_change', 'followers_count']; if (!validSortFields.includes(sortBy)) { logger.warn(`[${requestId}] Invalid sortBy: ${sortBy}`); return c.json({ success: false, error: `Invalid sortBy. Must be one of: ${validSortFields.join(', ')}` }, 400); } // Get KOL overview data from the service const data = await analyticsService.getKolPerformanceOverview( parseInt(timeRange, 10), projectId, sortBy, sortOrder, limit, offset ); // Debug mode - log additional event data if (debug.toLowerCase() === 'true' && process.env.NODE_ENV !== 'production') { await analyticsService.debugEventData(); } // Log successful response const duration = Date.now() - startTime; logger.info(`[${requestId}] KOL overview response sent successfully`, { duration, resultCount: data.kols.length, totalRecords: data.total }); // Return the data return c.json({ success: true, data: data.kols, pagination: { limit, offset, total: data.total } }); } catch (error) { // Log error const duration = Date.now() - startTime; logger.error(`[${requestId}] Error fetching KOL overview (${duration}ms)`, error); // Return error response return c.json({ success: false, error: 'Failed to fetch KOL overview data', message: error instanceof Error ? error.message : 'Unknown error' }, 500); } } /** * Get KOL conversion funnel data * Returns user counts and conversion rates for each funnel stage * * @param c Hono Context * @returns Response with funnel data */ async getKolFunnel(c: Context) { const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; const startTime = Date.now(); try { // Get query parameters const timeRange = c.req.query('timeRange') || '30'; // Default to 30 days const projectId = c.req.query('projectId'); // Optional project filter const debug = c.req.query('debug') || 'false'; // Debug mode logger.info(`[${requestId}] KOL funnel request received`, { timeRange, projectId, debug, userAgent: c.req.header('user-agent'), ip: c.req.header('x-forwarded-for') || 'unknown' }); // Validate time range 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); } // Get funnel data from service const data = await analyticsService.getKolFunnel( parseInt(timeRange, 10), projectId ); // Debug mode - log additional data if (debug.toLowerCase() === 'true' && process.env.NODE_ENV !== 'production') { await analyticsService.debugEventData(); } // Log successful response const duration = Date.now() - startTime; logger.info(`[${requestId}] KOL funnel response sent successfully`, { duration, stageCount: data.stages.length }); // Return the data return c.json({ success: true, data }); } catch (error) { // Log error const duration = Date.now() - startTime; logger.error(`[${requestId}] Error fetching KOL funnel (${duration}ms)`, error); // Return error response return c.json({ success: false, error: 'Failed to fetch KOL funnel data', message: error instanceof Error ? error.message : 'Unknown error' }, 500); } } /** * Get KOL post performance data * Returns table data of posts with key metrics and sentiment scores * * @param c Hono Context * @returns Response with post performance data */ async getPostPerformance(c: Context): Promise { const startTime = Date.now(); // 解析查询参数 const kolId = c.req.query('kolId'); const platform = c.req.query('platform'); const startDate = c.req.query('startDate'); const endDate = c.req.query('endDate'); const sortBy = c.req.query('sortBy'); const sortOrder = c.req.query('sortOrder'); const limit = c.req.query('limit'); const offset = c.req.query('offset'); // 记录请求参数 logger.info('Post performance data requested', { kolId, platform, startDate, endDate, sortBy, sortOrder, limit, offset }); try { // 验证时间范围 if (startDate && !this.isValidDateFormat(startDate)) { return c.json({ success: false, error: 'Invalid startDate format. Expected YYYY-MM-DD' }, 400); } if (endDate && !this.isValidDateFormat(endDate)) { return c.json({ success: false, error: 'Invalid endDate format. Expected YYYY-MM-DD' }, 400); } // 验证排序字段 const validSortFields = ['publish_date', 'views', 'likes', 'comments', 'shares', 'sentiment_score']; if (sortBy && !validSortFields.includes(sortBy)) { return c.json({ success: false, error: `Invalid sortBy field. Expected one of: ${validSortFields.join(', ')}` }, 400); } // 验证排序顺序 if (sortOrder && !['asc', 'desc', 'ASC', 'DESC'].includes(sortOrder)) { return c.json({ success: false, error: 'Invalid sortOrder. Expected: asc or desc' }, 400); } // 验证限制数量和偏移量 const parsedLimit = limit ? parseInt(limit, 10) : 10; if (isNaN(parsedLimit) || parsedLimit < 1 || parsedLimit > 100) { return c.json({ success: false, error: 'Invalid limit. Expected a number between 1 and 100' }, 400); } const parsedOffset = offset ? parseInt(offset, 10) : 0; if (isNaN(parsedOffset) || parsedOffset < 0) { return c.json({ success: false, error: 'Invalid offset. Expected a non-negative number' }, 400); } // 获取帖文表现数据 const { posts, total } = await analyticsService.getPostPerformance( kolId || undefined, platform || undefined, startDate || undefined, endDate || undefined, sortBy || 'publish_date', sortOrder || 'desc', parsedLimit, parsedOffset ); // 返回结果 const duration = Date.now() - startTime; logger.info(`Post performance data returned (${duration}ms)`, { totalPosts: total, returnedPosts: posts.length }); return c.json({ success: true, data: posts, meta: { total, returned: posts.length, limit: parsedLimit, offset: parsedOffset } }); } catch (error) { const duration = Date.now() - startTime; logger.error(`Error getting post performance data (${duration}ms)`, error); return c.json({ success: false, error: 'Failed to retrieve post performance data' }, 500); } } /** * Validate date string format (YYYY-MM-DD) * @param dateString Date string to validate * @returns True if valid date format */ private isValidDateFormat(dateString: string): boolean { const regex = /^\d{4}-\d{2}-\d{2}$/; if (!regex.test(dateString)) return false; const date = new Date(dateString); return date instanceof Date && !isNaN(date.getTime()); } /** * 获取概览卡片数据 * 返回包含留言总数、平均互动率和情感分析三个卡片数据 * * @param c Hono Context * @returns Response with dashboard card data */ async getDashboardCards(c: Context) { const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; const startTime = Date.now(); try { // 获取查询参数 const timeRange = c.req.query('timeRange') || '30'; // 默认30天 const projectId = c.req.query('projectId'); // 可选项目过滤 logger.info(`[${requestId}] Dashboard cards request received`, { timeRange, projectId, userAgent: c.req.header('user-agent'), ip: c.req.header('x-forwarded-for') || 'unknown' }); // 验证时间范围 if (!['7', '30', '90'].includes(timeRange)) { logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`); return c.json({ success: false, error: 'Invalid timeRange. Must be 7, 30, or 90.' }, 400); } // 获取概览卡片数据 const data = await analyticsService.getDashboardCardData( parseInt(timeRange, 10), projectId ); // 返回成功响应 const duration = Date.now() - startTime; logger.info(`[${requestId}] Dashboard cards response sent successfully`, { duration }); return c.json({ success: true, data }); } catch (error) { // 记录错误 const duration = Date.now() - startTime; logger.error(`[${requestId}] Error fetching dashboard cards (${duration}ms)`, error); // 返回错误响应 return c.json({ success: false, error: 'Failed to fetch dashboard card data', message: error instanceof Error ? error.message : 'Unknown error' }, 500); } } /** * 获取留言趋势数据 * 返回一段时间内留言数量的变化趋势 * * @param c Hono Context * @returns Response with comment trend data */ async getCommentTrend(c: Context) { const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; const startTime = Date.now(); try { // 获取查询参数 const timeRange = c.req.query('timeRange') || '30'; // 默认30天 const projectId = c.req.query('projectId'); // 可选项目过滤 const platform = c.req.query('platform'); // 可选平台过滤 logger.info(`[${requestId}] Comment trend request received`, { timeRange, projectId, platform, userAgent: c.req.header('user-agent'), ip: c.req.header('x-forwarded-for') || 'unknown' }); // 验证时间范围 if (!['7', '30', '90'].includes(timeRange)) { logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`); return c.json({ success: false, error: 'Invalid timeRange. Must be 7, 30, or 90.' }, 400); } // 获取留言趋势数据 const data = await analyticsService.getCommentTrend( parseInt(timeRange, 10), projectId, platform ); // 返回成功响应 const duration = Date.now() - startTime; logger.info(`[${requestId}] Comment trend response sent successfully`, { duration, dataPoints: data.data.length, totalComments: data.total_count }); return c.json({ success: true, data: data.data, metadata: { max_count: data.max_count, total_count: data.total_count } }); } catch (error) { // 记录错误 const duration = Date.now() - startTime; logger.error(`[${requestId}] Error fetching comment trend (${duration}ms)`, error); // 返回错误响应 return c.json({ success: false, error: 'Failed to fetch comment trend data', message: error instanceof Error ? error.message : 'Unknown error' }, 500); } } /** * 获取平台分布数据 * 返回不同社交平台上的事件分布情况 * * @param c Hono Context * @returns Response with platform distribution data */ async getPlatformDistribution(c: Context) { const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; const startTime = Date.now(); try { // 获取查询参数 const timeRange = c.req.query('timeRange') || '30'; // 默认30天 const projectId = c.req.query('projectId'); // 可选项目过滤 const eventType = c.req.query('eventType') || 'comment'; // 默认分析评论事件 logger.info(`[${requestId}] Platform distribution request received`, { timeRange, projectId, eventType, userAgent: c.req.header('user-agent'), ip: c.req.header('x-forwarded-for') || 'unknown' }); // 验证时间范围 if (!['7', '30', '90'].includes(timeRange)) { logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`); return c.json({ success: false, error: 'Invalid timeRange. Must be 7, 30, or 90.' }, 400); } // 验证事件类型 const validEventTypes = ['comment', 'like', 'view', 'share']; if (!validEventTypes.includes(eventType)) { logger.warn(`[${requestId}] Invalid eventType: ${eventType}`); return c.json({ success: false, error: `Invalid eventType. Must be one of: ${validEventTypes.join(', ')}` }, 400); } // 获取平台分布数据 const data = await analyticsService.getPlatformDistribution( parseInt(timeRange, 10), projectId, eventType ); // 返回成功响应 const duration = Date.now() - startTime; logger.info(`[${requestId}] Platform distribution response sent successfully`, { duration, platformCount: data.data.length, total: data.total }); return c.json({ success: true, data: data.data, metadata: { total: data.total, event_type: eventType } }); } catch (error) { // 记录错误 const duration = Date.now() - startTime; logger.error(`[${requestId}] Error fetching platform distribution (${duration}ms)`, error); // 返回错误响应 return c.json({ success: false, error: 'Failed to fetch platform distribution data', message: error instanceof Error ? error.message : 'Unknown error' }, 500); } } /** * 获取情感分析详情数据 * 返回正面、中性、负面评论的比例和分析 * * @param c Hono Context * @returns Response with sentiment analysis data */ async getSentimentAnalysis(c: Context) { const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; const startTime = Date.now(); try { // 获取查询参数 const timeRange = c.req.query('timeRange') || '30'; // 默认30天 const projectId = c.req.query('projectId'); // 可选项目过滤 const platform = c.req.query('platform'); // 可选平台过滤 logger.info(`[${requestId}] Sentiment analysis request received`, { timeRange, projectId, platform, userAgent: c.req.header('user-agent'), ip: c.req.header('x-forwarded-for') || 'unknown' }); // 验证时间范围 if (!['7', '30', '90'].includes(timeRange)) { logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`); return c.json({ success: false, error: 'Invalid timeRange. Must be 7, 30, or 90.' }, 400); } // 获取情感分析数据 const data = await analyticsService.getSentimentAnalysis( parseInt(timeRange, 10), projectId, platform ); // 返回成功响应 const duration = Date.now() - startTime; logger.info(`[${requestId}] Sentiment analysis response sent successfully`, { duration, total: data.total }); return c.json({ success: true, data }); } catch (error) { // 记录错误 const duration = Date.now() - startTime; logger.error(`[${requestId}] Error fetching sentiment analysis (${duration}ms)`, error); // 返回错误响应 return c.json({ success: false, error: 'Failed to fetch sentiment analysis data', message: error instanceof Error ? error.message : 'Unknown error' }, 500); } } /** * 获取热门文章数据 * 返回按互动数量或互动率排序的热门帖文 * * @param c Hono Context * @returns Response with popular posts data */ async getPopularPosts(c: Context) { const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; const startTime = Date.now(); try { // 获取查询参数 const timeRange = c.req.query('timeRange') || '30'; // 默认30天 const projectId = c.req.query('projectId'); // 可选项目过滤 const platform = c.req.query('platform'); // 可选平台过滤 const sortBy = c.req.query('sortBy') || 'engagement_count'; // 默认按互动数量排序 const limit = parseInt(c.req.query('limit') || '10', 10); // 默认返回10个 logger.info(`[${requestId}] Popular posts request received`, { timeRange, projectId, platform, sortBy, limit, userAgent: c.req.header('user-agent'), ip: c.req.header('x-forwarded-for') || 'unknown' }); // 验证时间范围 if (!['7', '30', '90'].includes(timeRange)) { logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`); return c.json({ success: false, error: 'Invalid timeRange. Must be 7, 30, or 90.' }, 400); } // 验证排序字段 if (!['engagement_count', 'engagement_rate'].includes(sortBy)) { logger.warn(`[${requestId}] Invalid sortBy: ${sortBy}`); return c.json({ success: false, error: 'Invalid sortBy. Must be engagement_count or engagement_rate.' }, 400); } // 获取热门文章数据 const data = await analyticsService.getPopularPosts( parseInt(timeRange, 10), projectId, platform, sortBy, Math.min(limit, 50) // 最多返回50条 ); // 返回成功响应 const duration = Date.now() - startTime; logger.info(`[${requestId}] Popular posts response sent successfully`, { duration, resultCount: data.posts.length, total: data.total }); return c.json({ success: true, data: data.posts, metadata: { total: data.total, high_engagement_count: data.posts.filter(post => post.is_high_engagement).length } }); } catch (error) { // 记录错误 const duration = Date.now() - startTime; logger.error(`[${requestId}] Error fetching popular posts (${duration}ms)`, error); // 返回错误响应 return c.json({ success: false, error: 'Failed to fetch popular posts data', message: error instanceof Error ? error.message : 'Unknown error' }, 500); } } } // Export singleton instance export const analyticsController = new AnalyticsController();