perf: facets 服务端缓存 + loadMonths 查询优化 + 首页去重复卡片
- facets API 增加内存缓存(TTL 5 分钟,键=角色+部门+筛选参数,容量 200 条) - loadMonths 从 findMany 全表扫描改为 groupBy 按月聚合(数据库侧完成) - 首页移除下方白卡区块(与编号索引重复)+ 恢复 05-07 规划中板块行
This commit is contained in:
parent
17e23c26ae
commit
02df0b7ef4
@ -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<string, { data: any; expiresAt: number }>()
|
||||
|
||||
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<string, number>()
|
||||
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(
|
||||
|
||||
112
app/page.tsx
112
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() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ===== 已上线板块的入口白卡 ===== */}
|
||||
{sessionLoading ? (
|
||||
<div className="grid gap-6 py-16 md:grid-cols-12 md:gap-8 md:py-24">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-[260px] animate-pulse rounded-xl border border-[#e7e8ea] bg-white/60 ${i % 2 === 0 ? 'md:col-span-7' : 'md:col-span-5'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<section className="grid gap-6 py-16 md:grid-cols-12 md:gap-8 md:py-24">
|
||||
<div data-card className="rounded-xl border border-[#e7e8ea] bg-white p-8 transition-all duration-[400ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-1.5 hover:border-[#c9ccd1] md:col-span-7 md:p-10">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<StatusDot tone="live" />
|
||||
<span className="text-[12px] text-[#6b7075]">运行中</span>
|
||||
<span className={`font-en ml-auto text-[11px] uppercase tracking-[0.14em] ${labelCls}`}>01 / Assets</span>
|
||||
</div>
|
||||
<h3 className="mt-5 text-[22px] font-semibold tracking-tight">创意资产库</h3>
|
||||
<p className="mt-3 text-[14px] leading-relaxed text-[#6b7075]">
|
||||
素材浏览、上传、分享与下载——创意资产的全部日常都在这里。
|
||||
</p>
|
||||
<div className="mt-7">
|
||||
<Link
|
||||
href="/materials"
|
||||
className={`inline-flex min-h-11 items-center rounded-lg bg-[#1a1c1d] px-6 text-[13px] font-medium text-white transition-all duration-200 hover:opacity-80 active:scale-[0.98] ${focusRing}`}
|
||||
>
|
||||
进入创意资产库
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div data-card className="rounded-xl border border-[#e7e8ea] bg-white p-8 transition-all duration-[400ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-1.5 hover:border-[#c9ccd1] md:col-span-5 md:p-10">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<StatusDot tone="live" />
|
||||
<span className="text-[12px] text-[#6b7075]">已上线</span>
|
||||
<span className={`font-en ml-auto text-[11px] uppercase tracking-[0.14em] ${labelCls}`}>02 / People</span>
|
||||
</div>
|
||||
<h3 className="mt-5 text-[22px] font-semibold tracking-tight">组织管理</h3>
|
||||
<p className="mt-3 text-[14px] leading-relaxed text-[#6b7075]">
|
||||
真实组织树、M/P 等级、汇报线与在职状态,数据源 HR 花名册。
|
||||
</p>
|
||||
<div className="mt-7">
|
||||
{isAdmin ? (
|
||||
<Link
|
||||
href="/organization"
|
||||
className={`inline-flex min-h-11 items-center rounded-lg bg-[#1a1c1d] px-6 text-[13px] font-medium text-white transition-all duration-200 hover:opacity-80 active:scale-[0.98] ${focusRing}`}
|
||||
>
|
||||
进入人员架构
|
||||
</Link>
|
||||
) : (
|
||||
<span className="inline-flex min-h-11 items-center rounded-lg border border-[#e7e8ea] px-6 text-[13px] text-[#6b7075]">
|
||||
需管理员权限
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div data-card className="rounded-xl border border-[#e7e8ea] bg-white p-8 transition-all duration-[400ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-1.5 hover:border-[#c9ccd1] md:col-span-5 md:p-10">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<StatusDot tone="live" />
|
||||
<span className="text-[12px] text-[#6b7075]">已上线</span>
|
||||
<span className={`font-en ml-auto text-[11px] uppercase tracking-[0.14em] ${labelCls}`}>03 / Performance</span>
|
||||
</div>
|
||||
<h3 className="mt-5 text-[22px] font-semibold tracking-tight">绩效考核</h3>
|
||||
<p className="mt-3 text-[14px] leading-relaxed text-[#6b7075]">
|
||||
个人考核、公共看板与调研管理——任务进度和结果在同一处收敛。
|
||||
</p>
|
||||
<div className="mt-7">
|
||||
<Link
|
||||
href="/performance-survey"
|
||||
className={`inline-flex min-h-11 items-center rounded-lg bg-[#1a1c1d] px-6 text-[13px] font-medium text-white transition-all duration-200 hover:opacity-80 active:scale-[0.98] ${focusRing}`}
|
||||
>
|
||||
进入绩效考核
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div data-card className="rounded-xl border border-[#e7e8ea] bg-white p-8 transition-all duration-[400ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-1.5 hover:border-[#c9ccd1] md:col-span-7 md:p-10">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<StatusDot tone="live" />
|
||||
<span className="text-[12px] text-[#6b7075]">已上线</span>
|
||||
<span className={`font-en ml-auto text-[11px] uppercase tracking-[0.14em] ${labelCls}`}>04 / Review</span>
|
||||
</div>
|
||||
<h3 className="mt-5 text-[22px] font-semibold tracking-tight">作品过稿看板</h3>
|
||||
<p className="mt-3 text-[14px] leading-relaxed text-[#6b7075]">
|
||||
作品提交、版本迭代与评审意见——让创意协作从上传后继续推进。
|
||||
</p>
|
||||
<div className="mt-7">
|
||||
<Link
|
||||
href="/work-review"
|
||||
className={`inline-flex min-h-11 items-center rounded-lg bg-[#1a1c1d] px-6 text-[13px] font-medium text-white transition-all duration-200 hover:opacity-80 active:scale-[0.98] ${focusRing}`}
|
||||
>
|
||||
进入作品过稿看板
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ===== 微型注脚 ===== */}
|
||||
<footer data-foot className="flex flex-wrap items-center gap-x-4 gap-y-2 border-t border-[#e7e8ea] py-10">
|
||||
<span className="font-en text-[10px] uppercase tracking-[0.2em] text-[#86898e]">Ground</span>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user