Files
curriculum-project-hub/docs/adr/0031-filelib-recycle-bin-and-recent-visits.md
ymy d072e9ec1e feat(filelib): 老师端左栏导航:回收站 + 最近打开(ADR-0031)
- 回收站:listBin(祖先全活跃的已删顶点;管理员/直连 MANAGE 可见)、
  restore(与 D15 对称只清本节点,落审计)、purge(仅管理员,pathIds 枚举
  子树按深度降序分批硬删,绕过 self-FK RESTRICT)
- 最近打开:FileLibRecentVisit 表(filePath='' 兜底 PG 唯一索引),客户端
  成功打开后上报(VIEW 门禁,upsert 刷新),列表 20 条,D8/D15 可见性过滤
- 前端:/app 左栏(文件库/最近打开/回收站);RecentView/BinView;
  GridLibraryView 埋点 + navTarget 跳转(breadcrumb 建栈,role 已捎带)
- 测试:filelib-nav 集成 4 例;全套 79 例绿
2026-07-31 13:27:04 +08:00

2.9 KiB

ADR 0031: File Library Recycle Bin And Recent-Visit Tracking

Status

Accepted.

Context

The teacher app (/app) gains a left navigation rail with three entries: 文件库 / 最近打开 / 回收站. Two of them need semantics that no prior decision covers:

  • 回收站 (recycle bin): D15 defined soft delete (mark deletedAt on the node only; a node is invisible when any ancestor is deleted) but never defined listing, restore, or permanent deletion.
  • 最近打开 (recent visits): nothing tracks opens.

Decision

Recycle bin

  • List: shows nodes with deletedAt != null whose ancestors are all active (the topmost deleted node per branch; descendants of a deleted node are represented by it and not listed separately).
  • Visibility/auth: a bin entry is visible to (a) the website administrator, or (b) any actor holding an active MANAGE grant on the deleted node itself (grants stay live through soft delete, so this is a plain grant query — no chain walk, no inheritance; the bin is a management surface, not a browsing surface).
  • Restore clears deletedAt on that node only (D15 symmetry: delete marks one node, restore unmarks one node). The subtree becomes visible again immediately. Same auth as the list entry. Audited (folder_restore / project_restore).
  • Permanent delete (彻底删除) is website-administrator only: hard-deletes the node and its whole subtree (descendants enumerated via the pathIds materialized path, deleted deepest-first because the self-FK is ON DELETE RESTRICT), in one transaction, with one audit entry (node_purge, detail carries removed count). Grants/settings/export-jobs cascade. There is no recovery; the UI must confirm explicitly.

Recent visits

  • Model: FileLibRecentVisit(organizationId, userId, nodeId, filePath, openedAt), unique on (organizationId, userId, nodeId, filePath) with filePath defaulting to "" (Postgres unique indexes treat NULLs as distinct). filePath = "" means the visit is the node itself (drill into folder/project); non-empty means a file preview inside that project.
  • Recording is client-driven: the teacher app POSTs after a successful open (folder drill, project open, file preview). The endpoint requires VIEW on the node (D8: no VIEW → 404, leaking nothing). Upsert semantics: re-opening refreshes openedAt. No audit entries — this is a per-user read model, not a权限-sensitive mutation.
  • List: the actor's own most recent 20, openedAt desc. Entries whose node is deleted or has any deleted ancestor are filtered out (D8/D15 visibility holds on every surface). Names are read live from FileLibNode (no denormalization).

Consequences

  • No change to existing permission algebra; both features are additive surfaces.
  • The bin deliberately does not offer per-owner bins or inherited-MANAGE visibility — if real usage demands it, that is a new decision.