links search
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { executeQuery, executeQuerySingle, buildFilter, buildPagination, buildOrderBy } from './clickhouse';
|
||||
import type { Event, EventsSummary, TimeSeriesData, GeoData, DeviceAnalytics, DeviceType } from './types';
|
||||
import type { Event, EventsSummary, TimeSeriesData, GeoData, DeviceAnalytics, DeviceType, EventsQueryParams } from './types';
|
||||
|
||||
// 时间粒度枚举
|
||||
export enum TimeGranularity {
|
||||
@@ -10,20 +10,7 @@ export enum TimeGranularity {
|
||||
}
|
||||
|
||||
// 获取事件列表
|
||||
export async function getEvents(params: {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
eventType?: string;
|
||||
linkId?: string;
|
||||
linkSlug?: string;
|
||||
userId?: string;
|
||||
teamId?: string;
|
||||
projectId?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}): Promise<{ events: Event[]; total: number }> {
|
||||
export async function getEvents(params: Partial<EventsQueryParams>): Promise<{ events: Event[]; total: number }> {
|
||||
const filter = buildFilter(params);
|
||||
const pagination = buildPagination(params.page, params.pageSize);
|
||||
const orderBy = buildOrderBy(params.sortBy, params.sortOrder);
|
||||
@@ -40,7 +27,48 @@ export async function getEvents(params: {
|
||||
|
||||
// 获取事件列表
|
||||
const query = `
|
||||
SELECT *
|
||||
SELECT
|
||||
event_id,
|
||||
event_time,
|
||||
event_type,
|
||||
event_attributes,
|
||||
link_id,
|
||||
link_slug,
|
||||
link_label,
|
||||
link_title,
|
||||
link_original_url,
|
||||
link_attributes,
|
||||
link_created_at,
|
||||
link_expires_at,
|
||||
link_tags,
|
||||
user_id,
|
||||
user_name,
|
||||
user_email,
|
||||
user_attributes,
|
||||
team_id,
|
||||
team_name,
|
||||
team_attributes,
|
||||
project_id,
|
||||
project_name,
|
||||
project_attributes,
|
||||
visitor_id,
|
||||
session_id,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
user_agent,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
FROM events
|
||||
${filter}
|
||||
${orderBy}
|
||||
@@ -49,7 +77,19 @@ export async function getEvents(params: {
|
||||
|
||||
const events = await executeQuery<Event>(query);
|
||||
|
||||
return { events, total };
|
||||
// 处理 JSON 字符串字段
|
||||
return {
|
||||
events: events.map(event => ({
|
||||
...event,
|
||||
event_attributes: JSON.parse(event.event_attributes as unknown as string),
|
||||
link_attributes: JSON.parse(event.link_attributes as unknown as string),
|
||||
link_tags: JSON.parse(event.link_tags as unknown as string),
|
||||
user_attributes: JSON.parse(event.user_attributes as unknown as string),
|
||||
team_attributes: JSON.parse(event.team_attributes as unknown as string),
|
||||
project_attributes: JSON.parse(event.project_attributes as unknown as string)
|
||||
})),
|
||||
total
|
||||
};
|
||||
}
|
||||
|
||||
// 获取事件概览
|
||||
|
||||
@@ -24,6 +24,11 @@ function buildDateFilter(startTime?: string, endTime?: string): string {
|
||||
return filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
|
||||
}
|
||||
|
||||
// 字符串转义函数
|
||||
function escapeString(str: string): string {
|
||||
return str.replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
// 构建通用过滤条件
|
||||
export function buildFilter(params: Partial<EventsQueryParams>): string {
|
||||
const filters = [];
|
||||
@@ -38,32 +43,45 @@ export function buildFilter(params: Partial<EventsQueryParams>): string {
|
||||
|
||||
// 事件类型过滤
|
||||
if (params.eventType) {
|
||||
filters.push(`event_type = '${params.eventType}'`);
|
||||
filters.push(`event_type = '${escapeString(params.eventType)}'`);
|
||||
}
|
||||
|
||||
// 链接ID过滤
|
||||
if (params.linkId) {
|
||||
filters.push(`link_id = '${params.linkId}'`);
|
||||
filters.push(`link_id = '${escapeString(params.linkId)}'`);
|
||||
}
|
||||
|
||||
// 链接短码过滤
|
||||
if (params.linkSlug) {
|
||||
filters.push(`link_slug = '${params.linkSlug}'`);
|
||||
filters.push(`link_slug = '${escapeString(params.linkSlug)}'`);
|
||||
}
|
||||
|
||||
// 用户ID过滤
|
||||
if (params.userId) {
|
||||
filters.push(`user_id = '${params.userId}'`);
|
||||
filters.push(`user_id = '${escapeString(params.userId)}'`);
|
||||
}
|
||||
|
||||
// 团队ID过滤
|
||||
if (params.teamId) {
|
||||
filters.push(`team_id = '${params.teamId}'`);
|
||||
filters.push(`team_id = '${escapeString(params.teamId)}'`);
|
||||
}
|
||||
|
||||
// 项目ID过滤
|
||||
if (params.projectId) {
|
||||
filters.push(`project_id = '${params.projectId}'`);
|
||||
filters.push(`project_id = '${escapeString(params.projectId)}'`);
|
||||
}
|
||||
|
||||
// 标签筛选
|
||||
if (params.tags && params.tags.length > 0) {
|
||||
const tagConditions = params.tags.map(tag =>
|
||||
`JSONHas(JSONExtractArrayRaw(link_tags), JSON_QUOTE('${escapeString(tag)}'))`
|
||||
);
|
||||
filters.push(`(${tagConditions.join(' OR ')})`);
|
||||
}
|
||||
|
||||
// Slug 模糊搜索
|
||||
if (params.searchSlug) {
|
||||
filters.push(`positionCaseInsensitive(link_slug, '${escapeString(params.searchSlug)}') > 0`);
|
||||
}
|
||||
|
||||
return filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
|
||||
|
||||
26
lib/types.ts
26
lib/types.ts
@@ -33,6 +33,14 @@ export interface ApiResponse<T> {
|
||||
total?: number;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
filters?: {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
tags?: string[];
|
||||
teamId?: string;
|
||||
projectId?: string;
|
||||
searchSlug?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,18 +54,26 @@ export interface EventsQueryParams {
|
||||
userId?: string;
|
||||
teamId?: string;
|
||||
projectId?: string;
|
||||
tags?: string[]; // 标签筛选
|
||||
searchSlug?: string; // slug搜索关键词
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// 属性值类型
|
||||
export type AttributeValue = string | number | boolean | null | AttributeValue[] | { [key: string]: AttributeValue };
|
||||
|
||||
// 属性记录类型
|
||||
export type AttributesRecord = Record<string, AttributeValue>;
|
||||
|
||||
// 事件基础信息
|
||||
export interface Event {
|
||||
event_id: string;
|
||||
event_time: string;
|
||||
event_type: EventType;
|
||||
event_attributes: Record<string, any>;
|
||||
event_attributes: AttributesRecord;
|
||||
|
||||
// 链接信息
|
||||
link_id: string;
|
||||
@@ -65,7 +81,7 @@ export interface Event {
|
||||
link_label: string;
|
||||
link_title: string;
|
||||
link_original_url: string;
|
||||
link_attributes: Record<string, any>;
|
||||
link_attributes: AttributesRecord;
|
||||
link_created_at: string;
|
||||
link_expires_at: string | null;
|
||||
link_tags: string[];
|
||||
@@ -74,17 +90,17 @@ export interface Event {
|
||||
user_id: string;
|
||||
user_name: string;
|
||||
user_email: string;
|
||||
user_attributes: Record<string, any>;
|
||||
user_attributes: AttributesRecord;
|
||||
|
||||
// 团队信息
|
||||
team_id: string;
|
||||
team_name: string;
|
||||
team_attributes: Record<string, any>;
|
||||
team_attributes: AttributesRecord;
|
||||
|
||||
// 项目信息
|
||||
project_id: string;
|
||||
project_name: string;
|
||||
project_attributes: Record<string, any>;
|
||||
project_attributes: AttributesRecord;
|
||||
|
||||
// 访问者信息
|
||||
visitor_id: string;
|
||||
|
||||
Reference in New Issue
Block a user