links search

This commit is contained in:
2025-03-31 16:46:33 +08:00
parent d75110d6b8
commit e7b3b735e0
11 changed files with 689 additions and 303 deletions

View File

@@ -2,23 +2,12 @@
import { useEffect } from 'react';
import SwaggerUI from 'swagger-ui-react';
import 'swagger-ui-react/swagger-ui.css';
export default function SwaggerPage() {
useEffect(() => {
// 设置页面标题
document.title = 'API Documentation - ShortURL Analytics';
// 动态添加Swagger UI CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = 'https://unpkg.com/swagger-ui-dist@5.20.1/swagger-ui.css';
document.head.appendChild(link);
// 清理函数
return () => {
document.head.removeChild(link);
};
}, []);
// Swagger配置
@@ -222,27 +211,93 @@ export default function SwaggerPage() {
get: {
tags: ['events'],
summary: 'Get events',
description: 'Retrieve events within a specified time range with pagination support',
description: 'Retrieve events with pagination and filtering support. If startTime and endTime are not provided, will return the most recent events.',
parameters: [
{
name: 'startTime',
in: 'query',
required: true,
required: false,
schema: {
type: 'string',
format: 'date-time',
},
description: 'Start time for events query (ISO 8601 format)',
description: 'Start time for events query (ISO 8601 format). If not provided, no lower time bound will be applied.',
},
{
name: 'endTime',
in: 'query',
required: true,
required: false,
schema: {
type: 'string',
format: 'date-time',
},
description: 'End time for events query (ISO 8601 format)',
description: 'End time for events query (ISO 8601 format). If not provided, no upper time bound will be applied.',
},
{
name: 'eventType',
in: 'query',
schema: {
type: 'string',
enum: ['click', 'redirect', 'conversion', 'error']
},
description: 'Filter events by type',
},
{
name: 'linkId',
in: 'query',
schema: {
type: 'string'
},
description: 'Filter events by link ID',
},
{
name: 'linkSlug',
in: 'query',
schema: {
type: 'string'
},
description: 'Filter events by link slug',
},
{
name: 'userId',
in: 'query',
schema: {
type: 'string'
},
description: 'Filter events by user ID',
},
{
name: 'teamId',
in: 'query',
schema: {
type: 'string'
},
description: 'Filter events by team ID',
},
{
name: 'projectId',
in: 'query',
schema: {
type: 'string'
},
description: 'Filter events by project ID',
},
{
name: 'tags',
in: 'query',
schema: {
type: 'string'
},
description: 'Filter events by tags (comma-separated list)',
example: 'promo,vip,summer'
},
{
name: 'searchSlug',
in: 'query',
schema: {
type: 'string'
},
description: 'Search events by partial link slug match',
},
{
name: 'page',
@@ -259,12 +314,31 @@ export default function SwaggerPage() {
in: 'query',
schema: {
type: 'integer',
default: 50,
default: 20,
minimum: 1,
maximum: 100,
},
description: 'Number of items per page',
},
{
name: 'sortBy',
in: 'query',
schema: {
type: 'string',
enum: ['event_time', 'event_type', 'link_slug', 'conversion_value']
},
description: 'Field to sort by',
},
{
name: 'sortOrder',
in: 'query',
schema: {
type: 'string',
enum: ['asc', 'desc'],
default: 'desc'
},
description: 'Sort order',
}
],
responses: {
'200': {
@@ -274,31 +348,116 @@ export default function SwaggerPage() {
schema: {
type: 'object',
properties: {
success: {
type: 'boolean',
example: true
},
data: {
type: 'array',
items: {
$ref: '#/components/schemas/Event',
$ref: '#/components/schemas/Event'
}
},
meta: {
type: 'object',
properties: {
total: {
type: 'number',
description: 'Total number of events matching the filters'
},
pagination: {
$ref: '#/components/schemas/Pagination',
page: {
type: 'number',
description: 'Current page number'
},
pageSize: {
type: 'number',
description: 'Number of items per page'
},
filters: {
type: 'object',
properties: {
tags: {
type: 'array',
items: {
type: 'string'
},
description: 'Applied tag filters'
},
teamId: {
type: 'string',
description: 'Applied team filter'
},
projectId: {
type: 'string',
description: 'Applied project filter'
},
searchSlug: {
type: 'string',
description: 'Applied slug search term'
}
}
}
}
}
}
},
example: {
success: true,
data: [
{
event_id: '123e4567-e89b-12d3-a456-426614174000',
event_time: '2024-03-31T10:00:00.000Z',
event_type: 'click',
link_slug: 'summer-promo',
link_tags: ['promo', 'summer'],
team_id: 'team1',
project_id: 'proj1'
}
],
meta: {
total: 150,
page: 1,
pageSize: 20,
filters: {
tags: ['promo', 'summer'],
teamId: 'team1',
projectId: 'proj1',
searchSlug: 'summer'
}
}
}
}
}
},
'400': {
description: 'Bad request',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error',
},
},
$ref: '#/components/schemas/Error'
},
example: {
success: false,
error: 'Invalid tags format'
}
}
}
},
'500': {
description: 'Server error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
example: {
success: false,
error: 'Internal server error'
}
}
}
}
}
},
},
'/events/summary': {
@@ -526,6 +685,74 @@ export default function SwaggerPage() {
},
},
},
'/events/tags': {
get: {
tags: ['events'],
summary: '获取标签列表',
description: '获取所有事件中的唯一标签列表。如果提供了 teamId则只返回该团队的标签。',
parameters: [
{
name: 'teamId',
in: 'query',
required: false,
schema: {
type: 'string',
},
description: '团队ID可选。如果提供则只返回该团队的标签。',
},
],
responses: {
'200': {
description: '成功获取标签列表',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: {
type: 'boolean',
example: true,
},
data: {
type: 'array',
items: {
type: 'object',
properties: {
tag_name: {
type: 'string',
example: 'marketing',
},
},
},
},
},
},
},
},
},
'500': {
description: '服务器错误',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: {
type: 'boolean',
example: false,
},
error: {
type: 'string',
example: 'Failed to fetch tags',
},
},
},
},
},
},
},
},
},
},
components: {
schemas: {

View File

@@ -1,47 +1,72 @@
import { NextRequest, NextResponse } from 'next/server';
import type { ApiResponse, EventsQueryParams, EventType } from '@/lib/types';
import {
getEvents,
getEventsSummary,
getTimeSeriesData,
getGeoAnalytics,
getDeviceAnalytics
} from '@/lib/analytics';
import { getEvents } from '@/lib/analytics';
// 获取事件列表
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const params: EventsQueryParams = {
startTime: searchParams.get('startTime') || undefined,
endTime: searchParams.get('endTime') || undefined,
eventType: searchParams.get('eventType') as EventType || undefined,
linkId: searchParams.get('linkId') || undefined,
linkSlug: searchParams.get('linkSlug') || undefined,
userId: searchParams.get('userId') || undefined,
teamId: searchParams.get('teamId') || undefined,
projectId: searchParams.get('projectId') || undefined,
// 构建查询参数,所有参数都是可选的
const params: Partial<EventsQueryParams> = {
// 时间范围参数现在是可选的
...(searchParams.has('startTime') && { startTime: searchParams.get('startTime')! }),
...(searchParams.has('endTime') && { endTime: searchParams.get('endTime')! }),
// 其他过滤参数
...(searchParams.has('eventType') && { eventType: searchParams.get('eventType') as EventType }),
...(searchParams.has('linkId') && { linkId: searchParams.get('linkId')! }),
...(searchParams.has('linkSlug') && { linkSlug: searchParams.get('linkSlug')! }),
...(searchParams.has('userId') && { userId: searchParams.get('userId')! }),
...(searchParams.has('teamId') && { teamId: searchParams.get('teamId')! }),
...(searchParams.has('projectId') && { projectId: searchParams.get('projectId')! }),
...(searchParams.has('tags') && {
tags: searchParams.get('tags')!.split(',').filter(Boolean)
}),
...(searchParams.has('searchSlug') && { searchSlug: searchParams.get('searchSlug')! }),
// 分页和排序参数,设置默认值
page: searchParams.has('page') ? parseInt(searchParams.get('page')!, 10) : 1,
pageSize: searchParams.has('pageSize') ? parseInt(searchParams.get('pageSize')!, 10) : 20,
sortBy: searchParams.get('sortBy') || undefined,
sortOrder: (searchParams.get('sortOrder') as 'asc' | 'desc') || undefined
...(searchParams.has('sortBy') && { sortBy: searchParams.get('sortBy')! }),
...(searchParams.has('sortOrder') && {
sortOrder: searchParams.get('sortOrder') as 'asc' | 'desc'
})
};
// 验证 tags 参数
if (params.tags?.some(tag => !tag.trim())) {
return NextResponse.json({
success: false,
error: 'Invalid tags format'
}, { status: 400 });
}
// 获取事件数据
const { events, total } = await getEvents(params);
// 构建响应
const response: ApiResponse<typeof events> = {
success: true,
data: events,
meta: {
total,
page: params.page,
pageSize: params.pageSize
page: params.page || 1,
pageSize: params.pageSize || 20,
filters: {
startTime: params.startTime,
endTime: params.endTime,
tags: params.tags,
teamId: params.teamId,
projectId: params.projectId,
searchSlug: params.searchSlug
}
}
};
return NextResponse.json(response);
} catch (error) {
console.error('Error in GET /events:', error);
const response: ApiResponse<null> = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'

View File

@@ -0,0 +1,99 @@
/**
* @swagger
* /api/events/tags:
* get:
* summary: 获取标签列表
* description: 获取所有事件中的唯一标签列表。如果提供了 teamId则只返回该团队的标签。
* tags:
* - Events
* parameters:
* - in: query
* name: teamId
* schema:
* type: string
* description: 团队ID可选。如果提供则只返回该团队的标签。
* responses:
* 200:
* description: 成功获取标签列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: array
* items:
* type: object
* properties:
* tag_name:
* type: string
* example: "marketing"
* 500:
* description: 服务器错误
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: false
* error:
* type: string
* example: "Failed to fetch tags"
*/
import { NextRequest, NextResponse } from 'next/server';
import clickhouse from '@/lib/clickhouse';
export async function GET(request: NextRequest) {
try {
// 从 URL 获取查询参数
const searchParams = request.nextUrl.searchParams;
const teamId = searchParams.get('teamId');
// 构建基础查询
let query = `
WITH
JSONExtractArrayRaw(link_tags) as tags_array,
arrayJoin(tags_array) as tag
SELECT DISTINCT
JSONExtractString(tag) as tag_name
FROM events
WHERE link_tags != '[]'
`;
const queryParams: Record<string, string> = {};
// 如果提供了 teamId添加团队过滤条件
if (teamId) {
query += ` AND team_id = {id:String}`;
queryParams.id = teamId;
}
// 添加排序
query += ` ORDER BY tag_name ASC`;
const result = await clickhouse.query({
query,
query_params: queryParams
});
const data = await result.json();
return NextResponse.json({
success: true,
data: data
});
} catch (error) {
console.error('Error fetching tags:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch tags' },
{ status: 500 }
);
}
}

View File

@@ -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
};
}
// 获取事件概览

