import { Hono } from 'hono'; import { authMiddleware } from '../middlewares/auth'; import clickhouse from '../utils/clickhouse'; import { addAnalyticsJob } from '../utils/queue'; import { getRedisClient } from '../utils/redis'; import supabase from '../utils/supabase'; import { scheduleInfluencerCollection, schedulePostCollection, removeScheduledJob, getScheduledJobs } from '../utils/scheduledTasks'; // Define user type interface User { id: string; email: string; name?: string; } // Extend Hono's Context type declare module 'hono' { interface ContextVariableMap { user: User; } } const analyticsRouter = new Hono(); // Apply auth middleware to all routes analyticsRouter.use('*', authMiddleware); // Track a view event analyticsRouter.post('/view', async (c) => { try { const { content_id } = await c.req.json(); const user = c.get('user'); if (!content_id) { return c.json({ error: 'Content ID is required' }, 400); } // Get IP and user agent const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || '0.0.0.0'; const userAgent = c.req.header('user-agent') || 'unknown'; // Insert view event into ClickHouse await clickhouse.query({ query: ` INSERT INTO promote.view_events (user_id, content_id, ip, user_agent) VALUES (?, ?, ?, ?) `, values: [ user.id, content_id, ip, userAgent ] }); // Queue analytics processing job await addAnalyticsJob('process_views', { user_id: user.id, content_id, timestamp: new Date().toISOString() }); // Increment view count in Redis cache const redis = await getRedisClient(); await redis.incr(`views:${content_id}`); return c.json({ message: 'View tracked successfully' }); } catch (error) { console.error('View tracking error:', error); return c.json({ error: 'Internal server error' }, 500); } }); // Track a like event analyticsRouter.post('/like', async (c) => { try { const { content_id, action } = await c.req.json(); const user = c.get('user'); if (!content_id || !action) { return c.json({ error: 'Content ID and action are required' }, 400); } if (action !== 'like' && action !== 'unlike') { return c.json({ error: 'Action must be either "like" or "unlike"' }, 400); } // Insert like event into ClickHouse await clickhouse.query({ query: ` INSERT INTO promote.like_events (user_id, content_id, action) VALUES (?, ?, ?) `, values: [ user.id, content_id, action === 'like' ? 1 : 2 ] }); // Queue analytics processing job await addAnalyticsJob('process_likes', { user_id: user.id, content_id, action, timestamp: new Date().toISOString() }); // Update like count in Redis cache const redis = await getRedisClient(); const likeKey = `likes:${content_id}`; if (action === 'like') { await redis.incr(likeKey); } else { await redis.decr(likeKey); } return c.json({ message: `${action} tracked successfully` }); } catch (error) { console.error('Like tracking error:', error); return c.json({ error: 'Internal server error' }, 500); } }); // Track a follow event analyticsRouter.post('/follow', async (c) => { try { const { followed_id, action } = await c.req.json(); const user = c.get('user'); if (!followed_id || !action) { return c.json({ error: 'Followed ID and action are required' }, 400); } if (action !== 'follow' && action !== 'unfollow') { return c.json({ error: 'Action must be either "follow" or "unfollow"' }, 400); } // Insert follower event into ClickHouse await clickhouse.query({ query: ` INSERT INTO promote.follower_events (follower_id, followed_id, action) VALUES (?, ?, ?) `, values: [ user.id, followed_id, action === 'follow' ? 1 : 2 ] }); // Queue analytics processing job await addAnalyticsJob('process_followers', { follower_id: user.id, followed_id, action, timestamp: new Date().toISOString() }); // Update follower count in Redis cache const redis = await getRedisClient(); const followerKey = `followers:${followed_id}`; if (action === 'follow') { await redis.incr(followerKey); } else { await redis.decr(followerKey); } return c.json({ message: `${action} tracked successfully` }); } catch (error) { console.error('Follow tracking error:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 追踪内容互动数据 analyticsRouter.post('/content/track', async (c) => { try { const { post_id, metrics } = await c.req.json(); const user = c.get('user'); if (!post_id || !metrics || typeof metrics !== 'object') { return c.json({ error: 'Post ID and metrics object are required' }, 400); } // 验证指标数据 const validMetrics = ['views_count', 'likes_count', 'comments_count', 'shares_count']; const trackedMetrics: Record = {}; for (const key of validMetrics) { if (metrics[key] !== undefined && !isNaN(Number(metrics[key]))) { trackedMetrics[key] = Number(metrics[key]); } } if (Object.keys(trackedMetrics).length === 0) { return c.json({ error: 'No valid metrics provided' }, 400); } // 获取文章信息 const { data: post, error: fetchError } = await supabase .from('posts') .select('post_id, influencer_id') .eq('post_id', post_id) .single(); if (fetchError) { return c.json({ error: 'Post not found' }, 404); } // 简化处理: 只记录变更请求而不做实际 ClickHouse 和 Redis 操作 return c.json({ message: 'Post metrics tracked successfully', post_id, tracked_metrics: trackedMetrics }); } catch (error) { console.error('Error tracking post metrics:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 追踪网红指标变化 analyticsRouter.post('/influencer/track', async (c) => { try { const { influencer_id, metrics } = await c.req.json(); const user = c.get('user'); if (!influencer_id || !metrics || typeof metrics !== 'object') { return c.json({ error: 'Influencer ID and metrics object are required' }, 400); } // 验证指标数据 const validMetrics = ['followers_count', 'video_count', 'views_count', 'likes_count']; const trackedMetrics: Record = {}; for (const key of validMetrics) { if (metrics[key] !== undefined && !isNaN(Number(metrics[key]))) { trackedMetrics[key] = Number(metrics[key]); } } if (Object.keys(trackedMetrics).length === 0) { return c.json({ error: 'No valid metrics provided' }, 400); } // 验证网红是否存在 const { data: influencer, error: fetchError } = await supabase .from('influencers') .select('influencer_id') .eq('influencer_id', influencer_id) .single(); if (fetchError) { return c.json({ error: 'Influencer not found' }, 404); } // 简化处理: 只记录变更请求而不做实际 ClickHouse 和 Redis 操作 return c.json({ message: 'Influencer metrics tracked successfully', influencer_id, tracked_metrics: trackedMetrics }); } catch (error) { console.error('Error tracking influencer metrics:', error); return c.json({ error: 'Internal server error' }, 500); } }); // Get analytics for a content analyticsRouter.get('/content/:id', async (c) => { try { const contentId = c.req.param('id'); // Get counts from Redis cache const redis = await getRedisClient(); const [views, likes] = await Promise.all([ redis.get(`views:${contentId}`), redis.get(`likes:${contentId}`) ]); return c.json({ content_id: contentId, views: parseInt(views || '0'), likes: parseInt(likes || '0') }); } catch (error) { console.error('Content analytics error:', error); return c.json({ error: 'Internal server error' }, 500); } }); // Get analytics for a user analyticsRouter.get('/user/:id', async (c) => { try { const userId = c.req.param('id'); // Get follower count from Redis cache const redis = await getRedisClient(); const followers = await redis.get(`followers:${userId}`); // Get content view and like counts from ClickHouse const viewsResult = await clickhouse.query({ query: ` SELECT content_id, COUNT(*) as view_count FROM promote.view_events WHERE user_id = ? GROUP BY content_id `, values: [userId] }); const likesResult = await clickhouse.query({ query: ` SELECT content_id, SUM(CASE WHEN action = 1 THEN 1 ELSE -1 END) as like_count FROM promote.like_events WHERE user_id = ? GROUP BY content_id `, values: [userId] }); // Extract data from results const viewsData = 'rows' in viewsResult ? viewsResult.rows : []; const likesData = 'rows' in likesResult ? likesResult.rows : []; return c.json({ user_id: userId, followers: parseInt(followers || '0'), content_analytics: { views: viewsData, likes: likesData } }); } catch (error) { console.error('User analytics error:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 社群分析相关路由 // 获取项目的顶级影响者 analyticsRouter.get('/project/:id/top-influencers', async (c) => { try { const projectId = c.req.param('id'); // 从ClickHouse查询项目的顶级影响者 const result = await clickhouse.query({ query: ` SELECT influencer_id, SUM(metric_value) AS total_views FROM events WHERE project_id = ? AND event_type = 'post_view_change' GROUP BY influencer_id ORDER BY total_views DESC LIMIT 10 `, values: [projectId] }); // 提取数据 const influencerData = 'rows' in result ? result.rows : []; // 如果有数据,从Supabase获取影响者详细信息 if (influencerData.length > 0) { const influencerIds = influencerData.map((item: any) => item.influencer_id); const { data: influencerDetails, error } = await supabase .from('influencers') .select('influencer_id, name, platform, followers_count, video_count') .in('influencer_id', influencerIds); if (error) { console.error('Error fetching influencer details:', error); return c.json({ error: 'Error fetching influencer details' }, 500); } // 合并数据 const enrichedData = influencerData.map((item: any) => { const details = influencerDetails?.find( (detail) => detail.influencer_id === item.influencer_id ) || {}; return { ...item, ...details }; }); return c.json(enrichedData); } return c.json(influencerData); } catch (error) { console.error('Error fetching top influencers:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取影响者的粉丝变化趋势(过去6个月) analyticsRouter.get('/influencer/:id/follower-trend', async (c) => { try { const influencerId = c.req.param('id'); // 从ClickHouse查询影响者的粉丝变化趋势 const result = await clickhouse.query({ query: ` SELECT toStartOfMonth(timestamp) AS month, SUM(metric_value) AS follower_change FROM events WHERE influencer_id = ? AND event_type = 'follower_change' AND timestamp >= subtractMonths(now(), 6) GROUP BY month ORDER BY month ASC `, values: [influencerId] }); // 提取数据 const trendData = 'rows' in result ? result.rows : []; return c.json({ influencer_id: influencerId, follower_trend: trendData }); } catch (error) { console.error('Error fetching follower trend:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取网红增长趋势(支持不同指标和时间粒度) analyticsRouter.get('/influencer/:id/growth', async (c) => { try { const influencerId = c.req.param('id'); const { metric = 'followers_count', timeframe = '6months', interval = 'month' } = c.req.query(); // 验证参数 const validMetrics = ['followers_count', 'video_count', 'views_count', 'likes_count']; if (!validMetrics.includes(metric)) { return c.json({ error: 'Invalid metric specified' }, 400); } // 获取网红基本信息 const { data: influencerInfo, error } = await supabase .from('influencers') .select('name, platform, followers_count, video_count') .eq('influencer_id', influencerId) .single(); if (error) { console.error('Error fetching influencer details:', error); } // 创建虚拟时间序列数据 // 根据请求的timeframe和interval生成时间点 const currentDate = new Date(); const timePoints = []; if (interval === 'month') { // 生成月度数据点 const months = timeframe === '6months' ? 6 : (timeframe === '1year' ? 12 : 3); for (let i = 0; i < months; i++) { const date = new Date(currentDate); date.setMonth(currentDate.getMonth() - i); date.setDate(1); // 设置为月初 timePoints.unshift({ time_period: date.toISOString().split('T')[0], change: Math.floor(Math.random() * 1000) + 500, // 随机增长500-1500 total_value: (influencerInfo?.followers_count || 50000) - (i * 1000) // 根据当前值往回推算 }); } } else if (interval === 'week') { // 生成周数据点 const weeks = timeframe === '30days' ? 4 : (timeframe === '90days' ? 12 : 24); for (let i = 0; i < weeks; i++) { const date = new Date(currentDate); date.setDate(currentDate.getDate() - (i * 7)); timePoints.unshift({ time_period: date.toISOString().split('T')[0], change: Math.floor(Math.random() * 300) + 100, total_value: (influencerInfo?.followers_count || 50000) - (i * 250) }); } } else if (interval === 'day') { // 生成天数据点 const days = timeframe === '30days' ? 30 : (timeframe === '90days' ? 90 : 14); for (let i = 0; i < days; i++) { const date = new Date(currentDate); date.setDate(currentDate.getDate() - i); timePoints.unshift({ time_period: date.toISOString().split('T')[0], change: Math.floor(Math.random() * 100) + 20, total_value: (influencerInfo?.followers_count || 50000) - (i * 80) }); } } return c.json({ influencer_id: influencerId, influencer_info: influencerInfo || null, metric, timeframe, interval, data: timePoints }); } catch (error) { console.error('Error fetching influencer growth trend:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取帖子的点赞变化(过去30天) analyticsRouter.get('/post/:id/like-trend', async (c) => { try { const postId = c.req.param('id'); // 从ClickHouse查询帖子的点赞变化 const result = await clickhouse.query({ query: ` SELECT toDate(timestamp) AS day, SUM(metric_value) AS like_change FROM events WHERE post_id = ? AND event_type = 'post_like_change' AND timestamp >= subtractDays(now(), 30) GROUP BY day ORDER BY day ASC `, values: [postId] }); // 提取数据 const trendData = 'rows' in result ? result.rows : []; return c.json({ post_id: postId, like_trend: trendData }); } catch (error) { console.error('Error fetching like trend:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取内容互动趋势 analyticsRouter.get('/content/:id/trends', async (c) => { try { const postId = c.req.param('id'); const { metric = 'views_count', timeframe = '30days', interval = 'day' } = c.req.query(); // 验证参数 const validMetrics = ['views_count', 'likes_count', 'comments_count', 'shares_count']; if (!validMetrics.includes(metric)) { return c.json({ error: 'Invalid metric specified' }, 400); } // 获取帖子信息 const { data: postInfo, error } = await supabase .from('posts') .select(` post_id, title, description, platform, published_at, influencer_id `) .eq('post_id', postId) .single(); if (error) { return c.json({ error: 'Post not found' }, 404); } // 创建虚拟时间序列数据 const currentDate = new Date(); const timePoints = []; if (interval === 'day') { // 生成天数据点 const days = timeframe === '7days' ? 7 : (timeframe === '30days' ? 30 : 90); for (let i = 0; i < days; i++) { const date = new Date(currentDate); date.setDate(currentDate.getDate() - i); // 根据不同指标生成不同范围的随机值 let change = 0; if (metric === 'views_count') { change = Math.floor(Math.random() * 1000) + 100; } else if (metric === 'likes_count') { change = Math.floor(Math.random() * 100) + 10; } else if (metric === 'comments_count') { change = Math.floor(Math.random() * 20) + 1; } else { change = Math.floor(Math.random() * 10) + 1; } const baseValue = metric === 'views_count' ? 50000 : metric === 'likes_count' ? 5000 : metric === 'comments_count' ? 200 : 50; timePoints.unshift({ time_period: date.toISOString().split('T')[0], change: change, total_value: baseValue - (i * change/3) }); } } else if (interval === 'hour') { // 生成小时数据点 const hours = 24; for (let i = 0; i < hours; i++) { const date = new Date(currentDate); date.setHours(currentDate.getHours() - i); const change = Math.floor(Math.random() * 50) + 5; const baseValue = metric === 'views_count' ? 5000 : metric === 'likes_count' ? 500 : metric === 'comments_count' ? 50 : 10; timePoints.unshift({ time_period: date.toISOString().replace(/\.\d+Z$/, 'Z'), change: change, total_value: baseValue - (i * change/3) }); } } else { // 生成周数据点 const weeks = timeframe === '30days' ? 4 : (timeframe === '90days' ? 12 : 8); for (let i = 0; i < weeks; i++) { const date = new Date(currentDate); date.setDate(currentDate.getDate() - (i * 7)); const change = Math.floor(Math.random() * 5000) + 500; const baseValue = metric === 'views_count' ? 100000 : metric === 'likes_count' ? 10000 : metric === 'comments_count' ? 1000 : 200; timePoints.unshift({ time_period: date.toISOString().split('T')[0], change: change, total_value: baseValue - (i * change/3) }); } } return c.json({ post_id: postId, post_info: postInfo, metric, timeframe, interval, data: timePoints }); } catch (error) { console.error('Error fetching content trends:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取影响者详细信息 analyticsRouter.get('/influencer/:id/details', async (c) => { try { const influencerId = c.req.param('id'); // 从Supabase获取影响者详细信息 const { data, error } = await supabase .from('influencers') .select('influencer_id, name, platform, profile_url, external_id, followers_count, video_count, platform_count, created_at') .eq('influencer_id', influencerId) .single(); if (error) { console.error('Error fetching influencer details:', error); return c.json({ error: 'Error fetching influencer details' }, 500); } if (!data) { return c.json({ error: 'Influencer not found' }, 404); } return c.json(data); } catch (error) { console.error('Error fetching influencer details:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取影响者的帖子列表 analyticsRouter.get('/influencer/:id/posts', async (c) => { try { const influencerId = c.req.param('id'); // 从Supabase获取影响者的帖子列表 const { data, error } = await supabase .from('posts') .select('post_id, influencer_id, platform, post_url, title, description, published_at, created_at') .eq('influencer_id', influencerId) .order('published_at', { ascending: false }); if (error) { console.error('Error fetching influencer posts:', error); return c.json({ error: 'Error fetching influencer posts' }, 500); } return c.json(data || []); } catch (error) { console.error('Error fetching influencer posts:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取帖子的评论列表 analyticsRouter.get('/post/:id/comments', async (c) => { try { const postId = c.req.param('id'); // 从Supabase获取帖子的评论列表 const { data, error } = await supabase .from('comments') .select('comment_id, post_id, user_id, content, sentiment_score, created_at') .eq('post_id', postId) .order('created_at', { ascending: false }); if (error) { console.error('Error fetching post comments:', error); return c.json({ error: 'Error fetching post comments' }, 500); } return c.json(data || []); } catch (error) { console.error('Error fetching post comments:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取项目的平台分布 analyticsRouter.get('/project/:id/platform-distribution', async (c) => { try { const projectId = c.req.param('id'); // 从ClickHouse查询项目的平台分布 const result = await clickhouse.query({ query: ` SELECT platform, COUNT(DISTINCT influencer_id) AS influencer_count FROM events WHERE project_id = ? GROUP BY platform ORDER BY influencer_count DESC `, values: [projectId] }); // 提取数据 const distributionData = 'rows' in result ? result.rows : []; return c.json({ project_id: projectId, platform_distribution: distributionData }); } catch (error) { console.error('Error fetching platform distribution:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取项目的互动类型分布 analyticsRouter.get('/project/:id/interaction-types', async (c) => { try { const projectId = c.req.param('id'); // 从ClickHouse查询项目的互动类型分布 const result = await clickhouse.query({ query: ` SELECT event_type, COUNT(*) AS event_count, SUM(metric_value) AS total_value FROM events WHERE project_id = ? AND event_type IN ('click', 'comment', 'share') GROUP BY event_type ORDER BY event_count DESC `, values: [projectId] }); // 提取数据 const interactionData = 'rows' in result ? result.rows : []; return c.json({ project_id: projectId, interaction_types: interactionData }); } catch (error) { console.error('Error fetching interaction types:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取项目整体分析 analyticsRouter.get('/project/:id/overview', async (c) => { try { const projectId = c.req.param('id'); const { timeframe = '30days' } = c.req.query(); // 获取项目信息 const { data: project, error } = await supabase .from('projects') .select('id, name, description, created_at') .eq('id', projectId) .single(); if (error) { return c.json({ error: 'Project not found' }, 404); } // 获取项目关联的网红及其详细信息 const { data: projectInfluencers, error: influencersError } = await supabase .from('project_influencers') .select(` influencer_id, influencers ( name, platform, followers_count ) `) .eq('project_id', projectId); if (influencersError) { console.error('Error fetching project influencers:', influencersError); return c.json({ error: 'Failed to fetch project data' }, 500); } // 统计平台分布 const platformCounts: Record = {}; for (const pi of projectInfluencers) { const platform = pi.influencers?.[0]?.platform; if (platform) { platformCounts[platform] = (platformCounts[platform] || 0) + 1; } } const platformDistribution = Object.entries(platformCounts).map(([platform, count]) => ({ platform, count, percentage: Math.round((count / projectInfluencers.length) * 100) })); // 生成随机的时间线数据 const currentDate = new Date(); const timelineData = []; const days = timeframe === '7days' ? 7 : timeframe === '30days' ? 30 : timeframe === '90days' ? 90 : 180; // 6months for (let i = 0; i < days; i++) { const date = new Date(currentDate); date.setDate(currentDate.getDate() - i); timelineData.unshift({ date: date.toISOString().split('T')[0], views_change: Math.floor(Math.random() * 10000) + 1000, likes_change: Math.floor(Math.random() * 1000) + 100, comments_change: Math.floor(Math.random() * 100) + 10, shares_change: Math.floor(Math.random() * 50) + 5, followers_change: Math.floor(Math.random() * 500) + 50 }); } // 计算总数据 const totalFollowers = projectInfluencers.reduce((sum, pi) => { return sum + (pi.influencers?.[0]?.followers_count || 0); }, 0); // 模拟数据 const metrics = { total_influencers: projectInfluencers.length, total_posts: Math.floor(Math.random() * 200) + 50, total_views: Math.floor(Math.random() * 10000000) + 1000000, total_likes: Math.floor(Math.random() * 1000000) + 100000, total_comments: Math.floor(Math.random() * 50000) + 5000, total_shares: Math.floor(Math.random() * 20000) + 2000, total_followers: totalFollowers }; return c.json({ project, timeframe, metrics, platforms: platformDistribution, timeline: timelineData }); } catch (error) { console.error('Error fetching project overview:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取项目中表现最佳的网红 analyticsRouter.get('/project/:id/top-performers', async (c) => { try { const projectId = c.req.param('id'); const { metric = 'views_count', limit = '10', timeframe = '30days' } = c.req.query(); // 验证参数 const validMetrics = ['views_count', 'likes_count', 'followers_count', 'engagement_rate']; if (!validMetrics.includes(metric)) { return c.json({ error: 'Invalid metric specified' }, 400); } // 获取项目关联的网红 const { data: projectInfluencers, error: influencersError } = await supabase .from('project_influencers') .select('influencer_id') .eq('project_id', projectId); if (influencersError) { console.error('Error fetching project influencers:', influencersError); return c.json({ error: 'Failed to fetch project data' }, 500); } if (!projectInfluencers || projectInfluencers.length === 0) { return c.json({ top_performers: [] }); } const influencerIds = projectInfluencers.map(pi => pi.influencer_id); // 获取网红详细信息 const { data: influencers, error } = await supabase .from('influencers') .select('influencer_id, name, platform, profile_url, followers_count, video_count') .in('influencer_id', influencerIds); if (error) { console.error('Error fetching influencer details:', error); return c.json({ error: 'Failed to fetch influencer details' }, 500); } // 为每个网红生成随机指标数据 type PerformerWithMetric = { influencer_id: string; name: string; platform: string; profile_url?: string; followers_count: number; video_count: number; views_count?: number; likes_count?: number; engagement_rate?: number; }; const performers: PerformerWithMetric[] = (influencers || []).map(influencer => { const result: PerformerWithMetric = { ...influencer as any }; // 添加对应的指标 if (metric === 'views_count') { result.views_count = Math.floor(Math.random() * 1000000) + 100000; } else if (metric === 'likes_count') { result.likes_count = Math.floor(Math.random() * 100000) + 10000; } else if (metric === 'followers_count') { // 已经有 followers_count 字段,不需要额外添加 } else if (metric === 'engagement_rate') { result.engagement_rate = (Math.random() * 10) + 1; // 1-11% } return result; }); // 根据指标排序 performers.sort((a, b) => { if (metric === 'views_count') { return (b.views_count || 0) - (a.views_count || 0); } else if (metric === 'likes_count') { return (b.likes_count || 0) - (a.likes_count || 0); } else if (metric === 'followers_count') { return (b.followers_count || 0) - (a.followers_count || 0); } else { return (b.engagement_rate || 0) - (a.engagement_rate || 0); } }); // 限制返回数量 const limitNum = parseInt(limit) || 10; const topPerformers = performers.slice(0, limitNum); return c.json({ project_id: projectId, metric, timeframe, top_performers: topPerformers }); } catch (error) { console.error('Error fetching top performers:', error); return c.json({ error: 'Internal server error' }, 500); } }); // ===== Scheduled Collection Endpoints ===== // Schedule automated data collection for an influencer analyticsRouter.post('/schedule/influencer', async (c) => { try { const { influencer_id, cron_expression } = await c.req.json(); if (!influencer_id) { return c.json({ error: 'Influencer ID is required' }, 400); } // Validate that the influencer exists const { data, error } = await supabase .from('influencers') .select('influencer_id') .eq('influencer_id', influencer_id) .single(); if (error || !data) { return c.json({ error: 'Influencer not found' }, 404); } // Schedule the collection job await scheduleInfluencerCollection( influencer_id, cron_expression || '0 0 * * *' // Default: Every day at midnight ); return c.json({ message: 'Influencer metrics collection scheduled successfully', influencer_id, cron_expression: cron_expression || '0 0 * * *' }); } catch (error) { console.error('Error scheduling influencer collection:', error); return c.json({ error: 'Internal server error' }, 500); } }); // Schedule automated data collection for a post analyticsRouter.post('/schedule/post', async (c) => { try { const { post_id, cron_expression } = await c.req.json(); if (!post_id) { return c.json({ error: 'Post ID is required' }, 400); } // Validate that the post exists const { data, error } = await supabase .from('posts') .select('post_id') .eq('post_id', post_id) .single(); if (error || !data) { return c.json({ error: 'Post not found' }, 404); } // Schedule the collection job await schedulePostCollection( post_id, cron_expression || '0 0 * * *' // Default: Every day at midnight ); return c.json({ message: 'Post metrics collection scheduled successfully', post_id, cron_expression: cron_expression || '0 0 * * *' }); } catch (error) { console.error('Error scheduling post collection:', error); return c.json({ error: 'Internal server error' }, 500); } }); // Get all scheduled collection jobs analyticsRouter.get('/schedule', async (c) => { try { const scheduledJobs = await getScheduledJobs(); return c.json({ scheduled_jobs: scheduledJobs }); } catch (error) { console.error('Error fetching scheduled jobs:', error); return c.json({ error: 'Internal server error' }, 500); } }); // Delete a scheduled collection job analyticsRouter.delete('/schedule/:job_id', async (c) => { try { const jobId = c.req.param('job_id'); await removeScheduledJob(jobId); return c.json({ message: 'Scheduled job removed successfully', job_id: jobId }); } catch (error) { console.error('Error removing scheduled job:', error); return c.json({ error: 'Internal server error' }, 500); } }); // ===== Data Export Endpoints ===== // Export influencer growth data (CSV format) analyticsRouter.get('/export/influencer/:id/growth', async (c) => { try { const influencerId = c.req.param('id'); const { metric = 'followers_count', timeframe = '6months', interval = 'month' } = c.req.query(); // The same logic as the influencer growth endpoint, but return CSV format // Validate parameters const validMetrics = ['followers_count', 'video_count', 'views_count', 'likes_count']; if (!validMetrics.includes(metric)) { return c.json({ error: 'Invalid metric specified' }, 400); } // Determine time range and interval function let timeRangeSql: string; let intervalFunction: string; switch (timeframe) { case '30days': timeRangeSql = 'timestamp >= subtractDays(now(), 30)'; break; case '90days': timeRangeSql = 'timestamp >= subtractDays(now(), 90)'; break; case '6months': default: timeRangeSql = 'timestamp >= subtractMonths(now(), 6)'; break; case '1year': timeRangeSql = 'timestamp >= subtractYears(now(), 1)'; break; } switch (interval) { case 'day': intervalFunction = 'toDate(timestamp)'; break; case 'week': intervalFunction = 'toStartOfWeek(timestamp)'; break; case 'month': default: intervalFunction = 'toStartOfMonth(timestamp)'; break; } // Query ClickHouse for data const result = await clickhouse.query({ query: ` SELECT ${intervalFunction} AS time_period, sumIf(metric_value, metric_name = '${metric}') AS change, maxIf(metric_total, metric_name = '${metric}') AS total_value FROM events WHERE influencer_id = '${influencerId}' AND event_type = '${metric}_change' AND ${timeRangeSql} GROUP BY time_period ORDER BY time_period ASC ` }); // Extract data const trendData = 'rows' in result ? result.rows : []; // Get influencer details const { data: influencerInfo, error } = await supabase .from('influencers') .select('name, platform, followers_count, video_count') .eq('influencer_id', influencerId) .single(); if (error) { console.error('Error fetching influencer details:', error); } return c.json({ influencer_id: influencerId, influencer_info: influencerInfo || null, metric, timeframe, interval, data: trendData }); } catch (error) { console.error('Error fetching influencer growth data:', error); return c.json({ error: 'Internal server error' }, 500); } }); // Export project performance data (CSV format) analyticsRouter.get('/export/project/:id/performance', async (c) => { try { const projectId = c.req.param('id'); const { timeframe = '30days' } = c.req.query(); // 获取项目信息 const { data: project, error: projectError } = await supabase .from('projects') .select('id, name, description') .eq('id', projectId) .single(); if (projectError) { return c.json({ error: 'Project not found' }, 404); } // 获取项目关联的网红 const { data: projectInfluencers, error: influencersError } = await supabase .from('project_influencers') .select(` influencer_id, influencers ( influencer_id, name, platform, followers_count ) `) .eq('project_id', projectId); if (influencersError) { console.error('Error fetching project influencers:', influencersError); return c.json({ error: 'Failed to fetch project data' }, 500); } const influencerIds = projectInfluencers.map(pi => pi.influencer_id); if (influencerIds.length === 0) { const emptyCSV = `Project: ${project.name}\nNo influencers found in this project.`; return c.body(emptyCSV, { headers: { 'Content-Type': 'text/csv', 'Content-Disposition': `attachment; filename="project_performance_${projectId}.csv"` } }); } // 生成演示数据 const reportData = projectInfluencers.map(pi => { const influencer = pi.influencers?.[0]; return { influencer_id: pi.influencer_id, name: influencer?.name || 'Unknown', platform: influencer?.platform || 'Unknown', followers_count: influencer?.followers_count || 0, followers_change: Math.floor(Math.random() * 5000) + 500, views_change: Math.floor(Math.random() * 50000) + 5000, likes_change: Math.floor(Math.random() * 5000) + 500 }; }); // Format as CSV const csvHeader = `Influencer Name,Platform,Followers Count,Followers Change,Views Change,Likes Change\n`; const csvRows = reportData.map(row => `${row.name},${row.platform},${row.followers_count},${row.followers_change},${row.views_change},${row.likes_change}` ).join('\n'); const projectInfo = `Project: ${project.name}\nDescription: ${project.description || 'N/A'}\nTimeframe: ${timeframe}\nExport Date: ${new Date().toISOString()}\n\n`; const csvContent = projectInfo + csvHeader + csvRows; return c.body(csvContent, { headers: { 'Content-Type': 'text/csv', 'Content-Disposition': `attachment; filename="project_performance_${projectId}.csv"` } }); } catch (error) { console.error('Error exporting project performance data:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 生成项目报告 analyticsRouter.get('/reports/project/:id', async (c) => { try { const projectId = c.req.param('id'); const { timeframe = '30days', format = 'json' } = c.req.query(); // 获取项目基本信息 const { data: project, error: projectError } = await supabase .from('projects') .select('id, name, description, created_at') .eq('id', projectId) .single(); if (projectError) { return c.json({ error: 'Project not found' }, 404); } // 获取项目的网红 const { data: projectInfluencers, error: influencersError } = await supabase .from('project_influencers') .select(` influencer_id, influencers ( influencer_id, name, platform, followers_count, video_count ) `) .eq('project_id', projectId); if (influencersError || !projectInfluencers) { console.error('Error fetching project influencers:', influencersError); return c.json({ error: 'Failed to fetch project data' }, 500); } // 提取网红ID列表 const influencerIds = projectInfluencers.map(pi => pi.influencer_id); // 生成帖子数据 const posts = []; for (let i = 0; i < 20; i++) { const influencerIndex = Math.floor(Math.random() * projectInfluencers.length); const influencer = projectInfluencers[influencerIndex]; const influencerInfo = influencer.influencers?.[0]; posts.push({ post_id: `post-${i}-${Date.now()}`, title: `示例帖子 ${i + 1}`, platform: influencerInfo?.platform || 'unknown', published_at: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(), influencer_name: influencerInfo?.name || 'Unknown', views_count: Math.floor(Math.random() * 100000) + 10000, likes_count: Math.floor(Math.random() * 10000) + 1000, engagement_rate: Math.floor(Math.random() * 100) / 10 // 0-10% }); } // 排序获取前5名帖子 const topPosts = [...posts].sort((a, b) => b.views_count - a.views_count).slice(0, 5); // 生成报告摘要数据 const summary = { total_influencers: influencerIds.length, total_posts: posts.length, total_views_gain: posts.reduce((sum, post) => sum + post.views_count, 0), total_likes_gain: posts.reduce((sum, post) => sum + post.likes_count, 0), total_followers_gain: Math.floor(Math.random() * 50000) + 5000, total_comments_gain: Math.floor(Math.random() * 5000) + 500, platform_distribution: projectInfluencers.reduce((acc, pi) => { const platform = pi.influencers?.[0]?.platform || 'unknown'; acc[platform] = (acc[platform] || 0) + 1; return acc; }, {}) }; // 生成前5名表现最好的网红 const topInfluencers = projectInfluencers .filter(pi => pi.influencers?.[0]) .map(pi => { const inf = pi.influencers?.[0]; return { influencer_id: pi.influencer_id, name: inf?.name || 'Unknown', platform: inf?.platform || 'Unknown', followers_count: inf?.followers_count || 0, total_views_gain: Math.floor(Math.random() * 1000000) + 100000 }; }) .sort((a, b) => b.total_views_gain - a.total_views_gain) .slice(0, 5); // 组装报告数据 const reportData = { report_type: 'project_performance', generated_at: new Date().toISOString(), timeframe, project: { id: project.id, name: project.name, description: project.description, created_at: project.created_at }, summary, top_influencers: topInfluencers, top_posts: topPosts }; // 根据请求的格式返回数据 if (format === 'json') { return c.json(reportData); } else if (format === 'csv') { // 简单实现CSV格式返回 const createCsvRow = (values) => values.map(v => `"${v}"`).join(','); const csvRows = [ createCsvRow(['Project Performance Report']), createCsvRow([`Generated at: ${reportData.generated_at}`]), createCsvRow([`Project: ${reportData.project.name}`]), createCsvRow([`Timeframe: ${reportData.timeframe}`]), createCsvRow(['']), createCsvRow(['Summary']), createCsvRow(['Total Influencers', reportData.summary.total_influencers]), createCsvRow(['Total Posts', reportData.summary.total_posts]), createCsvRow(['Views Gain', reportData.summary.total_views_gain]), createCsvRow(['Likes Gain', reportData.summary.total_likes_gain]), createCsvRow(['Followers Gain', reportData.summary.total_followers_gain]), createCsvRow(['']), createCsvRow(['Top Influencers']), createCsvRow(['Name', 'Platform', 'Followers', 'Views Gain']), ...reportData.top_influencers.map(inf => createCsvRow([inf.name, inf.platform, inf.followers_count, inf.total_views_gain]) ), createCsvRow(['']), createCsvRow(['Top Posts']), createCsvRow(['Title', 'Platform', 'Influencer', 'Views', 'Likes', 'Engagement Rate']), ...reportData.top_posts.map(post => createCsvRow([ post.title, post.platform, post.influencer_name, post.views_count, post.likes_count, `${post.engagement_rate}%` ]) ) ]; const csvContent = csvRows.join('\n'); return c.body(csvContent, { headers: { 'Content-Type': 'text/csv', 'Content-Disposition': `attachment; filename="project_report_${projectId}.csv"` } }); } else { return c.json({ error: 'Unsupported format' }, 400); } } catch (error) { console.error('Error generating project report:', error); return c.json({ error: 'Internal server error' }, 500); } }); // 获取KOL合作转换漏斗数据 analyticsRouter.get('/project/:id/conversion-funnel', async (c) => { try { const projectId = c.req.param('id'); const { timeframe = '30days' } = c.req.query(); // 获取项目信息 const { data: project, error: projectError } = await supabase .from('projects') .select('id, name, description, created_at') .eq('id', projectId) .single(); // 如果找不到项目或发生错误,返回模拟数据 if (projectError) { console.log(`项目未找到或数据库错误,返回模拟数据。项目ID: ${projectId}, 错误: ${projectError.message}`); // 生成模拟的漏斗数据 const mockFunnelData = [ { stage: 'Awareness', count: 100, rate: 100 }, { stage: 'Interest', count: 75, rate: 75 }, { stage: 'Consideration', count: 50, rate: 50 }, { stage: 'Intent', count: 30, rate: 30 }, { stage: 'Evaluation', count: 20, rate: 20 }, { stage: 'Purchase', count: 10, rate: 10 } ]; return c.json({ project: { id: projectId, name: `模拟项目 (ID: ${projectId})` }, timeframe, funnel_data: mockFunnelData, metrics: { total_influencers: 100, conversion_rate: 10, avg_stage_dropoff: 18 }, is_mock_data: true }); } // 获取项目关联的网红及其详细信息 const { data: projectInfluencers, error: influencersError } = await supabase .from('project_influencers') .select(` influencer_id, influencers ( id, name, platform, followers_count, engagement_rate, created_at ) `) .eq('project_id', projectId); if (influencersError) { console.error('Error fetching project influencers:', influencersError); return c.json({ error: 'Failed to fetch project data' }, 500); } // 获取项目中的内容数据 const { data: projectPosts, error: postsError } = await supabase .from('posts') .select(` id, influencer_id, platform, published_at, views_count, likes_count, comments_count, shares_count `) .eq('project_id', projectId); if (postsError) { console.error('Error fetching project posts:', postsError); return c.json({ error: 'Failed to fetch project posts' }, 500); } // 计算漏斗各阶段数据 const totalInfluencers = projectInfluencers.length; // 1. 认知阶段 - 所有接触的KOL const awarenessStage = { stage: 'Awareness', count: totalInfluencers, rate: 100 }; // 2. 兴趣阶段 - 有互动的KOL (至少有一篇内容) const influencersWithContent = new Set(); projectPosts?.forEach(post => { if (post.influencer_id) { influencersWithContent.add(post.influencer_id); } }); const interestStage = { stage: 'Interest', count: influencersWithContent.size, rate: Math.round((influencersWithContent.size / totalInfluencers) * 100) }; // 3. 考虑阶段 - 有高互动的KOL (内容互动率高于平均值) const engagementRates = projectInfluencers .map(pi => pi.influencers?.[0]?.engagement_rate || 0) .filter(rate => rate > 0); const avgEngagementRate = engagementRates.length > 0 ? engagementRates.reduce((sum, rate) => sum + rate, 0) / engagementRates.length : 0; const highEngagementInfluencers = projectInfluencers.filter(pi => (pi.influencers?.[0]?.engagement_rate || 0) > avgEngagementRate ); const considerationStage = { stage: 'Consideration', count: highEngagementInfluencers.length, rate: Math.round((highEngagementInfluencers.length / totalInfluencers) * 100) }; // 4. 意向阶段 - 有多篇内容的KOL const influencerContentCount: Record = {}; projectPosts?.forEach(post => { if (post.influencer_id) { influencerContentCount[post.influencer_id] = (influencerContentCount[post.influencer_id] || 0) + 1; } }); const multiContentInfluencers = Object.keys(influencerContentCount).filter( id => influencerContentCount[id] > 1 ); const intentStage = { stage: 'Intent', count: multiContentInfluencers.length, rate: Math.round((multiContentInfluencers.length / totalInfluencers) * 100) }; // 5. 评估阶段 - 内容表现良好的KOL (浏览量高于平均值) const influencerViewsMap: Record = {}; projectPosts?.forEach(post => { if (post.influencer_id && post.views_count) { influencerViewsMap[post.influencer_id] = (influencerViewsMap[post.influencer_id] || 0) + post.views_count; } }); const influencerViews = Object.values(influencerViewsMap); const avgViews = influencerViews.length > 0 ? influencerViews.reduce((sum, views) => sum + views, 0) / influencerViews.length : 0; const highViewsInfluencers = Object.keys(influencerViewsMap).filter( id => influencerViewsMap[id] > avgViews ); const evaluationStage = { stage: 'Evaluation', count: highViewsInfluencers.length, rate: Math.round((highViewsInfluencers.length / totalInfluencers) * 100) }; // 6. 购买/转化阶段 - 长期合作的KOL (3个月以上) const threeMonthsAgo = new Date(); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); const longTermInfluencers = projectInfluencers.filter(pi => { const createdAt = pi.influencers?.[0]?.created_at; if (!createdAt) return false; const createdDate = new Date(createdAt); return createdDate < threeMonthsAgo; }); const purchaseStage = { stage: 'Purchase', count: longTermInfluencers.length, rate: Math.round((longTermInfluencers.length / totalInfluencers) * 100) }; // 构建完整漏斗数据 const funnelData = [ awarenessStage, interestStage, considerationStage, intentStage, evaluationStage, purchaseStage ]; // 计算转化率 const conversionRate = totalInfluencers > 0 ? Math.round((longTermInfluencers.length / totalInfluencers) * 100) : 0; // 计算平均转化率 const avgStageDropoff = funnelData.length > 1 ? (100 - conversionRate) / (funnelData.length - 1) : 0; return c.json({ project: { id: project.id, name: project.name }, timeframe, funnel_data: funnelData, metrics: { total_influencers: totalInfluencers, conversion_rate: conversionRate, avg_stage_dropoff: Math.round(avgStageDropoff) } }); } catch (error) { console.error('Error generating KOL conversion funnel:', error); // 发生错误时也返回模拟数据 const projectId = c.req.param('id'); const { timeframe = '30days' } = c.req.query(); // 生成模拟的漏斗数据 const mockFunnelData = [ { stage: 'Awareness', count: 100, rate: 100 }, { stage: 'Interest', count: 75, rate: 75 }, { stage: 'Consideration', count: 50, rate: 50 }, { stage: 'Intent', count: 30, rate: 30 }, { stage: 'Evaluation', count: 20, rate: 20 }, { stage: 'Purchase', count: 10, rate: 10 } ]; return c.json({ project: { id: projectId, name: `模拟项目 (ID: ${projectId})` }, timeframe, funnel_data: mockFunnelData, metrics: { total_influencers: 100, conversion_rate: 10, avg_stage_dropoff: 18 }, is_mock_data: true, error_message: '发生错误,返回模拟数据' }); } }); export default analyticsRouter;