funel data
This commit is contained in:
@@ -1341,7 +1341,7 @@ analyticsRouter.get('/reports/project/:id', async (c) => {
|
||||
// 获取项目基本信息
|
||||
const { data: project, error: projectError } = await supabase
|
||||
.from('projects')
|
||||
.select('id, name, description, created_at, created_by')
|
||||
.select('id, name, description, created_at')
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
|
||||
@@ -1498,4 +1498,258 @@ analyticsRouter.get('/reports/project/:id', async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取KOL合作转换漏斗数据
|
||||
analyticsRouter.get('/project/:id/conversion-funnel', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
const { timeframe = '30days' } = c.req.query();
|
||||
|
||||
// 获取项目信息
|
||||
const { data: project, error: projectError } = await supabase
|
||||
.from('projects')
|
||||
.select('id, name, description, created_at')
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
|
||||
// 如果找不到项目或发生错误,返回模拟数据
|
||||
if (projectError) {
|
||||
console.log(`项目未找到或数据库错误,返回模拟数据。项目ID: ${projectId}, 错误: ${projectError.message}`);
|
||||
|
||||
// 生成模拟的漏斗数据
|
||||
const mockFunnelData = [
|
||||
{ stage: 'Awareness', count: 100, rate: 100 },
|
||||
{ stage: 'Interest', count: 75, rate: 75 },
|
||||
{ stage: 'Consideration', count: 50, rate: 50 },
|
||||
{ stage: 'Intent', count: 30, rate: 30 },
|
||||
{ stage: 'Evaluation', count: 20, rate: 20 },
|
||||
{ stage: 'Purchase', count: 10, rate: 10 }
|
||||
];
|
||||
|
||||
return c.json({
|
||||
project: {
|
||||
id: projectId,
|
||||
name: `模拟项目 (ID: ${projectId})`
|
||||
},
|
||||
timeframe,
|
||||
funnel_data: mockFunnelData,
|
||||
metrics: {
|
||||
total_influencers: 100,
|
||||
conversion_rate: 10,
|
||||
avg_stage_dropoff: 18
|
||||
},
|
||||
is_mock_data: true
|
||||
});
|
||||
}
|
||||
|
||||
// 获取项目关联的网红及其详细信息
|
||||
const { data: projectInfluencers, error: influencersError } = await supabase
|
||||
.from('project_influencers')
|
||||
.select(`
|
||||
influencer_id,
|
||||
influencers (
|
||||
id,
|
||||
name,
|
||||
platform,
|
||||
followers_count,
|
||||
engagement_rate,
|
||||
created_at
|
||||
)
|
||||
`)
|
||||
.eq('project_id', projectId);
|
||||
|
||||
if (influencersError) {
|
||||
console.error('Error fetching project influencers:', influencersError);
|
||||
return c.json({ error: 'Failed to fetch project data' }, 500);
|
||||
}
|
||||
|
||||
// 获取项目中的内容数据
|
||||
const { data: projectPosts, error: postsError } = await supabase
|
||||
.from('posts')
|
||||
.select(`
|
||||
id,
|
||||
influencer_id,
|
||||
platform,
|
||||
published_at,
|
||||
views_count,
|
||||
likes_count,
|
||||
comments_count,
|
||||
shares_count
|
||||
`)
|
||||
.eq('project_id', projectId);
|
||||
|
||||
if (postsError) {
|
||||
console.error('Error fetching project posts:', postsError);
|
||||
return c.json({ error: 'Failed to fetch project posts' }, 500);
|
||||
}
|
||||
|
||||
// 计算漏斗各阶段数据
|
||||
const totalInfluencers = projectInfluencers.length;
|
||||
|
||||
// 1. 认知阶段 - 所有接触的KOL
|
||||
const awarenessStage = {
|
||||
stage: 'Awareness',
|
||||
count: totalInfluencers,
|
||||
rate: 100
|
||||
};
|
||||
|
||||
// 2. 兴趣阶段 - 有互动的KOL (至少有一篇内容)
|
||||
const influencersWithContent = new Set<string>();
|
||||
projectPosts?.forEach(post => {
|
||||
if (post.influencer_id) {
|
||||
influencersWithContent.add(post.influencer_id);
|
||||
}
|
||||
});
|
||||
const interestStage = {
|
||||
stage: 'Interest',
|
||||
count: influencersWithContent.size,
|
||||
rate: Math.round((influencersWithContent.size / totalInfluencers) * 100)
|
||||
};
|
||||
|
||||
// 3. 考虑阶段 - 有高互动的KOL (内容互动率高于平均值)
|
||||
const engagementRates = projectInfluencers
|
||||
.map(pi => pi.influencers?.[0]?.engagement_rate || 0)
|
||||
.filter(rate => rate > 0);
|
||||
|
||||
const avgEngagementRate = engagementRates.length > 0
|
||||
? engagementRates.reduce((sum, rate) => sum + rate, 0) / engagementRates.length
|
||||
: 0;
|
||||
|
||||
const highEngagementInfluencers = projectInfluencers.filter(pi =>
|
||||
(pi.influencers?.[0]?.engagement_rate || 0) > avgEngagementRate
|
||||
);
|
||||
|
||||
const considerationStage = {
|
||||
stage: 'Consideration',
|
||||
count: highEngagementInfluencers.length,
|
||||
rate: Math.round((highEngagementInfluencers.length / totalInfluencers) * 100)
|
||||
};
|
||||
|
||||
// 4. 意向阶段 - 有多篇内容的KOL
|
||||
const influencerContentCount: Record<string, number> = {};
|
||||
projectPosts?.forEach(post => {
|
||||
if (post.influencer_id) {
|
||||
influencerContentCount[post.influencer_id] = (influencerContentCount[post.influencer_id] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
const multiContentInfluencers = Object.keys(influencerContentCount).filter(
|
||||
id => influencerContentCount[id] > 1
|
||||
);
|
||||
|
||||
const intentStage = {
|
||||
stage: 'Intent',
|
||||
count: multiContentInfluencers.length,
|
||||
rate: Math.round((multiContentInfluencers.length / totalInfluencers) * 100)
|
||||
};
|
||||
|
||||
// 5. 评估阶段 - 内容表现良好的KOL (浏览量高于平均值)
|
||||
const influencerViewsMap: Record<string, number> = {};
|
||||
projectPosts?.forEach(post => {
|
||||
if (post.influencer_id && post.views_count) {
|
||||
influencerViewsMap[post.influencer_id] = (influencerViewsMap[post.influencer_id] || 0) + post.views_count;
|
||||
}
|
||||
});
|
||||
|
||||
const influencerViews = Object.values(influencerViewsMap);
|
||||
const avgViews = influencerViews.length > 0
|
||||
? influencerViews.reduce((sum, views) => sum + views, 0) / influencerViews.length
|
||||
: 0;
|
||||
|
||||
const highViewsInfluencers = Object.keys(influencerViewsMap).filter(
|
||||
id => influencerViewsMap[id] > avgViews
|
||||
);
|
||||
|
||||
const evaluationStage = {
|
||||
stage: 'Evaluation',
|
||||
count: highViewsInfluencers.length,
|
||||
rate: Math.round((highViewsInfluencers.length / totalInfluencers) * 100)
|
||||
};
|
||||
|
||||
// 6. 购买/转化阶段 - 长期合作的KOL (3个月以上)
|
||||
const threeMonthsAgo = new Date();
|
||||
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
|
||||
|
||||
const longTermInfluencers = projectInfluencers.filter(pi => {
|
||||
const createdAt = pi.influencers?.[0]?.created_at;
|
||||
if (!createdAt) return false;
|
||||
|
||||
const createdDate = new Date(createdAt);
|
||||
return createdDate < threeMonthsAgo;
|
||||
});
|
||||
|
||||
const purchaseStage = {
|
||||
stage: 'Purchase',
|
||||
count: longTermInfluencers.length,
|
||||
rate: Math.round((longTermInfluencers.length / totalInfluencers) * 100)
|
||||
};
|
||||
|
||||
// 构建完整漏斗数据
|
||||
const funnelData = [
|
||||
awarenessStage,
|
||||
interestStage,
|
||||
considerationStage,
|
||||
intentStage,
|
||||
evaluationStage,
|
||||
purchaseStage
|
||||
];
|
||||
|
||||
// 计算转化率
|
||||
const conversionRate = totalInfluencers > 0
|
||||
? Math.round((longTermInfluencers.length / totalInfluencers) * 100)
|
||||
: 0;
|
||||
|
||||
// 计算平均转化率
|
||||
const avgStageDropoff = funnelData.length > 1
|
||||
? (100 - conversionRate) / (funnelData.length - 1)
|
||||
: 0;
|
||||
|
||||
return c.json({
|
||||
project: {
|
||||
id: project.id,
|
||||
name: project.name
|
||||
},
|
||||
timeframe,
|
||||
funnel_data: funnelData,
|
||||
metrics: {
|
||||
total_influencers: totalInfluencers,
|
||||
conversion_rate: conversionRate,
|
||||
avg_stage_dropoff: Math.round(avgStageDropoff)
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating KOL conversion funnel:', error);
|
||||
|
||||
// 发生错误时也返回模拟数据
|
||||
const projectId = c.req.param('id');
|
||||
const { timeframe = '30days' } = c.req.query();
|
||||
|
||||
// 生成模拟的漏斗数据
|
||||
const mockFunnelData = [
|
||||
{ stage: 'Awareness', count: 100, rate: 100 },
|
||||
{ stage: 'Interest', count: 75, rate: 75 },
|
||||
{ stage: 'Consideration', count: 50, rate: 50 },
|
||||
{ stage: 'Intent', count: 30, rate: 30 },
|
||||
{ stage: 'Evaluation', count: 20, rate: 20 },
|
||||
{ stage: 'Purchase', count: 10, rate: 10 }
|
||||
];
|
||||
|
||||
return c.json({
|
||||
project: {
|
||||
id: projectId,
|
||||
name: `模拟项目 (ID: ${projectId})`
|
||||
},
|
||||
timeframe,
|
||||
funnel_data: mockFunnelData,
|
||||
metrics: {
|
||||
total_influencers: 100,
|
||||
conversion_rate: 10,
|
||||
avg_stage_dropoff: 18
|
||||
},
|
||||
is_mock_data: true,
|
||||
error_message: '发生错误,返回模拟数据'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default analyticsRouter;
|
||||
@@ -1987,6 +1987,137 @@ export const openAPISpec = {
|
||||
}
|
||||
}
|
||||
},
|
||||
'/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'],
|
||||
|
||||
Reference in New Issue
Block a user