events apis

This commit is contained in:
2025-03-25 17:26:04 +08:00
parent 1e9e5928d7
commit 92d82b18a0
33 changed files with 859 additions and 2315 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +1,109 @@
import { createClient } from '@clickhouse/client';
import type { EventsQueryParams } from './types';
// Create configuration object using the URL approach
const config = {
// ClickHouse 客户端配置
const clickhouse = createClient({
url: process.env.CLICKHOUSE_URL || 'http://localhost:8123',
username: process.env.CLICKHOUSE_USER || 'default',
password: process.env.CLICKHOUSE_PASSWORD || '',
database: process.env.CLICKHOUSE_DATABASE || 'limq'
};
username: process.env.CLICKHOUSE_USER || 'admin',
password: process.env.CLICKHOUSE_PASSWORD || 'your_secure_password',
database: process.env.CLICKHOUSE_DB || 'shorturl_analytics'
});
// Create ClickHouse client with proper URL format
export const clickhouse = createClient(config);
// 构建日期过滤条件
function buildDateFilter(startTime?: string, endTime?: string): string {
const filters = [];
if (startTime) {
filters.push(`event_time >= parseDateTimeBestEffort('${startTime}')`);
}
if (endTime) {
filters.push(`event_time <= parseDateTimeBestEffort('${endTime}')`);
}
return filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
}
// 构建通用过滤条件
export function buildFilter(params: Partial<EventsQueryParams>): string {
const filters = [];
// 时间范围过滤
if (params.startTime || params.endTime) {
const dateFilter = buildDateFilter(params.startTime, params.endTime).replace('WHERE ', '');
if (dateFilter) {
filters.push(dateFilter);
}
}
// 事件类型过滤
if (params.eventType) {
filters.push(`event_type = '${params.eventType}'`);
}
// 链接ID过滤
if (params.linkId) {
filters.push(`link_id = '${params.linkId}'`);
}
// 链接短码过滤
if (params.linkSlug) {
filters.push(`link_slug = '${params.linkSlug}'`);
}
// 用户ID过滤
if (params.userId) {
filters.push(`user_id = '${params.userId}'`);
}
// 团队ID过滤
if (params.teamId) {
filters.push(`team_id = '${params.teamId}'`);
}
// 项目ID过滤
if (params.projectId) {
filters.push(`project_id = '${params.projectId}'`);
}
return filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
}
/**
* Execute ClickHouse query and return results
*/
// 构建分页
export function buildPagination(page?: number, pageSize?: number): string {
const limit = pageSize || 20;
const offset = ((page || 1) - 1) * limit;
return `LIMIT ${limit} OFFSET ${offset}`;
}
// 构建排序
export function buildOrderBy(sortBy?: string, sortOrder?: 'asc' | 'desc'): string {
if (!sortBy) {
return 'ORDER BY event_time DESC';
}
return `ORDER BY ${sortBy} ${sortOrder || 'desc'}`;
}
// 执行查询并处理错误
export async function executeQuery<T>(query: string): Promise<T[]> {
try {
const result = await clickhouse.query({
const resultSet = await clickhouse.query({
query,
format: 'JSONEachRow',
format: 'JSONEachRow'
});
const data = await result.json();
return data as T[];
const rows = await resultSet.json<T>();
return Array.isArray(rows) ? rows : [rows];
} catch (error) {
console.error('ClickHouse query error:', error);
throw error;
}
}
/**
* Execute ClickHouse query and return a single result
*/
// 执行查询并返回单个结果
export async function executeQuerySingle<T>(query: string): Promise<T | null> {
const results = await executeQuery<T>(query);
return results.length > 0 ? results[0] : null;
}
}
export default clickhouse;

171
lib/types.ts Normal file
View File

@@ -0,0 +1,171 @@
// 事件类型
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'
}
// 设备类型
export enum DeviceType {
MOBILE = 'mobile',
TABLET = 'tablet',
DESKTOP = 'desktop',
OTHER = 'other'
}
// API 响应基础接口
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
meta?: {
total?: number;
page?: number;
pageSize?: number;
};
}
// 事件查询参数
export interface EventsQueryParams {
startTime?: string; // ISO 格式时间
endTime?: string; // ISO 格式时间
eventType?: EventType;
linkId?: string;
linkSlug?: string;
userId?: string;
teamId?: string;
projectId?: string;
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
// 事件基础信息
export interface Event {
event_id: string;
event_time: string;
event_type: EventType;
event_attributes: Record<string, any>;
// 链接信息
link_id: string;
link_slug: string;
link_label: string;
link_title: string;
link_original_url: string;
link_attributes: Record<string, any>;
link_created_at: string;
link_expires_at: string | null;
link_tags: string[];
// 用户信息
user_id: string;
user_name: string;
user_email: string;
user_attributes: Record<string, any>;
// 团队信息
team_id: string;
team_name: string;
team_attributes: Record<string, any>;
// 项目信息
project_id: string;
project_name: string;
project_attributes: Record<string, any>;
// 访问者信息
visitor_id: string;
session_id: string;
ip_address: string;
country: string;
city: string;
device_type: DeviceType;
browser: string;
os: string;
user_agent: string;
// 来源信息
referrer: string;
utm_source: string;
utm_medium: string;
utm_campaign: string;
// 交互信息
time_spent_sec: number;
is_bounce: boolean;
is_qr_scan: boolean;
conversion_type: ConversionType;
conversion_value: number;
}
// 事件概览数据
export interface EventsSummary {
totalEvents: number;
uniqueVisitors: number;
totalConversions: number;
averageTimeSpent: number;
deviceTypes: {
mobile: number;
desktop: number;
tablet: number;
other: number;
};
browsers: Array<{
name: string;
count: number;
percentage: number;
}>;
operatingSystems: Array<{
name: string;
count: number;
percentage: number;
}>;
}
// 时间序列数据
export interface TimeSeriesData {
timestamp: string;
events: number;
visitors: number;
conversions: number;
}
// 地理位置数据
export interface GeoData {
location: string;
visits: number;
visitors: number;
percentage: number;
}
// 设备分析数据
export interface DeviceAnalytics {
deviceTypes: Array<{
type: DeviceType;
count: number;
percentage: number;
}>;
browsers: Array<{
name: string;
count: number;
percentage: number;
}>;
operatingSystems: Array<{
name: string;
count: number;
percentage: number;
}>;
}