post use api
This commit is contained in:
@@ -1511,35 +1511,35 @@ analyticsRouter.get('/project/:id/conversion-funnel', async (c) => {
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
|
||||
// 如果找不到项目或发生错误,返回模拟数据
|
||||
if (projectError) {
|
||||
console.log(`项目未找到或数据库错误,返回模拟数据。项目ID: ${projectId}, 错误: ${projectError.message}`);
|
||||
// // 如果找不到项目或发生错误,返回模拟数据
|
||||
// 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 }
|
||||
];
|
||||
// // 生成模拟的漏斗数据
|
||||
// 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
|
||||
});
|
||||
}
|
||||
// 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
|
||||
|
||||
@@ -1,802 +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);
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
`
|
||||
Reference in New Issue
Block a user