Files
shorturl-analytics/lib/analytics.ts
2025-03-21 12:08:37 +08:00

1266 lines
35 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid';
import { executeQuery, executeQuerySingle } from './clickhouse';
// 时间粒度枚举
export enum TimeGranularity {
HOUR = 'hour',
DAY = 'day',
WEEK = 'week',
MONTH = 'month',
}
// 事件类型枚举
export enum EventType {
CLICK = 'click',
REDIRECT = 'redirect',
CONVERSION = 'conversion',
ERROR = 'error',
}
// 转化类型枚举
export enum ConversionType {
VISIT = 'visit',
STAY = 'stay',
INTERACT = 'interact',
SIGNUP = 'signup',
SUBSCRIPTION = 'subscription',
PURCHASE = 'purchase',
}
// 构建日期过滤条件
function buildDateFilter(startDate?: string, endDate?: string): string {
let dateFilter = '';
if (startDate && endDate) {
dateFilter = ` AND date >= '${startDate}' AND date <= '${endDate}'`;
} else if (startDate) {
dateFilter = ` AND date >= '${startDate}'`;
} else if (endDate) {
dateFilter = ` AND date <= '${endDate}'`;
}
return dateFilter;
}
/**
* 获取链接概览数据
*/
export async function getLinkOverview(
linkId: string,
startDate?: string,
endDate?: string,
) {
try {
const dateFilter = buildDateFilter(startDate, endDate);
const query = `
SELECT
count() as total_visits,
uniq(visitor_id) as unique_visitors,
avg(time_spent_sec) as average_time_spent,
countIf(time_spent_sec < 10) as bounce_count,
countIf(event_type = 'conversion') as conversion_count,
uniq(referrer) as unique_referrers,
countIf(device_type = 'mobile') as mobile_count,
countIf(device_type = 'tablet') as tablet_count,
countIf(device_type = 'desktop') as desktop_count,
countIf(device_type = 'other') as other_count,
countIf(is_qr_scan = true) as qr_scan_count,
sum(conversion_value) as total_conversion_value
FROM link_events
WHERE link_id = '${linkId}'
${dateFilter}
`;
const result = await executeQuerySingle<{
total_visits: number;
unique_visitors: number;
average_time_spent: number;
bounce_count: number;
conversion_count: number;
unique_referrers: number;
mobile_count: number;
tablet_count: number;
desktop_count: number;
other_count: number;
qr_scan_count: number;
total_conversion_value: number;
}>(query);
if (!result) {
return {
totalVisits: 0,
uniqueVisitors: 0,
averageTimeSpent: 0,
bounceCount: 0,
conversionCount: 0,
uniqueReferrers: 0,
deviceTypes: {
mobile: 0,
tablet: 0,
desktop: 0,
other: 0,
},
qrScanCount: 0,
totalConversionValue: 0,
};
}
// 将设备类型计数转换为字典
const deviceTypes = {
mobile: Number(result.mobile_count),
tablet: Number(result.tablet_count),
desktop: Number(result.desktop_count),
other: Number(result.other_count),
};
return {
totalVisits: Number(result.total_visits),
uniqueVisitors: Number(result.unique_visitors),
averageTimeSpent: Number(result.average_time_spent),
bounceCount: Number(result.bounce_count),
conversionCount: Number(result.conversion_count),
uniqueReferrers: Number(result.unique_referrers),
deviceTypes,
qrScanCount: Number(result.qr_scan_count),
totalConversionValue: Number(result.total_conversion_value),
};
} catch (error) {
console.error('获取链接概览数据失败', error);
throw error;
}
}
/**
* 获取转化漏斗数据
*/
export async function getConversionFunnel(
linkId: string,
startDate?: string,
endDate?: string,
) {
try {
const dateFilter = buildDateFilter(startDate, endDate);
const query = `
SELECT
countIf(conversion_type = 'visit') as visit_count,
countIf(conversion_type = 'stay') as stay_count,
countIf(conversion_type = 'interact') as interact_count,
countIf(conversion_type = 'signup') as signup_count,
countIf(conversion_type = 'subscription') as subscription_count,
countIf(conversion_type = 'purchase') as purchase_count
FROM link_events
WHERE link_id = '${linkId}' AND event_type = 'conversion'
${dateFilter}
`;
const result = await executeQuerySingle<{
visit_count: number;
stay_count: number;
interact_count: number;
signup_count: number;
subscription_count: number;
purchase_count: number;
}>(query);
if (!result) {
return {
steps: [
{ name: 'Visit', value: 0, percent: 0 },
{ name: 'Stay', value: 0, percent: 0 },
{ name: 'Interact', value: 0, percent: 0 },
{ name: 'Signup', value: 0, percent: 0 },
{ name: 'Subscription', value: 0, percent: 0 },
{ name: 'Purchase', value: 0, percent: 0 },
],
totalConversions: 0,
conversionRate: 0,
};
}
// 计算总转化数
const totalConversions =
Number(result.visit_count) +
Number(result.stay_count) +
Number(result.interact_count) +
Number(result.signup_count) +
Number(result.subscription_count) +
Number(result.purchase_count);
// 计算转化率
const conversionRate = totalConversions > 0 ?
Number(result.purchase_count) / Number(result.visit_count) * 100 : 0;
// 构建步骤数据
const steps = [
{
name: 'Visit',
value: Number(result.visit_count),
percent: 100,
},
{
name: 'Stay',
value: Number(result.stay_count),
percent: result.visit_count > 0
? (Number(result.stay_count) / Number(result.visit_count)) * 100
: 0,
},
{
name: 'Interact',
value: Number(result.interact_count),
percent: result.visit_count > 0
? (Number(result.interact_count) / Number(result.visit_count)) * 100
: 0,
},
{
name: 'Signup',
value: Number(result.signup_count),
percent: result.visit_count > 0
? (Number(result.signup_count) / Number(result.visit_count)) * 100
: 0,
},
{
name: 'Subscription',
value: Number(result.subscription_count),
percent: result.visit_count > 0
? (Number(result.subscription_count) / Number(result.visit_count)) * 100
: 0,
},
{
name: 'Purchase',
value: Number(result.purchase_count),
percent: result.visit_count > 0
? (Number(result.purchase_count) / Number(result.visit_count)) * 100
: 0,
},
];
return {
steps,
totalConversions,
conversionRate,
};
} catch (error) {
console.error('获取转化漏斗数据失败', error);
throw error;
}
}
/**
* 获取访问趋势数据
*/
export async function getVisitTrends(
linkId: string,
startDate?: string,
endDate?: string,
granularity: TimeGranularity = TimeGranularity.DAY,
) {
try {
const dateFilter = buildDateFilter(startDate, endDate);
const queryString = `
SELECT
toStartOfInterval(event_time, INTERVAL 1 ${granularity}) as timestamp,
count() as visits,
uniq(visitor_id) as unique_visitors
FROM link_events
WHERE link_id = '${linkId}'
${dateFilter}
GROUP BY timestamp
ORDER BY timestamp
`;
const results = await executeQuery<{
timestamp: string;
visits: number;
unique_visitors: number;
}>(queryString);
// 计算总计
const totals = {
visits: results.reduce((sum, item) => sum + Number(item.visits), 0),
uniqueVisitors: results.reduce((sum, item) => sum + Number(item.unique_visitors), 0),
};
// 格式化时间戳
const trends = results.map(item => ({
timestamp: formatTimestamp(item.timestamp, granularity),
visits: Number(item.visits),
uniqueVisitors: Number(item.unique_visitors),
}));
return {
trends,
totals,
};
} catch (error) {
console.error('获取访问趋势数据失败', error);
throw error;
}
}
/**
* 追踪事件
*/
export async function trackEvent(eventData: {
linkId: string;
eventType: EventType;
visitorId?: string;
sessionId?: string;
referrer?: string;
userAgent?: string;
ipAddress?: string;
timeSpent?: number;
conversionType?: ConversionType;
conversionValue?: number;
customData?: Record<string, unknown>;
isQrScan?: boolean;
qrCodeId?: string;
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
}) {
try {
// 检查必要字段
if (!eventData.linkId) {
throw new Error('Missing required field: linkId');
}
// 生成缺失的ID和时间戳
const eventId = uuidv4();
const timestamp = new Date().toISOString();
const visitorId = eventData.visitorId || uuidv4();
const sessionId = eventData.sessionId || uuidv4();
// 设置默认值
const isQrScan = !!eventData.isQrScan;
const qrCodeId = eventData.qrCodeId || '';
const conversionValue = eventData.conversionValue || 0;
const conversionType = eventData.conversionType || ConversionType.VISIT;
const timeSpentSec = eventData.timeSpent || 0;
// 准备插入数据
const insertQuery = `
INSERT INTO link_events (
event_id, event_time, link_id, visitor_id, session_id,
event_type, ip_address, referrer, utm_source, utm_medium,
utm_campaign, user_agent, time_spent_sec, is_qr_scan,
qr_code_id, conversion_type, conversion_value, custom_data
) VALUES (
'${eventId}', '${timestamp}', '${eventData.linkId}', '${visitorId}', '${sessionId}',
'${eventData.eventType}', '${eventData.ipAddress || ''}', '${eventData.referrer || ''}',
'${eventData.utmSource || ''}', '${eventData.utmMedium || ''}',
'${eventData.utmCampaign || ''}', '${eventData.userAgent || ''}', ${timeSpentSec}, ${isQrScan},
'${qrCodeId}', '${conversionType}', ${conversionValue}, '${JSON.stringify(eventData.customData || {})}'
)
`;
await executeQuery(insertQuery);
return {
success: true,
eventId,
timestamp,
};
} catch (error) {
console.error('事件追踪失败', error);
throw error;
}
}
/**
* 格式化时间戳
*/
function formatTimestamp(timestamp: string, granularity: TimeGranularity): string {
const date = new Date(timestamp);
switch (granularity) {
case TimeGranularity.HOUR:
return `${date.toISOString().substring(0, 13)}:00`;
case TimeGranularity.DAY:
return date.toISOString().substring(0, 10);
case TimeGranularity.WEEK: {
const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
const pastDaysOfYear = (date.getTime() - firstDayOfYear.getTime()) / 86400000;
const weekNum = Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
return `${date.getFullYear()}-W${weekNum.toString().padStart(2, '0')}`;
}
case TimeGranularity.MONTH:
return date.toISOString().substring(0, 7);
default:
return date.toISOString().substring(0, 10);
}
}
/**
* 获取链接表现数据
*/
export async function getLinkPerformance(
linkId: string,
startDate?: string,
endDate?: string,
) {
try {
const dateFilter = buildDateFilter(startDate, endDate);
const query = `
SELECT
count() as total_clicks,
uniq(visitor_id) as unique_visitors,
avg(time_spent_sec) as average_time_spent,
countIf(time_spent_sec < 10) as bounce_count,
uniq(referrer) as unique_referrers,
countIf(event_type = 'conversion' AND conversion_type = 'purchase') as conversion_count,
count(DISTINCT DATE(event_time)) as active_days,
max(event_time) as last_click_time,
countIf(device_type = 'mobile') as mobile_clicks,
countIf(device_type = 'desktop') as desktop_clicks
FROM link_events
WHERE link_id = '${linkId}'
${dateFilter}
`;
const result = await executeQuerySingle<{
total_clicks: number;
unique_visitors: number;
average_time_spent: number;
bounce_count: number;
unique_referrers: number;
conversion_count: number;
active_days: number;
last_click_time: string;
mobile_clicks: number;
desktop_clicks: number;
}>(query);
if (!result) {
return {
totalClicks: 0,
uniqueVisitors: 0,
averageTimeSpent: 0,
bounceRate: 0,
uniqueReferrers: 0,
conversionRate: 0,
activeDays: 0,
lastClickTime: null,
deviceDistribution: {
mobile: 0,
desktop: 0,
},
};
}
// 计算跳出率
const bounceRate = result.total_clicks > 0 ?
(result.bounce_count / result.total_clicks) * 100 : 0;
// 计算转化率
const conversionRate = result.unique_visitors > 0 ?
(result.conversion_count / result.unique_visitors) * 100 : 0;
return {
totalClicks: Number(result.total_clicks),
uniqueVisitors: Number(result.unique_visitors),
averageTimeSpent: Number(result.average_time_spent),
bounceRate: Number(bounceRate.toFixed(2)),
uniqueReferrers: Number(result.unique_referrers),
conversionRate: Number(conversionRate.toFixed(2)),
activeDays: Number(result.active_days),
lastClickTime: result.last_click_time,
deviceDistribution: {
mobile: Number(result.mobile_clicks),
desktop: Number(result.desktop_clicks),
},
};
} catch (error) {
console.error('获取链接表现数据失败', error);
throw error;
}
}
/**
* 获取平台分布数据
*/
export async function getPlatformDistribution(
startDate?: string,
endDate?: string,
linkId?: string,
) {
try {
const dateFilter = buildDateFilter(startDate, endDate);
// 构建链接过滤条件
let linkFilter = '';
if (linkId) {
linkFilter = ` AND link_id = '${linkId}'`;
}
const query = `
SELECT
os,
browser,
count() as visit_count
FROM link_events
WHERE 1=1
${dateFilter}
${linkFilter}
GROUP BY os, browser
ORDER BY visit_count DESC
`;
const results = await executeQuery<{
os: string;
browser: string;
visit_count: number;
}>(query);
// 平台统计
const platforms: { [key: string]: number } = {};
// 浏览器统计
const browsers: { [key: string]: number } = {};
// 计算总访问量
const totalVisits = results.reduce((sum, item) => sum + Number(item.visit_count), 0);
// 处理平台和浏览器数据
for (const item of results) {
const platform = item.os || 'unknown';
const browser = item.browser || 'unknown';
const count = Number(item.visit_count);
// 累加平台数据
platforms[platform] = (platforms[platform] || 0) + count;
// 累加浏览器数据
browsers[browser] = (browsers[browser] || 0) + count;
}
// 计算百分比并格式化结果
const platformData = Object.entries(platforms).map(([name, count]) => ({
name,
count: Number(count),
percent: totalVisits > 0 ? Number(((count / totalVisits) * 100).toFixed(1)) : 0,
}));
const browserData = Object.entries(browsers).map(([name, count]) => ({
name,
count: Number(count),
percent: totalVisits > 0 ? Number(((count / totalVisits) * 100).toFixed(1)) : 0,
}));
// 按访问量排序
platformData.sort((a, b) => b.count - a.count);
browserData.sort((a, b) => b.count - a.count);
return {
totalVisits: totalVisits,
platforms: platformData,
browsers: browserData,
};
} catch (error) {
console.error('获取平台分布数据失败', error);
throw error;
}
}
/**
* 获取链接状态分布数据
*/
export async function getLinkStatusDistribution(
startDate?: string,
endDate?: string,
projectId?: string,
) {
try {
const dateFilter = buildDateFilter(startDate, endDate);
// 构建项目过滤条件
let projectFilter = '';
if (projectId) {
projectFilter = ` AND project_id = '${projectId}'`;
}
const query = `
SELECT
is_active,
count() as link_count
FROM links
WHERE 1=1
${dateFilter}
${projectFilter}
GROUP BY is_active
`;
const results = await executeQuery<{
is_active: boolean;
link_count: number;
}>(query);
// 初始化数据
let activeCount = 0;
let inactiveCount = 0;
// 处理查询结果
for (const item of results) {
if (item.is_active) {
activeCount = Number(item.link_count);
} else {
inactiveCount = Number(item.link_count);
}
}
// 计算总数
const totalLinks = activeCount + inactiveCount;
// 计算百分比
const activePercent = totalLinks > 0 ? (activeCount / totalLinks) * 100 : 0;
const inactivePercent = totalLinks > 0 ? (inactiveCount / totalLinks) * 100 : 0;
// 构建状态分布数据
const statusDistribution = [
{
status: 'active',
count: activeCount,
percent: Number(activePercent.toFixed(1)),
},
{
status: 'inactive',
count: inactiveCount,
percent: Number(inactivePercent.toFixed(1)),
},
];
return {
totalLinks,
statusDistribution,
};
} catch (error) {
console.error('获取链接状态分布数据失败', error);
throw error;
}
}
/**
* 获取设备分析详情
*/
export async function getDeviceAnalysis(
startDate?: string,
endDate?: string,
linkId?: string,
) {
try {
const dateFilter = buildDateFilter(startDate, endDate);
// 构建链接过滤条件
let linkFilter = '';
if (linkId) {
linkFilter = ` AND link_id = '${linkId}'`;
}
const query = `
SELECT
device_type,
count() as visit_count
FROM link_events
WHERE 1=1
${dateFilter}
${linkFilter}
GROUP BY device_type
ORDER BY visit_count DESC
`;
const results = await executeQuery<{
device_type: string;
visit_count: number;
}>(query);
// 设备类型统计
const deviceTypes: { [key: string]: number } = {};
// 计算总访问量
const totalVisits = results.reduce((sum, item) => sum + Number(item.visit_count), 0);
// 处理设备数据
for (const item of results) {
const type = item.device_type || 'unknown';
const count = Number(item.visit_count);
// 累加类型数据
deviceTypes[type] = (deviceTypes[type] || 0) + count;
}
// 计算百分比并格式化类型结果
const typeData = Object.entries(deviceTypes).map(([name, count]) => ({
name,
count: Number(count),
percent: totalVisits > 0 ? Number(((count / totalVisits) * 100).toFixed(1)) : 0,
}));
// 排序类型数据
typeData.sort((a, b) => b.count - a.count);
return {
totalVisits,
deviceTypes: typeData,
deviceBrands: [], // 返回空数组,因为数据库中没有设备品牌信息
deviceModels: [], // 返回空数组,因为数据库中没有设备型号信息
};
} catch (error) {
console.error('获取设备分析详情失败', error);
throw error;
}
}
/**
* 获取热门链接数据
*/
export async function getPopularLinks(
startDate?: string,
endDate?: string,
projectId?: string,
sortBy: 'visits' | 'uniqueVisitors' | 'conversionRate' = 'visits',
limit: number = 10,
) {
try {
const dateFilter = buildDateFilter(startDate, endDate);
// 构建项目过滤条件
let projectFilter = '';
if (projectId) {
projectFilter = ` AND l.project_id = '${projectId}'`;
}
// 根据排序字段构建ORDER BY子句
let orderBy = '';
switch (sortBy) {
case 'visits':
orderBy = 'ORDER BY total_visits DESC';
break;
case 'uniqueVisitors':
orderBy = 'ORDER BY unique_visitors DESC';
break;
case 'conversionRate':
orderBy = 'ORDER BY conversion_rate DESC';
break;
default:
orderBy = 'ORDER BY total_visits DESC';
}
const query = `
SELECT
l.link_id,
l.original_url,
l.title,
l.is_active,
count() as total_visits,
uniq(e.visitor_id) as unique_visitors,
countIf(e.event_type = 'conversion' AND e.conversion_type = 'purchase') as conversion_count,
countIf(e.time_spent_sec < 10) as bounce_count
FROM links l
JOIN link_events e ON l.link_id = e.link_id
WHERE 1=1
${dateFilter}
${projectFilter}
GROUP BY l.link_id, l.original_url, l.title, l.is_active
${orderBy}
LIMIT ${limit}
`;
const results = await executeQuery<{
link_id: string;
original_url: string;
title: string;
is_active: boolean;
total_visits: number;
unique_visitors: number;
conversion_count: number;
bounce_count: number;
}>(query);
// 处理查询结果
const links = results.map(link => {
const totalVisits = Number(link.total_visits);
const uniqueVisitors = Number(link.unique_visitors);
const conversionCount = Number(link.conversion_count);
const bounceCount = Number(link.bounce_count);
// 计算转化率
const conversionRate = uniqueVisitors > 0
? (conversionCount / uniqueVisitors) * 100
: 0;
// 计算跳出率
const bounceRate = totalVisits > 0
? (bounceCount / totalVisits) * 100
: 0;
return {
id: link.link_id,
url: link.original_url,
title: link.title || '无标题',
isActive: link.is_active,
totalVisits,
uniqueVisitors,
conversionCount,
conversionRate: Number(conversionRate.toFixed(2)),
bounceCount,
bounceRate: Number(bounceRate.toFixed(2)),
};
});
return {
links,
totalCount: links.length,
};
} catch (error) {
console.error('获取热门链接数据失败', error);
throw error;
}
}
/**
* 获取热门引荐来源数据
*/
export async function getPopularReferrers(
startDate?: string,
endDate?: string,
linkId?: string,
type: 'domain' | 'full' = 'domain',
limit: number = 10,
) {
try {
const dateFilter = buildDateFilter(startDate, endDate);
// 构建链接过滤条件
let linkFilter = '';
if (linkId) {
linkFilter = ` AND link_id = '${linkId}'`;
}
// 决定是按域名还是完整URL分组
const groupByField = type === 'domain'
? 'domain(referrer)'
: 'referrer';
const query = `
SELECT
${groupByField} as source,
count() as visit_count,
uniq(visitor_id) as unique_visitors,
countIf(event_type = 'conversion' AND conversion_type = 'purchase') as conversion_count,
avg(time_spent_sec) as average_time_spent
FROM link_events
WHERE referrer != ''
${dateFilter}
${linkFilter}
GROUP BY ${groupByField}
ORDER BY visit_count DESC
LIMIT ${limit}
`;
const results = await executeQuery<{
source: string;
visit_count: number;
unique_visitors: number;
conversion_count: number;
average_time_spent: number;
}>(query);
// 计算总访问量
const totalVisits = results.reduce((sum, item) => sum + Number(item.visit_count), 0);
// 处理查询结果
const referrers = results.map(referrer => {
const visitCount = Number(referrer.visit_count);
const uniqueVisitors = Number(referrer.unique_visitors);
const conversionCount = Number(referrer.conversion_count);
// 计算转化率
const conversionRate = uniqueVisitors > 0
? (conversionCount / uniqueVisitors) * 100
: 0;
// 计算百分比
const percent = totalVisits > 0
? (visitCount / totalVisits) * 100
: 0;
return {
source: referrer.source || '(direct)',
visitCount,
uniqueVisitors,
conversionCount,
conversionRate: Number(conversionRate.toFixed(2)),
averageTimeSpent: Number(referrer.average_time_spent),
percent: Number(percent.toFixed(1)),
};
});
return {
referrers,
totalVisits,
};
} catch (error) {
console.error('获取热门引荐来源数据失败', error);
throw error;
}
}
/**
* 获取QR码分析数据
*/
export async function getQrCodeAnalysis(
startDate?: string,
endDate?: string,
linkId?: string,
qrCodeId?: string,
) {
try {
const dateFilter = buildDateFilter(startDate, endDate);
// 构建过滤条件
let filters = ' AND is_qr_scan = true';
if (linkId) {
filters += ` AND link_id = '${linkId}'`;
}
if (qrCodeId) {
filters += ` AND qr_code_id = '${qrCodeId}'`;
}
// 查询QR码扫描基本指标
const basicQuery = `
SELECT
count() as total_scans,
uniq(visitor_id) as unique_scanners,
countIf(event_type = 'conversion' AND conversion_type = 'purchase') as conversion_count,
avg(time_spent_sec) as average_time_spent
FROM link_events
WHERE 1=1
${dateFilter}
${filters}
`;
const basicResult = await executeQuerySingle<{
total_scans: number;
unique_scanners: number;
conversion_count: number;
average_time_spent: number;
}>(basicQuery);
// 查询QR码扫描的位置分布
const locationQuery = `
SELECT
city,
country,
count() as scan_count
FROM link_events
WHERE 1=1
${dateFilter}
${filters}
GROUP BY city, country
ORDER BY scan_count DESC
LIMIT 10
`;
const locationResults = await executeQuery<{
city: string;
country: string;
scan_count: number;
}>(locationQuery);
// 查询QR码扫描设备分布
const deviceQuery = `
SELECT
device_type,
count() as scan_count
FROM link_events
WHERE 1=1
${dateFilter}
${filters}
GROUP BY device_type
ORDER BY scan_count DESC
`;
const deviceResults = await executeQuery<{
device_type: string;
scan_count: number;
}>(deviceQuery);
// 查询QR码扫描时间分布
const timeQuery = `
SELECT
toHour(event_time) as hour,
count() as scan_count
FROM link_events
WHERE 1=1
${dateFilter}
${filters}
GROUP BY hour
ORDER BY hour
`;
const timeResults = await executeQuery<{
hour: number;
scan_count: number;
}>(timeQuery);
// 计算基本指标
const totalScans = Number(basicResult?.total_scans || 0);
const uniqueScanners = Number(basicResult?.unique_scanners || 0);
const conversionCount = Number(basicResult?.conversion_count || 0);
// 计算转化率
const conversionRate = uniqueScanners > 0
? (conversionCount / uniqueScanners) * 100
: 0;
// 处理位置数据
const locations = locationResults.map(loc => ({
city: loc.city || 'Unknown',
country: loc.country || 'Unknown',
scanCount: Number(loc.scan_count),
percent: totalScans > 0 ? Number(((Number(loc.scan_count) / totalScans) * 100).toFixed(1)) : 0,
}));
// 处理设备类型数据
const deviceCounts: { [key: string]: number } = {};
for (const device of deviceResults) {
const type = device.device_type || 'unknown';
deviceCounts[type] = Number(device.scan_count);
}
// 计算设备分布的百分比
const totalDeviceCount = Object.values(deviceCounts).reduce((sum, count) => sum + count, 0);
const deviceDistribution = Object.entries(deviceCounts).map(([type, count]) => ({
type,
count,
percent: totalDeviceCount > 0 ? Number(((count / totalDeviceCount) * 100).toFixed(1)) : 0,
}));
// 排序设备分布
deviceDistribution.sort((a, b) => b.count - a.count);
// 处理时间分布数据
const hourlyDistribution = Array.from({ length: 24 }, (_, i) => ({
hour: i,
scanCount: 0,
percent: 0
}));
for (const time of timeResults) {
const hour = Number(time.hour);
const count = Number(time.scan_count);
if (hour >= 0 && hour < 24) {
hourlyDistribution[hour].scanCount = count;
hourlyDistribution[hour].percent = totalScans > 0 ? (count / totalScans) * 100 : 0;
}
}
return {
overview: {
totalScans,
uniqueScanners,
conversionCount,
conversionRate: Number(conversionRate.toFixed(2)),
averageTimeSpent: Number(basicResult?.average_time_spent || 0),
},
locations,
deviceDistribution,
hourlyDistribution,
};
} catch (error) {
console.error('获取QR码分析数据失败', error);
throw error;
}
}
/**
* 获取概览卡片数据
*/
export async function getOverviewCards(
startDate?: string,
endDate?: string,
projectId?: string,
) {
try {
const dateFilter = buildDateFilter(startDate, endDate);
// 构建项目过滤条件
let projectFilter = '';
if (projectId) {
projectFilter = ` AND l.project_id = '${projectId}'`;
}
// 获取当前周期的数据
const currentQuery = `
SELECT
count(DISTINCT e.link_id) as total_links,
count() as total_visits,
uniq(e.visitor_id) as unique_visitors,
countIf(e.event_type = 'conversion' AND e.conversion_type = 'purchase') as total_conversions,
sum(e.conversion_value) as total_revenue,
countIf(l.is_active = true) as active_links
FROM link_events e
JOIN links l ON e.link_id = l.link_id
WHERE 1=1
${dateFilter}
${projectFilter}
`;
const currentResult = await executeQuerySingle<{
total_links: number;
total_visits: number;
unique_visitors: number;
total_conversions: number;
total_revenue: number;
active_links: number;
}>(currentQuery);
// 计算前一时期的日期范围
let previousStartDate = '';
let previousEndDate = '';
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
const duration = end.getTime() - start.getTime();
const prevStart = new Date(start.getTime() - duration);
const prevEnd = new Date(end.getTime() - duration);
previousStartDate = prevStart.toISOString().split('T')[0];
previousEndDate = prevEnd.toISOString().split('T')[0];
}
// 获取前一时期的数据
let previousResult = null;
if (previousStartDate && previousEndDate) {
const previousDateFilter = buildDateFilter(previousStartDate, previousEndDate);
const previousQuery = `
SELECT
count(DISTINCT e.link_id) as total_links,
count() as total_visits,
uniq(e.visitor_id) as unique_visitors,
countIf(e.event_type = 'conversion' AND e.conversion_type = 'purchase') as total_conversions,
sum(e.conversion_value) as total_revenue,
countIf(l.is_active = true) as active_links
FROM link_events e
JOIN links l ON e.link_id = l.link_id
WHERE 1=1
${previousDateFilter}
${projectFilter}
`;
previousResult = await executeQuerySingle<{
total_links: number;
total_visits: number;
unique_visitors: number;
total_conversions: number;
total_revenue: number;
active_links: number;
}>(previousQuery);
}
// 计算同比变化
function calculateChange(current: number, previous: number): number {
if (previous === 0) return 0;
return Number(((current - previous) / previous * 100).toFixed(1));
}
// 获取当前值,并设置默认值
const currentTotalLinks = Number(currentResult?.total_links || 0);
const currentTotalVisits = Number(currentResult?.total_visits || 0);
const currentUniqueVisitors = Number(currentResult?.unique_visitors || 0);
const currentTotalConversions = Number(currentResult?.total_conversions || 0);
const currentTotalRevenue = Number(currentResult?.total_revenue || 0);
const currentActiveLinks = Number(currentResult?.active_links || 0);
// 获取前一时期的值,并设置默认值
const previousTotalLinks = Number(previousResult?.total_links || 0);
const previousTotalVisits = Number(previousResult?.total_visits || 0);
const previousUniqueVisitors = Number(previousResult?.unique_visitors || 0);
const previousTotalConversions = Number(previousResult?.total_conversions || 0);
const previousTotalRevenue = Number(previousResult?.total_revenue || 0);
const previousActiveLinks = Number(previousResult?.active_links || 0);
// 计算转化率
const currentConversionRate = currentUniqueVisitors > 0
? (currentTotalConversions / currentUniqueVisitors) * 100
: 0;
const previousConversionRate = previousUniqueVisitors > 0
? (previousTotalConversions / previousUniqueVisitors) * 100
: 0;
// 计算活跃链接百分比
const currentActivePercentage = currentTotalLinks > 0
? (currentActiveLinks / currentTotalLinks) * 100
: 0;
const previousActivePercentage = previousTotalLinks > 0
? (previousActiveLinks / previousTotalLinks) * 100
: 0;
// 构建结果
return {
cards: [
{
title: '总访问量',
currentValue: currentTotalVisits,
previousValue: previousTotalVisits,
change: calculateChange(currentTotalVisits, previousTotalVisits),
format: 'number',
},
{
title: '独立访客',
currentValue: currentUniqueVisitors,
previousValue: previousUniqueVisitors,
change: calculateChange(currentUniqueVisitors, previousUniqueVisitors),
format: 'number',
},
{
title: '转化次数',
currentValue: currentTotalConversions,
previousValue: previousTotalConversions,
change: calculateChange(currentTotalConversions, previousTotalConversions),
format: 'number',
},
{
title: '总收入',
currentValue: currentTotalRevenue,
previousValue: previousTotalRevenue,
change: calculateChange(currentTotalRevenue, previousTotalRevenue),
format: 'currency',
},
{
title: '转化率',
currentValue: Number(currentConversionRate.toFixed(1)),
previousValue: Number(previousConversionRate.toFixed(1)),
change: calculateChange(currentConversionRate, previousConversionRate),
format: 'percent',
},
{
title: '活跃链接率',
currentValue: Number(currentActivePercentage.toFixed(1)),
previousValue: Number(previousActivePercentage.toFixed(1)),
change: calculateChange(currentActivePercentage, previousActivePercentage),
format: 'percent',
},
],
timeRange: {
current: {
startDate: startDate || '',
endDate: endDate || '',
},
previous: {
startDate: previousStartDate,
endDate: previousEndDate,
},
},
};
} catch (error) {
console.error('获取概览卡片数据失败', error);
throw error;
}
}