an
This commit is contained in:
@@ -347,6 +347,97 @@ analyticsRouter.get('/influencer/:id/follower-trend', async (c) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取网红增长趋势(支持不同指标和时间粒度)
|
||||||
|
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天)
|
// 获取帖子的点赞变化(过去30天)
|
||||||
analyticsRouter.get('/post/:id/like-trend', async (c) => {
|
analyticsRouter.get('/post/:id/like-trend', async (c) => {
|
||||||
try {
|
try {
|
||||||
@@ -690,190 +781,42 @@ analyticsRouter.get('/export/influencer/:id/growth', async (c) => {
|
|||||||
query: `
|
query: `
|
||||||
SELECT
|
SELECT
|
||||||
${intervalFunction} AS time_period,
|
${intervalFunction} AS time_period,
|
||||||
sumIf(metric_value, metric_name = ?) AS change,
|
sumIf(metric_value, metric_name = '${metric}') AS change,
|
||||||
maxIf(metric_total, metric_name = ?) AS total_value
|
maxIf(metric_total, metric_name = '${metric}') AS total_value
|
||||||
FROM promote.events
|
FROM events
|
||||||
WHERE
|
WHERE
|
||||||
influencer_id = ? AND
|
influencer_id = '${influencerId}' AND
|
||||||
event_type = ? AND
|
event_type = '${metric}_change' AND
|
||||||
${timeRangeSql}
|
${timeRangeSql}
|
||||||
GROUP BY time_period
|
GROUP BY time_period
|
||||||
ORDER BY time_period ASC
|
ORDER BY time_period ASC
|
||||||
`,
|
`
|
||||||
values: [
|
|
||||||
metric,
|
|
||||||
metric,
|
|
||||||
influencerId,
|
|
||||||
`${metric}_change`
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get influencer info
|
// Extract data
|
||||||
const { data: influencer } = await supabase
|
const trendData = 'rows' in result ? result.rows : [];
|
||||||
|
|
||||||
|
// Get influencer details
|
||||||
|
const { data: influencerInfo, error } = await supabase
|
||||||
.from('influencers')
|
.from('influencers')
|
||||||
.select('name, platform')
|
.select('name, platform, followers_count, video_count')
|
||||||
.eq('influencer_id', influencerId)
|
.eq('influencer_id', influencerId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
// Extract trend data
|
if (error) {
|
||||||
const trendData = 'rows' in result ? result.rows : [];
|
console.error('Error fetching influencer details:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Format as CSV
|
return c.json({
|
||||||
const csvHeader = `Time Period,Change,Total Value\n`;
|
influencer_id: influencerId,
|
||||||
const csvRows = trendData.map((row: any) =>
|
influencer_info: influencerInfo || null,
|
||||||
`${row.time_period},${row.change},${row.total_value}`
|
metric,
|
||||||
).join('\n');
|
timeframe,
|
||||||
|
interval,
|
||||||
const influencerInfo = influencer
|
data: trendData
|
||||||
? `Influencer: ${influencer.name} (${influencer.platform})\nMetric: ${metric}\nTimeframe: ${timeframe}\nInterval: ${interval}\n\n`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const csvContent = influencerInfo + csvHeader + csvRows;
|
|
||||||
|
|
||||||
return c.body(csvContent, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/csv',
|
|
||||||
'Content-Disposition': `attachment; filename="influencer_growth_${influencerId}.csv"`
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error exporting influencer growth data:', 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();
|
|
||||||
|
|
||||||
// Get project information
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get project influencers
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine time range
|
|
||||||
let startDate: Date;
|
|
||||||
const endDate = new Date();
|
|
||||||
|
|
||||||
switch (timeframe) {
|
|
||||||
case '7days':
|
|
||||||
startDate = new Date(endDate);
|
|
||||||
startDate.setDate(endDate.getDate() - 7);
|
|
||||||
break;
|
|
||||||
case '30days':
|
|
||||||
default:
|
|
||||||
startDate = new Date(endDate);
|
|
||||||
startDate.setDate(endDate.getDate() - 30);
|
|
||||||
break;
|
|
||||||
case '90days':
|
|
||||||
startDate = new Date(endDate);
|
|
||||||
startDate.setDate(endDate.getDate() - 90);
|
|
||||||
break;
|
|
||||||
case '6months':
|
|
||||||
startDate = new Date(endDate);
|
|
||||||
startDate.setMonth(endDate.getMonth() - 6);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get influencer details
|
|
||||||
const { data: influencersData } = await supabase
|
|
||||||
.from('influencers')
|
|
||||||
.select('influencer_id, name, platform, followers_count')
|
|
||||||
.in('influencer_id', influencerIds);
|
|
||||||
|
|
||||||
// Get metrics from ClickHouse
|
|
||||||
const metricsResult = await clickhouse.query({
|
|
||||||
query: `
|
|
||||||
SELECT
|
|
||||||
influencer_id,
|
|
||||||
sumIf(metric_value, event_type = 'followers_count_change') AS followers_change,
|
|
||||||
sumIf(metric_value, event_type = 'post_views_count_change') AS views_change,
|
|
||||||
sumIf(metric_value, event_type = 'post_likes_count_change') AS likes_change
|
|
||||||
FROM promote.events
|
|
||||||
WHERE
|
|
||||||
influencer_id IN (?) AND
|
|
||||||
timestamp >= ? AND
|
|
||||||
timestamp <= ?
|
|
||||||
GROUP BY influencer_id
|
|
||||||
`,
|
|
||||||
values: [
|
|
||||||
influencerIds,
|
|
||||||
startDate.toISOString(),
|
|
||||||
endDate.toISOString()
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract metrics data
|
|
||||||
const metricsData = 'rows' in metricsResult ? metricsResult.rows : [];
|
|
||||||
|
|
||||||
// Combine data
|
|
||||||
const reportData = (influencersData || []).map(influencer => {
|
|
||||||
const metrics = metricsData.find((m: any) => m.influencer_id === influencer.influencer_id) || {
|
|
||||||
followers_change: 0,
|
|
||||||
views_change: 0,
|
|
||||||
likes_change: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
influencer_id: influencer.influencer_id,
|
|
||||||
name: influencer.name,
|
|
||||||
platform: influencer.platform,
|
|
||||||
followers_count: influencer.followers_count,
|
|
||||||
followers_change: metrics.followers_change || 0,
|
|
||||||
views_change: metrics.views_change || 0,
|
|
||||||
likes_change: metrics.likes_change || 0
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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);
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
802
backend/src/routes/analytics.ts.bak
Normal file
802
backend/src/routes/analytics.ts.bak
Normal file
@@ -0,0 +1,802 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定时间范围和间隔函数
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从ClickHouse查询数据
|
||||||
|
const result = await clickhouse.query({
|
||||||
|
query: `
|
||||||
|
SELECT
|
||||||
|
${intervalFunction} AS time_period,
|
||||||
|
sumIf(metric_value, metric_name = ?) AS change,
|
||||||
|
maxIf(metric_total, metric_name = ?) AS total_value
|
||||||
|
FROM promote.events
|
||||||
|
WHERE
|
||||||
|
influencer_id = ? AND
|
||||||
|
event_type = ? AND
|
||||||
|
${timeRangeSql}
|
||||||
|
GROUP BY time_period
|
||||||
|
ORDER BY time_period ASC
|
||||||
|
`,
|
||||||
|
values: [
|
||||||
|
metric,
|
||||||
|
metric,
|
||||||
|
influencerId,
|
||||||
|
`${metric}_change`
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提取数据
|
||||||
|
const trendData = 'rows' in result ? result.rows : [];
|
||||||
|
|
||||||
|
// 获取网红基本信息
|
||||||
|
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 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('/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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 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 = ?) AS change,
|
||||||
|
maxIf(metric_total, metric_name = ?) AS total_value
|
||||||
|
FROM promote.events
|
||||||
|
WHERE
|
||||||
|
influencer_id = ? AND
|
||||||
|
event_type = ? AND
|
||||||
|
${timeRangeSql}
|
||||||
|
GROUP BY time_period
|
||||||
|
ORDER BY time_period ASC
|
||||||
|
`
|
||||||
@@ -1555,7 +1555,876 @@ 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}/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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
|||||||
Reference in New Issue
Block a user