View File

@@ -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 ')}` : '';

View File

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

15
next.config.js Normal file
View File

@@ -0,0 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// 启用 webpack 配置
webpack: (config) => {
// 添加 CSS 处理规则
config.module.rules.push({
test: /\.css$/,
use: ['style-loader', 'css-loader'],
});
return config;
},
};
module.exports = nextConfig;

View File

@@ -45,8 +45,10 @@
"@types/react-dom": "^19",
"@types/swagger-ui-react": "^4.18.3",
"css-loader": "^7.1.2",
"dotenv": "^16.4.7",
"eslint": "^9",
"eslint-config-next": "15.2.3",
"pg": "^8.14.0",
"style-loader": "^4.0.0",
"tailwindcss": "^4",
"typescript": "^5"

113
pnpm-lock.yaml generated
View File

@@ -69,12 +69,18 @@ importers:
css-loader:
specifier: ^7.1.2
version: 7.1.2(webpack@5.98.0)
dotenv:
specifier: ^16.4.7
version: 16.4.7
eslint:
specifier: ^9
version: 9.22.0(jiti@2.4.2)
eslint-config-next:
specifier: 15.2.3
version: 15.2.3(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
pg:
specifier: ^8.14.0
version: 8.14.0
style-loader:
specifier: ^4.0.0
version: 4.0.0(webpack@5.98.0)
@@ -1173,6 +1179,10 @@ packages:
dompurify@3.2.4:
resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==}
dotenv@16.4.7:
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
engines: {node: '>=12'}
drange@1.1.1:
resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==}
engines: {node: '>=4'}
@@ -2024,6 +2034,40 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
pg-cloudflare@1.1.1:
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
pg-connection-string@2.7.0:
resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
pg-pool@3.8.0:
resolution: {integrity: sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==}
peerDependencies:
pg: '>=8.0'
pg-protocol@1.8.0:
resolution: {integrity: sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==}
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
pg@8.14.0:
resolution: {integrity: sha512-nXbVpyoaXVmdqlKEzToFf37qzyeeh7mbiXsnoWvstSqohj88yaa/I/Rq/HEVn2QPSZEuLIJa/jSpRDyzjEx4FQ==}
engines: {node: '>= 8.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
pgpass@1.0.5:
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2078,6 +2122,22 @@ packages:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
postgres-bytea@1.0.0:
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
engines: {node: '>=0.10.0'}
postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -2384,6 +2444,10 @@ packages:
space-separated-tokens@1.1.5:
resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
@@ -4013,6 +4077,8 @@ snapshots:
optionalDependencies:
'@types/trusted-types': 2.0.7
dotenv@16.4.7: {}
drange@1.1.1: {}
dunder-proto@1.0.1:
@@ -4983,6 +5049,41 @@ snapshots:
path-parse@1.0.7: {}
pg-cloudflare@1.1.1:
optional: true
pg-connection-string@2.7.0: {}
pg-int8@1.0.1: {}
pg-pool@3.8.0(pg@8.14.0):
dependencies:
pg: 8.14.0
pg-protocol@1.8.0: {}
pg-types@2.2.0:
dependencies:
pg-int8: 1.0.1
postgres-array: 2.0.0
postgres-bytea: 1.0.0
postgres-date: 1.0.7
postgres-interval: 1.2.0
pg@8.14.0:
dependencies:
pg-connection-string: 2.7.0
pg-pool: 3.8.0(pg@8.14.0)
pg-protocol: 1.8.0
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.1.1
pgpass@1.0.5:
dependencies:
split2: 4.2.0
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -5031,6 +5132,16 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postgres-array@2.0.0: {}
postgres-bytea@1.0.0: {}
postgres-date@1.0.7: {}
postgres-interval@1.2.0:
dependencies:
xtend: 4.0.2
prelude-ls@1.2.1: {}
prismjs@1.27.0: {}
@@ -5396,6 +5507,8 @@ snapshots:
space-separated-tokens@1.1.5: {}
split2@4.2.0: {}
sprintf-js@1.0.3: {}
stable-hash@0.0.5: {}

View File

@@ -1,225 +0,0 @@
获取所有表...
数据库 limq 中找到以下表:
- .inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb
- .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc
- .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1
- .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0
- .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024
- .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea
- link_daily_stats
- link_events
- link_hourly_patterns
- links
- platform_distribution
- project_daily_stats
- projects
- qr_scans
- qrcode_daily_stats
- qrcodes
- sessions
- team_daily_stats
- team_members
- teams
所有ClickHouse表:
.inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb, .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc, .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1, .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0, .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024, .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea, link_daily_stats, link_events, link_hourly_patterns, links, platform_distribution, project_daily_stats, projects, qr_scans, qrcode_daily_stats, qrcodes, sessions, team_daily_stats, team_members, teams
获取表 .inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb 的结构...
获取表 .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc 的结构...
获取表 .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1 的结构...
获取表 .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0 的结构...
获取表 .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024 的结构...
获取表 .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea 的结构...
获取表 link_daily_stats 的结构...
表 link_daily_stats 的列:
- date (Date, 无默认值)
- link_id (String, 无默认值)
- total_clicks (UInt64, 无默认值)
- unique_visitors (UInt64, 无默认值)
- unique_sessions (UInt64, 无默认值)
- total_time_spent (UInt64, 无默认值)
- avg_time_spent (Float64, 无默认值)
- bounce_count (UInt64, 无默认值)
- conversion_count (UInt64, 无默认值)
- unique_referrers (UInt64, 无默认值)
- mobile_count (UInt64, 无默认值)
- tablet_count (UInt64, 无默认值)
- desktop_count (UInt64, 无默认值)
- qr_scan_count (UInt64, 无默认值)
- total_conversion_value (Float64, 无默认值)
获取表 link_events 的结构...
表 link_events 的列:
- event_id (UUID, 默认值: generateUUIDv4())
- event_time (DateTime64(3), 默认值: now64())
- date (Date, 默认值: toDate(event_time))
- link_id (String, 无默认值)
- channel_id (String, 无默认值)
- visitor_id (String, 无默认值)
- session_id (String, 无默认值)
- event_type (Enum8('click' = 1, 'redirect' = 2, 'conversion' = 3, 'error' = 4), 无默认值)
- ip_address (String, 无默认值)
- country (String, 无默认值)
- city (String, 无默认值)
- referrer (String, 无默认值)
- utm_source (String, 无默认值)
- utm_medium (String, 无默认值)
- utm_campaign (String, 无默认值)
- user_agent (String, 无默认值)
- device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值)
- browser (String, 无默认值)
- os (String, 无默认值)
- time_spent_sec (UInt32, 默认值: 0)
- is_bounce (Bool, 默认值: true)
- is_qr_scan (Bool, 默认值: false)
- qr_code_id (String, 默认值: '')
- conversion_type (Enum8('visit' = 1, 'stay' = 2, 'interact' = 3, 'signup' = 4, 'subscription' = 5, 'purchase' = 6), 默认值: 'visit')
- conversion_value (Float64, 默认值: 0)
- custom_data (String, 默认值: '{}')
获取表 link_hourly_patterns 的结构...
表 link_hourly_patterns 的列:
- date (Date, 无默认值)
- hour (UInt8, 无默认值)
- link_id (String, 无默认值)
- visits (UInt64, 无默认值)
- unique_visitors (UInt64, 无默认值)
获取表 links 的结构...
表 links 的列:
- link_id (String, 无默认值)
- original_url (String, 无默认值)
- created_at (DateTime64(3), 无默认值)
- created_by (String, 无默认值)
- title (String, 无默认值)
- description (String, 无默认值)
- tags (Array(String), 无默认值)
- is_active (Bool, 默认值: true)
- expires_at (Nullable(DateTime), 无默认值)
- team_id (String, 默认值: '')
- project_id (String, 默认值: '')
获取表 platform_distribution 的结构...
表 platform_distribution 的列:
- date (Date, 无默认值)
- utm_source (String, 无默认值)
- device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值)
- visits (UInt64, 无默认值)
- unique_visitors (UInt64, 无默认值)
获取表 project_daily_stats 的结构...
表 project_daily_stats 的列:
- date (Date, 无默认值)
- project_id (String, 无默认值)
- total_clicks (UInt64, 无默认值)
- unique_visitors (UInt64, 无默认值)
- conversion_count (UInt64, 无默认值)
- links_used (UInt64, 无默认值)
- qr_scan_count (UInt64, 无默认值)
获取表 projects 的结构...
表 projects 的列:
- project_id (String, 无默认值)
- team_id (String, 无默认值)
- name (String, 无默认值)
- created_at (DateTime, 无默认值)
- created_by (String, 无默认值)
- description (String, 默认值: '')
- is_archived (Bool, 默认值: false)
- links_count (UInt32, 默认值: 0)
- total_clicks (UInt64, 默认值: 0)
- last_updated (DateTime, 默认值: now())
获取表 qr_scans 的结构...
表 qr_scans 的列:
- scan_id (UUID, 默认值: generateUUIDv4())
- qr_code_id (String, 无默认值)
- link_id (String, 无默认值)
- scan_time (DateTime64(3), 无默认值)
- visitor_id (String, 无默认值)
- location (String, 无默认值)
- device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值)
- led_to_conversion (Bool, 默认值: false)
获取表 qrcode_daily_stats 的结构...
表 qrcode_daily_stats 的列:
- date (Date, 无默认值)
- qr_code_id (String, 无默认值)
- total_scans (UInt64, 无默认值)
- unique_scanners (UInt64, 无默认值)
- conversions (UInt64, 无默认值)
- mobile_scans (UInt64, 无默认值)
- tablet_scans (UInt64, 无默认值)
- desktop_scans (UInt64, 无默认值)
- unique_locations (UInt64, 无默认值)
获取表 qrcodes 的结构...
表 qrcodes 的列:
- qr_code_id (String, 无默认值)
- link_id (String, 无默认值)
- team_id (String, 无默认值)
- project_id (String, 默认值: '')
- name (String, 无默认值)
- description (String, 默认值: '')
- created_at (DateTime, 无默认值)
- created_by (String, 无默认值)
- updated_at (DateTime, 默认值: now())
- qr_type (Enum8('standard' = 1, 'custom' = 2, 'dynamic' = 3), 默认值: 'standard')
- image_url (String, 默认值: '')
- design_config (String, 默认值: '{}')
- is_active (Bool, 默认值: true)
- total_scans (UInt64, 默认值: 0)
- unique_scanners (UInt32, 默认值: 0)
获取表 sessions 的结构...
表 sessions 的列:
- session_id (String, 无默认值)
- visitor_id (String, 无默认值)
- link_id (String, 无默认值)
- started_at (DateTime64(3), 无默认值)
- last_activity (DateTime64(3), 无默认值)
- ended_at (Nullable(DateTime64(3)), 无默认值)
- duration_sec (UInt32, 默认值: 0)
- session_pages (UInt8, 默认值: 1)
- is_completed (Bool, 默认值: false)
获取表 team_daily_stats 的结构...
表 team_daily_stats 的列:
- date (Date, 无默认值)
- team_id (String, 无默认值)
- total_clicks (UInt64, 无默认值)
- unique_visitors (UInt64, 无默认值)
- conversion_count (UInt64, 无默认值)
- links_used (UInt64, 无默认值)
- qr_scan_count (UInt64, 无默认值)
获取表 team_members 的结构...
表 team_members 的列:
- team_id (String, 无默认值)
- user_id (String, 无默认值)
- role (Enum8('owner' = 1, 'admin' = 2, 'editor' = 3, 'viewer' = 4), 无默认值)
- joined_at (DateTime, 默认值: now())
- invited_by (String, 无默认值)
- is_active (Bool, 默认值: true)
- last_active (DateTime, 默认值: now())
获取表 teams 的结构...
表 teams 的列:
- team_id (String, 无默认值)
- name (String, 无默认值)
- created_at (DateTime, 无默认值)
- created_by (String, 无默认值)
- description (String, 默认值: '')
- avatar_url (String, 默认值: '')
- is_active (Bool, 默认值: true)
- plan_type (Enum8('free' = 1, 'pro' = 2, 'enterprise' = 3), 无默认值)
- members_count (UInt32, 默认值: 1)
ClickHouse数据库结构检查完成

