diff --git a/app/api/materials/facets/route.ts b/app/api/materials/facets/route.ts index a7acd8a..f41fdb5 100644 --- a/app/api/materials/facets/route.ts +++ b/app/api/materials/facets/route.ts @@ -78,6 +78,40 @@ function toMaterialTypeItems(rows: any[]) { })) } +// ===== 服务端内存缓存 ===== +// facets 的 7 条 SQL 在大库上可耗时 20 秒以上(尤其 loadMonths 的全表扫描)。 +// 相同权限上下文 + 相同筛选参数的结果在 TTL 内直接返回缓存。 +const FACETS_CACHE_TTL_MS = 5 * 60 * 1000 // 5 分钟 +const facetsCache = new Map() + +function buildFacetsCacheKey(searchParams: URLSearchParams, user: any): string { + // 以角色+部门+筛选参数为缓存键:同权限的人共享同一份结果 + const role = String(user?.role || 'USER') + const dept = String(user?.department || '') + const managed = String(user?.managedDepartment || '') + const filterKey = searchParams.toString() + return `${role}|${dept}|${managed}|${filterKey}` +} + +function getFacetsFromCache(key: string): any | null { + const entry = facetsCache.get(key) + if (!entry) return null + if (Date.now() > entry.expiresAt) { + facetsCache.delete(key) + return null + } + return entry.data +} + +function setFacetsCache(key: string, data: any) { + // 简单容量上限:最多保留 200 个不同筛选组合的缓存 + if (facetsCache.size > 200) { + const oldest = facetsCache.keys().next().value + if (oldest) facetsCache.delete(oldest) + } + facetsCache.set(key, { data, expiresAt: Date.now() + FACETS_CACHE_TTL_MS }) +} + export async function GET(request: NextRequest) { try { const session = await getServerSession(authOptions) @@ -90,6 +124,14 @@ export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams const user = session.user as any + + // 缓存命中:直接返回,跳过全部 7 条 SQL + const cacheKey = buildFacetsCacheKey(searchParams, user) + const cached = getFacetsFromCache(cacheKey) + if (cached) { + return NextResponse.json(cached) + } + const context = await createMaterialQueryContext(searchParams, user) const buildWhere = async (exclude?: MaterialFilterDimension) => @@ -148,15 +190,36 @@ export async function GET(request: NextRequest) { return { items: [], total: 0 } } - const rows = await prisma.material.findMany({ + // 用 groupBy 替代 findMany 全表扫描:只取按月聚合的计数,不拉全量 createdAt 行 + const rows = await (prisma.material.groupBy as any)({ + by: ['createdAt'], where: result.where, - select: { createdAt: true }, - orderBy: { createdAt: 'desc' }, + _count: { _all: true }, }) + // groupBy 返回去重后的 createdAt 及其计数,汇总为月度计数 + const monthMap = new Map() + let total = 0 + for (const row of rows) { + const date = new Date(row.createdAt) + const value = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}` + const count = Number(row._count?._all || 0) + monthMap.set(value, (monthMap.get(value) || 0) + count) + total += count + } + return { - items: toMonthItems(rows), - total: rows.length, + items: Array.from(monthMap.entries()) + .map(([value, count]) => { + const [, month] = value.split('-') + return { + value, + label: `${value.slice(0, 4)}年${Number.parseInt(month, 10)}月`, + count, + } + }) + .sort((a, b) => b.value.localeCompare(a.value)), + total, } } @@ -174,7 +237,7 @@ export async function GET(request: NextRequest) { ]) const materialTypeResult = await loadMaterialTypes() - return NextResponse.json({ + const responseData = { total, categories: categoryResult.items, scenes: sceneResult.items, @@ -190,7 +253,12 @@ export async function GET(request: NextRequest) { productCategory: productCategoryResult.total, month: monthResult.total, }, - }) + } + + // 写入缓存,供同权限用户在 TTL 内复用 + setFacetsCache(cacheKey, responseData) + + return NextResponse.json(responseData) } catch (error) { if (error instanceof MaterialQueryError) { return NextResponse.json( diff --git a/app/page.tsx b/app/page.tsx index d3104a7..e5db2cd 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -147,7 +147,6 @@ export default function BoardHomePage() { !pageRef.current?.querySelector('[data-hero-title]') || !pageRef.current.querySelector('[data-row-line]') || !pageRef.current.querySelector('[data-row]') || - !pageRef.current.querySelector('[data-card]') || !pageRef.current.querySelector('[data-foot]') ) { return @@ -190,20 +189,6 @@ export default function BoardHomePage() { { y: 0, autoAlpha: 1, duration: 0.55, stagger: 0.07, clearProps: 'opacity,visibility,transform' }, '-=0.5', ) - // 白卡:虚焦落位(与标题同语言) - .fromTo( - '[data-card]', - { y: 20, autoAlpha: 0, filter: 'blur(8px)' }, - { - y: 0, - autoAlpha: 1, - filter: 'blur(0px)', - duration: 0.75, - stagger: 0.12, - clearProps: 'opacity,visibility,transform,filter', - }, - '-=0.4', - ) .fromTo( '[data-foot]', { autoAlpha: 0 }, @@ -326,103 +311,6 @@ export default function BoardHomePage() { )} - {/* ===== 已上线板块的入口白卡 ===== */} - {sessionLoading ? ( -
- {Array.from({ length: 4 }).map((_, i) => ( -
- ))} -
- ) : ( -
-
-
- - 运行中 - 01 / Assets -
-

创意资产库

-

- 素材浏览、上传、分享与下载——创意资产的全部日常都在这里。 -

-
- - 进入创意资产库 - -
-
-
-
- - 已上线 - 02 / People -
-

组织管理

-

- 真实组织树、M/P 等级、汇报线与在职状态,数据源 HR 花名册。 -

-
- {isAdmin ? ( - - 进入人员架构 - - ) : ( - - 需管理员权限 - - )} -
-
-
-
- - 已上线 - 03 / Performance -
-

绩效考核

-

- 个人考核、公共看板与调研管理——任务进度和结果在同一处收敛。 -

-
- - 进入绩效考核 - -
-
-
-
- - 已上线 - 04 / Review -
-

作品过稿看板

-

- 作品提交、版本迭代与评审意见——让创意协作从上传后继续推进。 -

-
- - 进入作品过稿看板 - -
-
-
- )} - {/* ===== 微型注脚 ===== */}