From e7b3b735e0b9a40a8818fa60e9447420b3020648 Mon Sep 17 00:00:00 2001 From: William Tso Date: Mon, 31 Mar 2025 16:46:33 +0800 Subject: [PATCH] links search --- app/(swagger)/swagger/page.tsx | 287 ++++++++++++++++-- app/api/events/route.ts | 65 ++-- app/api/events/tags/route.ts | 99 ++++++ lib/analytics.ts | 74 +++-- lib/clickhouse.ts | 30 +- lib/types.ts | 26 +- next.config.js | 15 + package.json | 2 + pnpm-lock.yaml | 113 +++++++ ...khouse-schema-2025-03-20T13-57-59-013Z.log | 225 -------------- ...khouse-schema-2025-03-31T06-07-06-639Z.log | 56 ++++ 11 files changed, 689 insertions(+), 303 deletions(-) create mode 100644 app/api/events/tags/route.ts create mode 100644 next.config.js delete mode 100644 scripts/db/db-reports/clickhouse-schema-2025-03-20T13-57-59-013Z.log create mode 100644 scripts/db/db-reports/clickhouse-schema-2025-03-31T06-07-06-639Z.log diff --git a/app/(swagger)/swagger/page.tsx b/app/(swagger)/swagger/page.tsx index 8c232dc..c73ef64 100644 --- a/app/(swagger)/swagger/page.tsx +++ b/app/(swagger)/swagger/page.tsx @@ -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' + } }, - pagination: { - $ref: '#/components/schemas/Pagination', - }, - }, + meta: { + type: 'object', + properties: { + total: { + type: 'number', + description: 'Total number of events matching the filters' + }, + 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: { diff --git a/app/api/events/route.ts b/app/api/events/route.ts index 6a43d03..c4819ad 100644 --- a/app/api/events/route.ts +++ b/app/api/events/route.ts @@ -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 = { + // 时间范围参数现在是可选的 + ...(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 = { 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 = { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' diff --git a/app/api/events/tags/route.ts b/app/api/events/tags/route.ts new file mode 100644 index 0000000..a61fdb5 --- /dev/null +++ b/app/api/events/tags/route.ts @@ -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 = {}; + + // 如果提供了 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 } + ); + } +} \ No newline at end of file diff --git a/lib/analytics.ts b/lib/analytics.ts index 168c8b2..4242c6e 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -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): 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(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 + }; } // 获取事件概览 diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index f990b57..8b5e4f3 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -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): string { const filters = []; @@ -38,32 +43,45 @@ export function buildFilter(params: Partial): 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 ')}` : ''; diff --git a/lib/types.ts b/lib/types.ts index 167e6c5..875c244 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -33,6 +33,14 @@ export interface ApiResponse { 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; + // 事件基础信息 export interface Event { event_id: string; event_time: string; event_type: EventType; - event_attributes: Record; + 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; + 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; + user_attributes: AttributesRecord; // 团队信息 team_id: string; team_name: string; - team_attributes: Record; + team_attributes: AttributesRecord; // 项目信息 project_id: string; project_name: string; - project_attributes: Record; + project_attributes: AttributesRecord; // 访问者信息 visitor_id: string; diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..e0820f8 --- /dev/null +++ b/next.config.js @@ -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; \ No newline at end of file diff --git a/package.json b/package.json index 9b91751..73a8cb9 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e930d6f..25a6170 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/scripts/db/db-reports/clickhouse-schema-2025-03-20T13-57-59-013Z.log b/scripts/db/db-reports/clickhouse-schema-2025-03-20T13-57-59-013Z.log deleted file mode 100644 index 05c9b73..0000000 --- a/scripts/db/db-reports/clickhouse-schema-2025-03-20T13-57-59-013Z.log +++ /dev/null @@ -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数据库结构检查完成 diff --git a/scripts/db/db-reports/clickhouse-schema-2025-03-31T06-07-06-639Z.log b/scripts/db/db-reports/clickhouse-schema-2025-03-31T06-07-06-639Z.log new file mode 100644 index 0000000..9874634 --- /dev/null +++ b/scripts/db/db-reports/clickhouse-schema-2025-03-31T06-07-06-639Z.log @@ -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数据库结构检查完成