201 lines
5.7 KiB
TypeScript
201 lines
5.7 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
||
import clickhouse from '@/lib/clickhouse';
|
||
import type { ApiResponse } from '@/lib/types';
|
||
|
||
interface UtmData {
|
||
utm_value: string;
|
||
clicks: number;
|
||
visitors: number;
|
||
avg_time_spent: number;
|
||
bounces: number;
|
||
conversions: number;
|
||
}
|
||
|
||
// 辅助函数,将日期格式化为标准格式
|
||
function formatDateTime(dateString: string): string {
|
||
const date = new Date(dateString);
|
||
return date.toISOString().split('.')[0];
|
||
}
|
||
|
||
export async function GET(request: NextRequest) {
|
||
try {
|
||
const searchParams = request.nextUrl.searchParams;
|
||
|
||
// 获取过滤参数
|
||
const startTime = searchParams.get('startTime');
|
||
const endTime = searchParams.get('endTime');
|
||
const linkId = searchParams.get('linkId');
|
||
const subpath = searchParams.get('subpath');
|
||
|
||
// 获取团队、项目和标签筛选参数
|
||
const teamIds = searchParams.getAll('teamId');
|
||
const projectIds = searchParams.getAll('projectId');
|
||
const tagIds = searchParams.getAll('tagId');
|
||
const tagNames = searchParams.getAll('tagName');
|
||
|
||
// 获取UTM类型参数
|
||
const utmType = searchParams.get('utmType') || 'source';
|
||
|
||
// 添加调试日志
|
||
console.log('UTM API received parameters:', {
|
||
startTime,
|
||
endTime,
|
||
linkId,
|
||
subpath,
|
||
teamIds,
|
||
projectIds,
|
||
tagIds,
|
||
tagNames,
|
||
utmType,
|
||
url: request.url
|
||
});
|
||
|
||
// 构建WHERE子句
|
||
let whereClause = '';
|
||
const conditions = [];
|
||
|
||
if (startTime) {
|
||
conditions.push(`event_time >= toDateTime('${formatDateTime(startTime)}')`);
|
||
}
|
||
|
||
if (endTime) {
|
||
conditions.push(`event_time <= toDateTime('${formatDateTime(endTime)}')`);
|
||
}
|
||
|
||
if (linkId) {
|
||
conditions.push(`link_id = '${linkId}'`);
|
||
}
|
||
|
||
// 添加子路径筛选 - 使用更精确的匹配方式
|
||
if (subpath && subpath.trim() !== '') {
|
||
console.log('====== UTM API SUBPATH DEBUG ======');
|
||
console.log('Raw subpath param:', subpath);
|
||
|
||
// 清理并准备subpath值
|
||
let cleanSubpath = subpath.trim();
|
||
// 移除开头的斜杠以便匹配
|
||
if (cleanSubpath.startsWith('/')) {
|
||
cleanSubpath = cleanSubpath.substring(1);
|
||
}
|
||
// 移除结尾的斜杠以便匹配
|
||
if (cleanSubpath.endsWith('/')) {
|
||
cleanSubpath = cleanSubpath.substring(0, cleanSubpath.length - 1);
|
||
}
|
||
|
||
console.log('Cleaned subpath:', cleanSubpath);
|
||
|
||
// 使用正则表达式匹配URL中的第二个路径部分
|
||
// 示例: 在 "https://abc.com/slug/subpath/" 中匹配 "subpath"
|
||
const condition = `match(JSONExtractString(event_attributes, 'full_url'), '/[^/]+/${cleanSubpath}(/|\\\\?|$)')`;
|
||
|
||
console.log('Final SQL condition:', condition);
|
||
console.log('==================================');
|
||
|
||
conditions.push(condition);
|
||
}
|
||
|
||
// 添加团队筛选
|
||
if (teamIds && teamIds.length > 0) {
|
||
// 如果只有一个团队ID
|
||
if (teamIds.length === 1) {
|
||
conditions.push(`team_id = '${teamIds[0]}'`);
|
||
} else {
|
||
// 多个团队ID
|
||
conditions.push(`team_id IN ('${teamIds.join("','")}')`);
|
||
}
|
||
}
|
||
|
||
// 添加项目筛选
|
||
if (projectIds && projectIds.length > 0) {
|
||
// 如果只有一个项目ID
|
||
if (projectIds.length === 1) {
|
||
conditions.push(`project_id = '${projectIds[0]}'`);
|
||
} else {
|
||
// 多个项目ID
|
||
conditions.push(`project_id IN ('${projectIds.join("','")}')`);
|
||
}
|
||
}
|
||
|
||
// 添加标签筛选
|
||
if ((tagIds && tagIds.length > 0) || (tagNames && tagNames.length > 0)) {
|
||
// 优先使用tagNames,如果有的话
|
||
const tagsToUse = tagNames.length > 0 ? tagNames : tagIds;
|
||
|
||
// 使用与buildFilter函数相同的处理方式
|
||
const tagConditions = tagsToUse.map(tag =>
|
||
`link_tags LIKE '%${tag}%'`
|
||
);
|
||
conditions.push(`(${tagConditions.join(' OR ')})`);
|
||
}
|
||
|
||
if (conditions.length > 0) {
|
||
whereClause = `WHERE ${conditions.join(' AND ')}`;
|
||
}
|
||
|
||
// 确定要分组的UTM字段
|
||
let utmField;
|
||
switch (utmType) {
|
||
case 'source':
|
||
utmField = 'utm_source';
|
||
break;
|
||
case 'medium':
|
||
utmField = 'utm_medium';
|
||
break;
|
||
case 'campaign':
|
||
utmField = 'utm_campaign';
|
||
break;
|
||
case 'term':
|
||
utmField = 'utm_term';
|
||
break;
|
||
case 'content':
|
||
utmField = 'utm_content';
|
||
break;
|
||
default:
|
||
utmField = 'utm_source';
|
||
}
|
||
|
||
// 构建SQL查询
|
||
const query = `
|
||
SELECT
|
||
${utmField} AS utm_value,
|
||
COUNT(*) AS clicks,
|
||
uniqExact(visitor_id) AS visitors,
|
||
round(AVG(time_spent_sec), 2) AS avg_time_spent,
|
||
countIf(is_bounce = 1) AS bounces,
|
||
countIf(conversion_type IN ('visit', 'stay', 'interact', 'signup', 'subscription', 'purchase')) AS conversions
|
||
FROM shorturl_analytics.events
|
||
${whereClause}
|
||
${whereClause ? 'AND' : 'WHERE'} ${utmField} != ''
|
||
GROUP BY utm_value
|
||
ORDER BY clicks DESC
|
||
LIMIT 100
|
||
`;
|
||
|
||
// 执行查询
|
||
const result = await clickhouse.query({
|
||
query,
|
||
format: 'JSONEachRow',
|
||
});
|
||
|
||
// 获取查询结果
|
||
const rows = await result.json();
|
||
const data = rows as UtmData[];
|
||
|
||
// 返回数据
|
||
const response: ApiResponse<UtmData[]> = {
|
||
success: true,
|
||
data
|
||
};
|
||
|
||
return NextResponse.json(response);
|
||
} catch (error) {
|
||
console.error('Error fetching UTM data:', error);
|
||
|
||
const response: ApiResponse<null> = {
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||
};
|
||
|
||
return NextResponse.json(response, { status: 500 });
|
||
}
|
||
} |