From 746e89ec63cce37dabe341fb7dabcfba77cbc6c2 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sat, 18 Apr 2026 21:34:58 +0800 Subject: [PATCH] feat(server/posts): public preview and paginated list by slug Made-with: Cursor --- server/api/public/profile/[publicSlug].get.ts | 6 +- server/service/posts/index.ts | 102 +++++++++++++++++++++----- 2 files changed, 85 insertions(+), 23 deletions(-) diff --git a/server/api/public/profile/[publicSlug].get.ts b/server/api/public/profile/[publicSlug].get.ts index d56148b..2a48564 100644 --- a/server/api/public/profile/[publicSlug].get.ts +++ b/server/api/public/profile/[publicSlug].get.ts @@ -1,7 +1,7 @@ import { dbGlobal } from "drizzle-pkg/lib/db"; import { users } from "drizzle-pkg/lib/schema/auth"; import { and, eq } from "drizzle-orm"; -import { listPublicPostsBySlug } from "#server/service/posts"; +import { getPublicPostsPreviewBySlug } from "#server/service/posts"; import { listPublicTimelineBySlug } from "#server/service/timeline"; import { listPublicRssItemsBySlug } from "#server/service/rss"; import { parseSocialLinksJson } from "#server/service/profile"; @@ -32,7 +32,7 @@ export default defineEventHandler(async (event) => { }; bio: { markdown: string | null } | null; links: typeof links; - posts: Awaited>; + posts: Awaited>; timeline: Awaited>; rssItems: Awaited>; } = { @@ -43,7 +43,7 @@ export default defineEventHandler(async (event) => { }, bio: null, links, - posts: await listPublicPostsBySlug(publicSlug), + posts: await getPublicPostsPreviewBySlug(publicSlug), timeline: await listPublicTimelineBySlug(publicSlug), rssItems: await listPublicRssItemsBySlug(publicSlug), }; diff --git a/server/service/posts/index.ts b/server/service/posts/index.ts index ba183ec..2e144b5 100644 --- a/server/service/posts/index.ts +++ b/server/service/posts/index.ts @@ -2,8 +2,10 @@ import { dbGlobal } from "drizzle-pkg/lib/db"; import { postMediaRefs, posts } from "drizzle-pkg/lib/schema/content"; import { reconcileAssetTimestampsAfterRefChange, syncPostMediaRefs } from "#server/service/media"; import { users } from "drizzle-pkg/lib/schema/auth"; -import { and, desc, eq } from "drizzle-orm"; +import { and, count, desc, eq } from "drizzle-orm"; +import { PUBLIC_LIST_PAGE_SIZE, PUBLIC_PREVIEW_LIMIT } from "#server/constants/public-profile-lists"; import { visibilitySchema, type Visibility } from "#server/constants/visibility"; +import { normalizePublicListPage } from "#server/utils/public-pagination"; import { visibilityShareToken } from "#server/utils/share-token"; import { nextIntegerId } from "#server/utils/sqlite-id"; @@ -128,25 +130,85 @@ export async function deletePost(userId: number, id: number) { return true; } -export async function listPublicPostsBySlug(publicSlug: string) { - return dbGlobal - .select({ - title: posts.title, - excerpt: posts.excerpt, - slug: posts.slug, - coverUrl: posts.coverUrl, - publishedAt: posts.publishedAt, - }) - .from(posts) - .innerJoin(users, eq(posts.userId, users.id)) - .where( - and( - eq(users.publicSlug, publicSlug), - eq(users.status, "active"), - eq(posts.visibility, "public"), - ), - ) - .orderBy(desc(posts.publishedAt), desc(posts.id)); +const publicPostsListSelect = { + title: posts.title, + excerpt: posts.excerpt, + slug: posts.slug, + coverUrl: posts.coverUrl, + publishedAt: posts.publishedAt, +} as const; + +function publicPostsListWhere(publicSlug: string) { + return and( + eq(users.publicSlug, publicSlug), + eq(users.status, "active"), + eq(posts.visibility, "public"), + ); +} + +export async function getPublicPostsPreviewBySlug(publicSlug: string): Promise<{ + items: Array<{ + title: string; + excerpt: string; + slug: string; + coverUrl: string | null; + publishedAt: Date | null; + }>; + total: number; +}> { + const whereClause = publicPostsListWhere(publicSlug); + const [countRows, items] = await Promise.all([ + dbGlobal + .select({ total: count() }) + .from(posts) + .innerJoin(users, eq(posts.userId, users.id)) + .where(whereClause), + dbGlobal + .select(publicPostsListSelect) + .from(posts) + .innerJoin(users, eq(posts.userId, users.id)) + .where(whereClause) + .orderBy(desc(posts.publishedAt), desc(posts.id)) + .limit(PUBLIC_PREVIEW_LIMIT), + ]); + return { items, total: countRows[0]?.total ?? 0 }; +} + +export async function getPublicPostsPageBySlug( + publicSlug: string, + pageRaw: unknown, +): Promise<{ + items: Array<{ + title: string; + excerpt: string; + slug: string; + coverUrl: string | null; + publishedAt: Date | null; + }>; + total: number; + page: number; + pageSize: number; +}> { + const page = normalizePublicListPage(pageRaw); + const pageSize = PUBLIC_LIST_PAGE_SIZE; + const offset = (page - 1) * pageSize; + const whereClause = publicPostsListWhere(publicSlug); + const [countRows, items] = await Promise.all([ + dbGlobal + .select({ total: count() }) + .from(posts) + .innerJoin(users, eq(posts.userId, users.id)) + .where(whereClause), + dbGlobal + .select(publicPostsListSelect) + .from(posts) + .innerJoin(users, eq(posts.userId, users.id)) + .where(whereClause) + .orderBy(desc(posts.publishedAt), desc(posts.id)) + .limit(pageSize) + .offset(offset), + ]); + return { items, total: countRows[0]?.total ?? 0, page, pageSize }; } export async function getPublicPostByPublicSlugAndSlug(publicSlug: string, postSlug: string) {