diff --git a/frontend/src/pages/api/increment-views.ts b/frontend/src/pages/api/increment-views.ts index 83f5da4..cf4f247 100644 --- a/frontend/src/pages/api/increment-views.ts +++ b/frontend/src/pages/api/increment-views.ts @@ -1,10 +1,23 @@ import type { APIRoute } from 'astro'; +import crypto from 'crypto'; const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090'; const POCKETBASE_ID_REGEX = /^[a-z0-9]{15}$/; -export const POST: APIRoute = async ({ url }) => { +function getClientIp(request: Request): string { + const forwarded = request.headers.get('x-forwarded-for'); + if (forwarded) { + return forwarded.split(',')[0].trim(); + } + return request.headers.get('x-real-ip') || 'unknown'; +} + +function generateVisitorHash(ip: string, userAgent: string): string { + return crypto.createHash('sha256').update(ip + userAgent).digest('hex').slice(0, 32); +} + +export const POST: APIRoute = async ({ request, url }) => { try { const postId = url.searchParams.get('postId'); @@ -27,19 +40,62 @@ export const POST: APIRoute = async ({ url }) => { } const post = await postRes.json(); - const newViews = (post.views || 0) + 1; - await fetch( - `${POCKETBASE_URL}/api/collections/posts/records/${postId}`, - { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ views: newViews }), - } + const ip = getClientIp(request); + const userAgent = request.headers.get('user-agent') || 'unknown'; + const visitorHash = generateVisitorHash(ip, userAgent); + + const now = new Date(); + const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); + const yesterdayStr = yesterday.toISOString(); + + const existingViewRes = await fetch( + `${POCKETBASE_URL}/api/collections/post_views/records?` + + new URLSearchParams({ + filter: `post="${postId}" && visitor_hash="${visitorHash}" && created >= "${yesterdayStr}"`, + }) ); + let isNewView = false; + + if (existingViewRes.ok) { + const existingData = await existingViewRes.json(); + if (existingData.items?.length === 0) { + isNewView = true; + + await fetch( + `${POCKETBASE_URL}/api/collections/post_views/records`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + post: postId, + visitor_hash: visitorHash, + ip: ip, + user_agent: userAgent, + }), + } + ); + } + } + + let totalViews = post.views || 0; + + if (isNewView) { + totalViews += 1; + + await fetch( + `${POCKETBASE_URL}/api/collections/posts/records/${postId}`, + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ views: totalViews }), + } + ); + } + return new Response( - JSON.stringify({ views: newViews }), + JSON.stringify({ views: totalViews, isNewView }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); @@ -50,4 +106,4 @@ export const POST: APIRoute = async ({ url }) => { { status: 500, headers: { 'Content-Type': 'application/json' } } ); } -}; \ No newline at end of file +};