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