From acf8a06375e36646cd8d6f90d00adbdd37602b18 Mon Sep 17 00:00:00 2001 From: William Tso Date: Mon, 10 Mar 2025 19:10:51 +0800 Subject: [PATCH] an --- backend/.env.example | 28 -- backend/src/routes/analytics.ts | 677 ++++++++++++++++++++++++++++++++ 2 files changed, 677 insertions(+), 28 deletions(-) delete mode 100644 backend/.env.example diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index 1fe9e40..0000000 --- a/backend/.env.example +++ /dev/null @@ -1,28 +0,0 @@ -PORT=4000 - -SUPABASE_URL="your-supabase-url" -SUPABASE_KEY="your-supabase-key" -SUPABASE_ANON_KEY="your-supabase-anon-key" -DATABASE_URL="your-database-url" - -REDIS_HOST="localhost" -REDIS_PORT="6379" -REDIS_PASSWORD="" -DOMAIN="upj.to" -ENABLED_ROUTES=all - -# ClickHouse Configuration -CLICKHOUSE_HOST="localhost" -CLICKHOUSE_PORT="8123" -CLICKHOUSE_USER="admin" -CLICKHOUSE_PASSWORD="your_secure_password" -CLICKHOUSE_DATABASE="promote" - -# BullMQ Configuration -BULL_REDIS_HOST="localhost" -BULL_REDIS_PORT="6379" -BULL_REDIS_PASSWORD="" - -# JWT Configuration -JWT_SECRET="your-jwt-secret-key" -JWT_EXPIRES_IN="7d" \ No newline at end of file diff --git a/backend/src/routes/analytics.ts b/backend/src/routes/analytics.ts index 2b29f4a..7b1f273 100644 --- a/backend/src/routes/analytics.ts +++ b/backend/src/routes/analytics.ts @@ -178,6 +178,102 @@ analyticsRouter.post('/follow', async (c) => { } }); +// 追踪内容互动数据 +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 { @@ -473,6 +569,128 @@ analyticsRouter.get('/post/:id/like-trend', async (c) => { } }); +// 获取内容互动趋势 +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 { @@ -616,6 +834,214 @@ analyticsRouter.get('/project/:id/interaction-types', async (c) => { } }); +// 获取项目整体分析 +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 @@ -821,4 +1247,255 @@ analyticsRouter.get('/export/influencer/:id/growth', async (c) => { } }); +// 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, created_by') + .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); + } +}); + export default analyticsRouter; \ No newline at end of file