init ana page with apis

This commit is contained in:
2025-03-21 12:08:37 +08:00
commit 271230fca7
71 changed files with 15699 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from 'next/server';
import { getLinkDetailsById } from '@/app/api/links/service';
export async function GET(
request: NextRequest,
context: { params: { linkId: string } }
) {
try {
const params = await context.params;
const linkId = params.linkId;
const link = await getLinkDetailsById(linkId);
if (!link) {
return NextResponse.json(
{ error: 'Link not found' },
{ status: 404 }
);
}
return NextResponse.json(link);
} catch (error) {
console.error('Failed to fetch link details:', error);
return NextResponse.json(
{ error: 'Failed to fetch link details', message: (error as Error).message },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from 'next/server';
import { getLinkById } from '../service';
export async function GET(
request: NextRequest,
{ params }: { params: { linkId: string } }
) {
try {
const { linkId } = params;
const link = await getLinkById(linkId);
if (!link) {
return NextResponse.json(
{ error: 'Link not found' },
{ status: 404 }
);
}
return NextResponse.json(link);
} catch (error) {
console.error('Failed to fetch link details:', error);
return NextResponse.json(
{ error: 'Failed to fetch link details', message: (error as Error).message },
{ status: 500 }
);
}
}

157
app/api/links/repository.ts Normal file
View File

@@ -0,0 +1,157 @@
import { executeQuery, executeQuerySingle } from '@/lib/clickhouse';
import { Link, LinkQueryParams } from '../types';
/**
* Find links with filtering options
*/
export async function findLinks({
limit = 10,
offset = 0,
searchTerm = '',
tagFilter = '',
isActive = null,
}: LinkQueryParams) {
// Build WHERE conditions
const conditions = [];
if (searchTerm) {
conditions.push(`
(lower(title) LIKE lower('%${searchTerm}%') OR
lower(original_url) LIKE lower('%${searchTerm}%'))
`);
}
if (tagFilter) {
conditions.push(`hasAny(tags, ['${tagFilter}'])`);
}
if (isActive !== null) {
conditions.push(`is_active = ${isActive ? 'true' : 'false'}`);
}
const whereClause = conditions.length > 0
? `WHERE ${conditions.join(' AND ')}`
: '';
// Get total count
const countQuery = `
SELECT count() as total
FROM links
${whereClause}
`;
const countData = await executeQuery<{ total: number }>(countQuery);
const total = countData.length > 0 ? countData[0].total : 0;
// 使用左连接获取链接数据和统计信息
const linksQuery = `
SELECT
l.link_id,
l.original_url,
l.created_at,
l.created_by,
l.title,
l.description,
l.tags,
l.is_active,
l.expires_at,
l.team_id,
l.project_id,
count(le.event_id) as visits,
count(DISTINCT le.visitor_id) as unique_visits
FROM links l
LEFT JOIN link_events le ON l.link_id = le.link_id
${whereClause}
GROUP BY
l.link_id,
l.original_url,
l.created_at,
l.created_by,
l.title,
l.description,
l.tags,
l.is_active,
l.expires_at,
l.team_id,
l.project_id
ORDER BY l.created_at DESC
LIMIT ${limit}
OFFSET ${offset}
`;
const links = await executeQuery<Link>(linksQuery);
return {
links,
total,
limit,
offset,
page: Math.floor(offset / limit) + 1,
totalPages: Math.ceil(total / limit)
};
}
/**
* Find a single link by ID
*/
export async function findLinkById(linkId: string): Promise<Link | null> {
const query = `
SELECT
l.link_id,
l.original_url,
l.created_at,
l.created_by,
l.title,
l.description,
l.tags,
l.is_active,
l.expires_at,
l.team_id,
l.project_id,
count(le.event_id) as visits,
count(DISTINCT le.visitor_id) as unique_visits
FROM links l
LEFT JOIN link_events le ON l.link_id = le.link_id
WHERE l.link_id = '${linkId}'
GROUP BY
l.link_id,
l.original_url,
l.created_at,
l.created_by,
l.title,
l.description,
l.tags,
l.is_active,
l.expires_at,
l.team_id,
l.project_id
LIMIT 1
`;
return await executeQuerySingle<Link>(query);
}
/**
* Find a single link by ID - only basic info without statistics
*/
export async function findLinkDetailsById(linkId: string): Promise<Omit<Link, 'visits' | 'unique_visits'> | null> {
const query = `
SELECT
link_id,
original_url,
created_at,
created_by,
title,
description,
tags,
is_active,
expires_at,
team_id,
project_id
FROM links
WHERE link_id = '${linkId}'
LIMIT 1
`;
return await executeQuerySingle<Omit<Link, 'visits' | 'unique_visits'>>(query);
}

32
app/api/links/route.ts Normal file
View File

@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from 'next/server';
import { LinkQueryParams } from '../types';
import { getLinks } from './service';
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
// Parse request parameters
const params: LinkQueryParams = {
limit: searchParams.has('limit') ? Number(searchParams.get('limit')) : 10,
page: searchParams.has('page') ? Number(searchParams.get('page')) : 1,
searchTerm: searchParams.get('search') || '',
tagFilter: searchParams.get('tag') || '',
};
// Handle active status filter
const activeFilter = searchParams.get('active');
if (activeFilter === 'true') params.isActive = true;
if (activeFilter === 'false') params.isActive = false;
// Get link data
const result = await getLinks(params);
return NextResponse.json(result);
} catch (error) {
console.error('Failed to fetch links:', error);
return NextResponse.json(
{ error: 'Failed to fetch links', message: (error as Error).message },
{ status: 500 }
);
}
}

42
app/api/links/service.ts Normal file
View File

@@ -0,0 +1,42 @@
import { Link, LinkQueryParams, PaginatedResponse } from '../types';
import { findLinkById, findLinkDetailsById, findLinks } from './repository';
/**
* Get links with pagination information
*/
export async function getLinks(params: LinkQueryParams): Promise<PaginatedResponse<Link>> {
// Convert page number to offset
const { page, limit = 10, ...otherParams } = params;
const offset = page ? (page - 1) * limit : params.offset || 0;
const result = await findLinks({
...otherParams,
limit,
offset
});
return {
data: result.links,
pagination: {
total: result.total,
limit: result.limit,
offset: result.offset,
page: result.page,
totalPages: result.totalPages
}
};
}
/**
* Get a single link by ID with full details (including statistics)
*/
export async function getLinkById(linkId: string): Promise<Link | null> {
return await findLinkById(linkId);
}
/**
* Get a single link by ID - only basic info without statistics
*/
export async function getLinkDetailsById(linkId: string): Promise<Omit<Link, 'visits' | 'unique_visits'> | null> {
return await findLinkDetailsById(linkId);
}