links search
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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'
|
||||
|
||||
99
app/api/events/tags/route.ts
Normal file
99
app/api/events/tags/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user