View File

@@ -0,0 +1,56 @@
获取所有表...
数据库 shorturl_analytics 中找到以下表:
- events
所有ClickHouse表:
events
获取表 events 的结构...
表 events 的列:
- event_id (String, 无默认值)
- event_time (DateTime64(3), 无默认值)
- event_type (String, 无默认值)
- event_attributes (String, 默认值: '{}')
- link_id (String, 无默认值)
- link_slug (String, 无默认值)
- link_label (String, 无默认值)
- link_title (String, 无默认值)
- link_original_url (String, 无默认值)
- link_attributes (String, 默认值: '{}')
- link_created_at (DateTime64(3), 无默认值)
- link_expires_at (Nullable(DateTime64(3)), 无默认值)
- link_tags (String, 默认值: '[]')
- user_id (String, 无默认值)
- user_name (String, 无默认值)
- user_email (String, 无默认值)
- user_attributes (String, 默认值: '{}')
- team_id (String, 无默认值)
- team_name (String, 无默认值)
- team_attributes (String, 默认值: '{}')
- project_id (String, 无默认值)
- project_name (String, 无默认值)
- project_attributes (String, 默认值: '{}')
- qr_code_id (String, 无默认值)
- qr_code_name (String, 无默认值)
- qr_code_attributes (String, 默认值: '{}')
- visitor_id (String, 无默认值)
- session_id (String, 无默认值)
- ip_address (String, 无默认值)
- country (String, 无默认值)
- city (String, 无默认值)
- device_type (String, 无默认值)
- browser (String, 无默认值)
- os (String, 无默认值)
- user_agent (String, 无默认值)
- referrer (String, 无默认值)
- utm_source (String, 无默认值)
- utm_medium (String, 无默认值)
- utm_campaign (String, 无默认值)
- time_spent_sec (UInt32, 默认值: 0)
- is_bounce (Bool, 默认值: true)
- is_qr_scan (Bool, 默认值: false)
- conversion_type (String, 无默认值)
- conversion_value (Float64, 默认值: 0)
ClickHouse数据库结构检查完成