DocumentStructure 不是通用“文档目录树”模块。它拥有三块能力:可复用的文件夹模板及节点快照、当前用户的资源收藏关系,以及协调 Documents 软删除对象的回收站应用端口。真正的 DocumentCategory、文档正文、知识库和回收站 Store 实现仍归 Documents。
1. 当前真实能力
已经实现:
- 租户内唯一命名的文件夹模板;
- 当前节点集合与父子临时 ID 到持久 ID 的完整重映射;
- 节点 ID 非空、同请求唯一、父节点存在、无环、最大深度 10;
- 更新时保存被替换节点集合的 AOT-safe JSON 快照;
- 模板列表、详情、创建、更新、删除、预览和应用;
- Scriban 动态名称表达式的编译和渲染;
- 当前用户的收藏新增、列表和本人删除;
- Document/Category 回收站列表、恢复和彻底删除分派;
- Store 同时约束调用租户与
ICurrentTenant.EffectiveTenantId; - SqlSugar / EF Core 共用 ORM 中立持久化元数据与测试基础。
当前没有:模板历史查询、版本 diff、回滚、发布审批、并发 ETag、应用幂等、批量原子分类创建、失败补偿、目标类型白名单、知识库实例授权、Scriban 错误报告、收藏资源解析、收藏权限目录、Purge 保留/法律留存策略和完整 HTTP 安全测试。
2. 模块结构
DocumentStructure Application 当前直接引用 Documents Application 和 Contracts;模板应用直接依赖 IRepository<DocumentCategory>。回收站方向相反:端口由 DocumentStructure Application 拥有,Documents Infrastructure 实现。前者扩大了模块耦合,后者符合 owner adapter 的组合方向。
3. 十三条 HTTP 路由
| 方法与路由 | 用途 | 实现方式 |
|---|---|---|
POST /api/v1/document-structures/templates | 创建模板 | 生成 |
GET /api/v1/document-structures/templates | 列表与筛选 | 生成 |
GET /api/v1/document-structures/templates/{id} | 模板详情 | 生成 |
PUT /api/v1/document-structures/templates/{id} | 更新与生成快照 | 生成 |
DELETE /api/v1/document-structures/templates/{id} | 软删模板、删当前节点 | 生成 |
POST /api/v1/document-structures/templates/{id}/preview | 动态名称预览 | 手写 |
POST /api/v1/document-structures/templates/{id}/apply | 创建分类树 | 手写 |
POST /api/v1/document-structures/favorites | 新增收藏 | 生成 |
GET /api/v1/document-structures/favorites | 当前用户收藏 | 生成 |
DELETE /api/v1/document-structures/favorites/{id} | 删除本人收藏 | 生成 |
GET /api/v1/document-structures/recycle-bin | 回收站分页 | 生成 |
POST /api/v1/document-structures/recycle-bin/{id}/restore | 恢复 Document/Category | 生成 |
DELETE /api/v1/document-structures/recycle-bin/{id} | 彻底删除 | 生成 |
手写路由组应用认证、userPolicy 限流和标准超时。生成请求均实现 IAuthorizedRequest,但没有显式覆写 Resource,依赖 AuthorizationResourceConventions.FromRequestType。权限目录定义模板与回收站权限,却没有 Favorite 权限常量;必须用真实 HTTP policy 测试证明约定映射,而不能只看常量存在。
4. 一条真实创建模板路径
var result = await mediator.Send(new CreateFolderTemplate.Command( Name: "案件标准目录", Description: "为每个案件创建材料与交付目录", TargetType: "KnowledgeBase", Nodes: [ // Id 是本次请求内临时标识;Store 会分配最终主键。 new("root-material", null, "材料", null, "folder", "#2563eb", null, 10), new("child-contract", "root-material", "合同", null, "file", "#475569", null, 20) ]), cancellationToken);
if (result.IsFailure) return result.Error;
// 返回节点使用持久化 Id,ParentNodeId 也已同步重映射。return result.Value!;Handler 先检查模板名称非空与租户内重名,再验证完整图,最后交给 Store 写根和节点。数据库对 (TenantId, Name) 有唯一约束,应用层“先查后写”仍可能在并发时冲突;当前没有把唯一异常映射为稳定 Conflict。
5. 应用模板的真实时序
TargetType 没有与模板 TargetType 比较,也没有决定不同 owner;无论传什么,代码都把 TargetId 当 KnowledgeBaseId 创建 DocumentCategory。Handler 没有显式验证知识库存在或当前用户可管理它。
6. 收藏不是授权
收藏表唯一键是 TenantId、UserId、TargetType、TargetId。新增只检查类型和 ID 非空,不检查类型白名单、目标存在或 owner 权限,并在 UserId 缺失时写 "0"。列表只按当前租户/用户/分组读关系,不回源过滤已删除或失权对象。
因此 FavoriteItem 只是引用,不是可访问资源,也不是目标详情。消费端必须通过 owner 的授权查询解析它;更好的 GA 契约是在模块服务端批量解析并删除/标记悬空项。
7. 回收站是跨模块协调面
IDocumentRecycleBinStore 定义在 DocumentStructure Application,真正实现位于 Documents Infrastructure。Handler 只接受 ItemType=Document 或 Category 并分派。Query 的 KnowledgeBaseId、PageIndex、PageSize 原样交给 Store。
当前返回成功不证明对象存在,也不返回受影响数量。分类的递归恢复/Purge、父级冲突、附件、索引、版本、收藏与事件如何处理,需要以 Documents Store 和集成测试为准。
8. 章节路线
- 模板节点图、版本与持久化:图不变量、ID 重映射、快照、覆盖写与版本缺口;
- 模板预览、应用与 Scriban:动态命名、创建顺序、部分成功、幂等与目标授权;
- 收藏、回收站与对象安全:引用语义、owner 回源、恢复/Purge 和跨模块边界;
- 测试、运维与商业 GA 门禁:现有证据、故障矩阵、容量、指标和发布阻断项。
9. GA 红线
- 模板应用先验证当前租户、目标类型、知识库存在和对象级管理权限;
- 整棵树使用原子批量创建,或具备可证明的补偿与恢复;
- 无法解析的节点必须失败,不能用部分 CreatedCategoryIds 冒充完整成功;
- 引入 ApplicationId 幂等键,重复与并发不重复创建分类;
- Scriban 使用真异步取消、资源限制、允许变量/函数和可见错误;
- 快照 schema/version 可读取、diff、回滚和迁移;
- 模板并发更新使用版本条件/ETag,唯一冲突类型化;
- 收藏要求真实 UserId、TargetType allowlist、owner 存在性与授权;
- 回收站读写绑定 KnowledgeBase 和具体对象权限,Purge 受保留/法律留存控制;
- 生成端点资源约定、Favorite 权限和权限目录以 HTTP 测试固定;
- 列表消除逐模板 Count 的 N+1,并建立大租户/大模板容量基线;
- 双 ORM 事务、并发、故障、恢复和运维证据进入发布门禁。
10. 源码导航
# 核对 11 条生成路由与 2 条手写路由。rg -n "GenerateEndpoint\(|MapPost\(" \ src/Platform/DocumentStructure src/Hosts/BitzOrcas.Api/Endpoints/DocumentStructureEndpoints.cs -g '*.cs'
# 暴露模板应用的直接 Documents 依赖、同步阻塞和静默回退。rg -n "IRepository<DocumentCategory>|GetAwaiter\(\)\.GetResult|CancellationToken.None|return node.Name" \ src/Platform/DocumentStructure -g '*.cs'
# 当前应无应用幂等、版本回滚与收藏 owner resolver。rg -n "ApplicationId|RollbackTemplate|FavoriteTargetResolver|RowVersion" \ src/Platform/DocumentStructure -g '*.cs'