an
This commit is contained in:
@@ -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"
|
|
||||||
@@ -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<string, number> = {};
|
||||||
|
|
||||||
|
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<string, number> = {};
|
||||||
|
|
||||||
|
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
|
// Get analytics for a content
|
||||||
analyticsRouter.get('/content/:id', async (c) => {
|
analyticsRouter.get('/content/:id', async (c) => {
|
||||||
try {
|
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) => {
|
analyticsRouter.get('/influencer/:id/details', async (c) => {
|
||||||
try {
|
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<string, number> = {};
|
||||||
|
|
||||||
|
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 =====
|
// ===== Scheduled Collection Endpoints =====
|
||||||
|
|
||||||
// Schedule automated data collection for an influencer
|
// 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;
|
export default analyticsRouter;
|
||||||
Reference in New Issue
Block a user