This commit is contained in:
2025-03-13 20:06:58 +08:00
parent b8ea4e7097
commit 72c040cf19
4 changed files with 381 additions and 14 deletions

View File

@@ -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