kunnel
This commit is contained in:
@@ -12,11 +12,11 @@ export interface KolPerformanceData {
|
||||
profile_url: string;
|
||||
followers_count: number;
|
||||
followers_change: number;
|
||||
followers_change_percentage: number | null;
|
||||
followers_change_percentage: number | null | string;
|
||||
likes_change: number;
|
||||
likes_change_percentage: number | null;
|
||||
likes_change_percentage: number | null | string;
|
||||
follows_change: number;
|
||||
follows_change_percentage: number | null;
|
||||
follows_change_percentage: number | null | string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,6 +27,34 @@ export interface KolPerformanceResponse {
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 漏斗阶段数据
|
||||
*/
|
||||
export interface FunnelStageData {
|
||||
stage: string; // 阶段名称
|
||||
stage_display: string; // 阶段显示名称
|
||||
count: number; // 用户数量
|
||||
percentage: number; // 总体占比(%)
|
||||
conversion_rate: number | null; // 与上一阶段的转化率(%)
|
||||
}
|
||||
|
||||
/**
|
||||
* 漏斗分析总览数据
|
||||
*/
|
||||
export interface FunnelOverview {
|
||||
average_conversion_rate: number; // 平均转化率(%)
|
||||
highest_conversion_stage: string; // 转化率最高的阶段
|
||||
lowest_conversion_stage: string; // 转化率最低的阶段
|
||||
}
|
||||
|
||||
/**
|
||||
* 漏斗分析响应
|
||||
*/
|
||||
export interface FunnelResponse {
|
||||
stages: FunnelStageData[]; // 各阶段数据
|
||||
overview: FunnelOverview; // 总览数据
|
||||
}
|
||||
|
||||
/**
|
||||
* Analytics service for KOL performance data
|
||||
*/
|
||||
@@ -169,11 +197,11 @@ export class AnalyticsService {
|
||||
profile_url: String(influencer.profile_url || ''),
|
||||
followers_count: Number(influencer.followers_count || 0),
|
||||
followers_change: Number(events.followers_change || 0),
|
||||
followers_change_percentage: null, // We'll calculate this in a separate query if needed
|
||||
followers_change_percentage: "待计算",
|
||||
likes_change: Number(events.likes_change || 0),
|
||||
likes_change_percentage: null,
|
||||
likes_change_percentage: "待计算",
|
||||
follows_change: Number(events.follows_change || 0),
|
||||
follows_change_percentage: null
|
||||
follows_change_percentage: "待计算"
|
||||
};
|
||||
});
|
||||
|
||||
@@ -368,6 +396,157 @@ export class AnalyticsService {
|
||||
logger.warn('Error in debugEventData', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取KOL合作转换漏斗数据
|
||||
* @param timeRange 时间范围(天数)
|
||||
* @param projectId 可选项目ID
|
||||
* @returns 漏斗数据
|
||||
*/
|
||||
async getKolFunnel(
|
||||
timeRange: number,
|
||||
projectId?: string
|
||||
): Promise<FunnelResponse> {
|
||||
const startTime = Date.now();
|
||||
logger.info('Fetching KOL funnel data', { timeRange, projectId });
|
||||
|
||||
try {
|
||||
// 计算时间范围
|
||||
const currentDate = new Date();
|
||||
const pastDate = new Date();
|
||||
pastDate.setDate(currentDate.getDate() - timeRange);
|
||||
|
||||
// 格式化日期
|
||||
const currentDateStr = this.formatDateForClickhouse(currentDate);
|
||||
const pastDateStr = this.formatDateForClickhouse(pastDate);
|
||||
|
||||
// 构建项目过滤条件
|
||||
const projectFilter = projectId ? `AND project_id = '${projectId}'` : '';
|
||||
|
||||
// 漏斗阶段及其显示名称
|
||||
const stages = [
|
||||
{ id: 'exposure', display: '曝光' },
|
||||
{ id: 'interest', display: '兴趣' },
|
||||
{ id: 'consideration', display: '考虑' },
|
||||
{ id: 'intent', display: '意向' },
|
||||
{ id: 'evaluation', display: '评估' },
|
||||
{ id: 'purchase', display: '购买' }
|
||||
];
|
||||
|
||||
// 查询每个阶段的用户数量
|
||||
const funnelQuery = `
|
||||
SELECT
|
||||
funnel_stage,
|
||||
COUNT(DISTINCT user_id) as user_count
|
||||
FROM
|
||||
events
|
||||
WHERE
|
||||
date BETWEEN '${pastDateStr}' AND '${currentDateStr}'
|
||||
${projectFilter}
|
||||
GROUP BY
|
||||
funnel_stage
|
||||
ORDER BY
|
||||
CASE funnel_stage
|
||||
WHEN 'exposure' THEN 1
|
||||
WHEN 'interest' THEN 2
|
||||
WHEN 'consideration' THEN 3
|
||||
WHEN 'intent' THEN 4
|
||||
WHEN 'evaluation' THEN 5
|
||||
WHEN 'purchase' THEN 6
|
||||
ELSE 7
|
||||
END
|
||||
`;
|
||||
|
||||
logger.debug('Executing ClickHouse query for funnel data', {
|
||||
funnelQuery: funnelQuery.replace(/\n\s+/g, ' ').trim()
|
||||
});
|
||||
|
||||
// 执行查询
|
||||
const funnelData = await this.executeClickhouseQuery(funnelQuery);
|
||||
|
||||
// 将结果转换为Map便于查找
|
||||
const stageCounts: Record<string, number> = {};
|
||||
if (Array.isArray(funnelData)) {
|
||||
funnelData.forEach(item => {
|
||||
if (item.funnel_stage && item.user_count) {
|
||||
stageCounts[item.funnel_stage] = Number(item.user_count);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 计算总用户数(以最上层漏斗的用户数为准)
|
||||
const totalUsers = stageCounts['exposure'] || 0;
|
||||
|
||||
// 构建漏斗阶段数据
|
||||
const stagesData: FunnelStageData[] = [];
|
||||
let prevCount = 0;
|
||||
|
||||
stages.forEach((stage, index) => {
|
||||
const count = stageCounts[stage.id] || 0;
|
||||
const percentage = totalUsers > 0 ? (count / totalUsers * 100) : 0;
|
||||
let conversionRate = null;
|
||||
|
||||
if (index > 0 && prevCount > 0) {
|
||||
conversionRate = (count / prevCount) * 100;
|
||||
}
|
||||
|
||||
stagesData.push({
|
||||
stage: stage.id,
|
||||
stage_display: stage.display,
|
||||
count,
|
||||
percentage: parseFloat(percentage.toFixed(2)),
|
||||
conversion_rate: conversionRate !== null ? parseFloat(conversionRate.toFixed(2)) : null
|
||||
});
|
||||
|
||||
prevCount = count;
|
||||
});
|
||||
|
||||
// 计算总览数据
|
||||
const conversionRates = stagesData
|
||||
.filter(stage => stage.conversion_rate !== null)
|
||||
.map(stage => stage.conversion_rate as number);
|
||||
|
||||
const averageConversionRate = conversionRates.length > 0
|
||||
? parseFloat((conversionRates.reduce((sum, rate) => sum + rate, 0) / conversionRates.length).toFixed(2))
|
||||
: 0;
|
||||
|
||||
// 找出转化率最高和最低的阶段
|
||||
let highestStage = { stage_display: '无数据', rate: 0 };
|
||||
let lowestStage = { stage_display: '无数据', rate: 100 };
|
||||
|
||||
stagesData.forEach(stage => {
|
||||
if (stage.conversion_rate !== null) {
|
||||
if (stage.conversion_rate > highestStage.rate) {
|
||||
highestStage = { stage_display: stage.stage_display, rate: stage.conversion_rate };
|
||||
}
|
||||
if (stage.conversion_rate < lowestStage.rate) {
|
||||
lowestStage = { stage_display: stage.stage_display, rate: stage.conversion_rate };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const overview: FunnelOverview = {
|
||||
average_conversion_rate: averageConversionRate,
|
||||
highest_conversion_stage: highestStage.stage_display,
|
||||
lowest_conversion_stage: lowestStage.stage_display
|
||||
};
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info('KOL funnel data fetched successfully', {
|
||||
duration,
|
||||
totalUsers
|
||||
});
|
||||
|
||||
return {
|
||||
stages: stagesData,
|
||||
overview
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
logger.error(`Error in getKolFunnel (${duration}ms)`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
|
||||
Reference in New Issue
Block a user