Files
promote/backend/src/routes/analytics.ts
2025-03-11 13:05:51 +08:00

1755 lines
53 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<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
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<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 =====
// 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<string>();
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<string, number> = {};
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<string, number> = {};
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;