This commit is contained in:
2025-03-10 19:10:51 +08:00
parent 5ef6c75360
commit acf8a06375
2 changed files with 677 additions and 28 deletions

View File

@@ -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"

View File

@@ -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;