feat(filelib): 移除最近打开模块(ADR-0032,supersede ADR-0031 对应半部)

- FileLibRecentVisit 删表(手写迁移;表当日新建无生产数据)
- recentService/recentRoutes/RecentView 删除;GridLibraryView 埋点与
  navTarget 跳转一并移除;types 清 RecentEntry
- 左栏保留 文件库/回收站(ADR-0031 回收站半部不受影响)
- breadcrumb 的 role 字段保留(独立可用的增量字段)
This commit is contained in:
ymy
2026-07-31 13:39:18 +08:00
parent 475b397ca8
commit 83a6b012b7
11 changed files with 38 additions and 366 deletions
-106
View File
@@ -1,106 +0,0 @@
/**
* 最近打开(ADR-0031)。
*
* 记录:客户端在成功打开后上报;node 需 VIEW(D8,无权即 404 不泄露);
* upsert 语义 —— 重复打开只刷新 openedAt。不写审计(按用户的读模型,
* 非权限敏感写)。
* 列表:本人最近 20 条,openedAt 倒序;节点已删或**任一祖先已删**的条目
* 过滤掉(D8/D15 可见性在每个面都成立);名称 join FileLibNode 实时取。
*/
import type { PrismaClient } from "@prisma/client";
import { requireAccessInTx, type AccessDeps, type FileLibActor } from "./treeService.js";
export type RecentDeps = AccessDeps & { readonly prisma: PrismaClient };
export interface RecentEntryDto {
readonly nodeId: string;
readonly kind: "FOLDER" | "PROJECT";
readonly name: string;
/** "" = 节点本身;非空 = 项目内文件路径。 */
readonly filePath: string;
readonly openedAt: Date;
}
const RECENT_LIMIT = 20;
export async function recordVisit(
deps: RecentDeps,
actor: FileLibActor,
nodeId: string,
filePath: string | undefined,
): Promise<void> {
const path = filePath ?? "";
await deps.prisma.$transaction(async (tx) => requireAccessInTx(tx, deps, actor, nodeId, "VIEW"));
await deps.prisma.fileLibRecentVisit.upsert({
where: {
organizationId_userId_nodeId_filePath: {
organizationId: deps.organizationId,
userId: actor.userId,
nodeId,
filePath: path,
},
},
update: { openedAt: new Date() },
create: {
organizationId: deps.organizationId,
userId: actor.userId,
nodeId,
filePath: path,
openedAt: new Date(),
},
});
}
export async function listRecent(deps: RecentDeps, actor: FileLibActor): Promise<readonly RecentEntryDto[]> {
// 可见性过滤会丢弃一部分,超取再截断。
const rows = await deps.prisma.fileLibRecentVisit.findMany({
where: { organizationId: deps.organizationId, userId: actor.userId },
orderBy: { openedAt: "desc" },
take: RECENT_LIMIT * 3,
});
if (rows.length === 0) return [];
const nodeIds = [...new Set(rows.map((r) => r.nodeId))];
const nodes = await deps.prisma.fileLibNode.findMany({
where: { id: { in: nodeIds } },
select: { id: true, kind: true, name: true, pathIds: true, deletedAt: true },
});
const byId = new Map(nodes.map((n) => [n.id, n]));
// 祖先活跃性:收集所有节点的祖先段,查已删集合。
const ancestorIds = new Set<string>();
for (const n of nodes) {
for (const s of n.pathIds.split("/").filter((x) => x !== "" && x !== n.id)) ancestorIds.add(s);
}
const deletedAncestorIds = new Set(
ancestorIds.size === 0
? []
: (
await deps.prisma.fileLibNode.findMany({
where: { id: { in: [...ancestorIds] }, deletedAt: { not: null } },
select: { id: true },
})
).map((r) => r.id),
);
const out: RecentEntryDto[] = [];
for (const row of rows) {
if (out.length >= RECENT_LIMIT) break;
const node = byId.get(row.nodeId);
if (node === undefined || node.deletedAt !== null) continue;
const hidden = node.pathIds
.split("/")
.filter((s) => s !== "" && s !== node.id)
.some((s) => deletedAncestorIds.has(s));
if (hidden) continue;
out.push({
nodeId: node.id,
kind: node.kind,
name: node.name,
filePath: row.filePath,
openedAt: row.openedAt,
});
}
return out;
}