Documents 提供多租户知识库、树形分类、版本化文档、发布与归档、标签、访问规则和白板模型。它的内容聚合与版本快照已经可以支撑后台知识管理,但访问规则、成员、搜索索引、文件转换和实时协作仍有明显的“模型已经存在,产品闭环尚未形成”现象。
1. 模块要解决的问题
Documents 把“可编辑文本”提升为具有业务生命周期的内容资产:
- 知识库组织内容域,分类与父文档提供导航层级;
Document保存正文、元数据、状态和完整版本快照;- 发布、取消发布、归档和回滚由聚合方法保护;
- 标签、访问规则、知识库成员和白板成员作为聚合内 JSON 集合保存;
- QueryShape 读模型提供详情、列表与树查询;
- 内存搜索、文档缓存、文件服务、云盘端口和协作端口提供扩展接缝。
它不负责二进制文件资产安全、通用搜索引擎或评论线程。Files、Search、Comments 是独立模块;当前 Documents 内部的同名服务不能替代这些平台模块。
2. 当前结构
三个项目的职责分配如下:
| 项目 | 主要内容 | 依赖方向 |
|---|---|---|
BitzOrcas.Platform.Documents.Contracts | 聚合、DTO、权限/功能目录、查询端口、事件、云盘/协作端口 | 不暴露 ORM 或连接器 SDK |
BitzOrcas.Platform.Documents.Application | 公开命令与查询、处理器、映射、内存搜索/缓存/文件服务 | 依赖 Contracts 与框架抽象 |
BitzOrcas.Platform.Documents.Infrastructure | QueryShape 读模型、协作默认实现、云盘适配器、索引订阅方 | 依赖 Application/Contracts 与连接器 |
3. 聚合关系不是外键完整性
图中的关系是领域标识关系,不等于所有入口都做了引用校验。当前 CreateDocument 不验证知识库、分类或父文档是否存在,也不验证分类是否属于指定知识库;CreateWhiteboard 不验证可选 KnowledgeBaseId;CreateKnowledgeBase 不验证 ParentId 或环。分类创建只在有 ParentId 时验证父分类属于同一知识库。
4. HTTP 表面
文档与版本
| 方法与路由 | 用例 | 通用动作 |
|---|---|---|
POST /api/v1/documents/ | 创建 Draft 和 1.0.0 | Create |
GET /api/v1/documents | 租户列表,最多 100/页 | 仅认证 |
GET /api/v1/documents/{id} | 完整详情 | 仅认证 |
PUT /api/v1/documents/{id} | 更新元信息 | Update |
PUT /api/v1/documents/{id}/content | 新建 Patch 版本 | Update |
GET /api/v1/documents/{id}/versions | 版本列表 | 仅认证 |
GET /api/v1/documents/{id}/versions/{version} | 单版本 | 仅认证 |
GET /api/v1/documents/{id}/versions/diff | 简化行级差异 | 仅认证 |
POST /api/v1/documents/{id}/versions/{version}/rollback | 以旧内容创建新 Patch | Update |
POST /api/v1/documents/{id}/publish | Draft → Published | Update |
POST /api/v1/documents/{id}/unpublish | Published → Draft | Update |
POST /api/v1/documents/{id}/archive | Published → Archived | Update |
POST/DELETE .../tags | 增删标签 | Update |
POST/DELETE .../access-rules | 增删 ACL 数据 | Update |
DELETE /api/v1/documents/{id} | 软删除 | Delete |
“仅认证”表示消息没有实现 IAuthorizedRequest;端点仍受默认认证约束,但不会经过框架的资源动作授权决策。
知识库、分类与白板
| 资源 | 创建/写入 | 读取 | 当前注意点 |
|---|---|---|---|
| 知识库 | /api/v1/knowledgebases、/{id}、/{id}/members | detail/list/tree | SetKnowledgeBaseAccessCommand 没有生成端点;读取只做租户过滤 |
| 分类 | /api/v1/documents/categories/、/{id} | /tree?knowledgeBaseId= | 权限目录没有 category 权限定义;树构建无环检测 |
| 白板 | /api/v1/whiteboards、/{id}/data、成员、归档、删除 | detail/list | 权限目录没有 whiteboard 权限定义;成员不参与授权 |
搜索与建议查询实现了 IAuthorizedRequest,却没有 [GenerateEndpoint],所以它们目前不是 HTTP 表面。协作服务、文档文件服务和云盘端口也没有本模块公开端点。
5. 文档生命周期
当前聚合没有 Draft → Archived,也没有 Archived → Draft/Published。归档会阻止正文、元信息、发布、取消发布、新增标签和新增访问规则;但移除标签、移除访问规则以及成员类操作的规则并不一致,不能把 Archived 理解成全字段不可变。
每个版本是完整正文快照,不是增量补丁。版本包含 VersionNumber、Content、SHA-256、UTF-8 字节数、变更摘要、创建人/时间和发布标记。普通更新只增加 Patch,因此 1.0.0 之后是 1.0.1;当前没有 Major/Minor 升级入口。
6. 从创建到发布的客户端示例
public async Task PublishArticleAsync( HttpClient api, string knowledgeBaseId, CancellationToken cancellationToken){ // 1. 创建时正文与第一个完整快照一并写入;调用者必须是 User。 using var create = await api.PostAsJsonAsync( "/api/v1/documents/", new { knowledgeBaseId, categoryId = (string?)null, parentId = (string?)null, title = "订单取消操作手册", description = "面向客服和运营的处置步骤", content = "# 订单取消\n\n先核对支付状态。", contentType = "Markdown", languageCode = "zh-CN", allowComments = true, allowCollaboration = false }, cancellationToken);
create.EnsureSuccessStatusCode(); var document = await create.Content.ReadFromJsonAsync<DocumentDto>(cancellationToken);
// 2. 每次内容更新都会保存完整快照并把 Patch 加一。 using var update = await api.PutAsJsonAsync( $"/api/v1/documents/{document!.DocumentId}/content", new { content = "# 订单取消\n\n1. 核对支付状态。\n2. 记录取消原因。", changeSummary = "补充审计要求" }, cancellationToken); update.EnsureSuccessStatusCode();
// 3. Publish 请求没有 expectedVersion;并发编辑窗口需由客户端自己收敛。 using var publish = await api.PostAsync( $"/api/v1/documents/{document.DocumentId}/publish", content: null, cancellationToken); publish.EnsureSuccessStatusCode();}
public sealed record DocumentDto(string DocumentId, string CurrentVersion);这段代码展示真实路由和当前并发边界。生产编辑器应在保存前重新读取 CurrentVersion,并在服务端增加显式 expectedVersion/并发令牌;仅靠客户端比较不能消除竞态。
7. 权限、功能与租户的真实矩阵
| 层 | 已有事实 | 当前缺口 |
|---|---|---|
| 认证 | 生成端点默认要求认证 | 不代表资源级可见性 |
| 通用授权 | 29 个写命令实现 IAuthorizedRequest | 公开 GET 查询没有实现;搜索查询没有 HTTP 路由 |
| 权限目录 | document 6 项、knowledgebase 4 项 | category/whiteboard 资源常量存在,但没有权限定义 |
| 动作映射 | create/update/delete/view 可派生 | Publish、标签、ACL 都使用 Update;publish/manage 目录项没有对应动作 |
| ACL/成员 | 数据可创建、删除、序列化 | 没有进入 handler/read model 的准入判断 |
| Feature | documents.manage,默认关闭 | Documents 源码未见运行时 Feature 决策 |
| 租户 | 文档/知识库/分类/白板读写按 User.TenantId | 未使用 EffectiveTenantId;协作会话读模型甚至没有租户谓词 |
在这些差距修复前,不要向外承诺“私有知识库”“按成员可见”“文档 ACL”或“功能套餐已生效”。如果业务必须上线,应在网关/应用层增加失败关闭的访问策略,并用契约测试证明所有读取与写入路径一致。
8. 持久化与计数
四个主聚合都直接标注表元数据,使用统一聚合持久化:
DocsDocument:租户 + 软删除;版本、标签、ACL 为 JSON 列;DocsKnowledgeBase:租户 + 软删除;成员为 JSON 列;DocsDocumentCategory:租户 + 软删除;同知识库、同父级、同名具有唯一索引;DocsWhiteboards:租户 + 软删除;画布 JSON 与成员 JSON 同行保存;DocsCollaborationSession:租户 + 软删除;参与者 JSON 同行保存,但当前内存协作服务不持久化它。
KnowledgeBase.DocumentCount 与 DocumentCategory.DocumentCount 有领域增减方法,却没有接入 Create/Delete Document 处理器。因此删除知识库/分类时依赖计数的判断不能作为可靠引用完整性。知识库删除会额外查询文档仓储,分类删除只看可能失真的计数。
9. 当前可以依赖与不能依赖的能力
| 能力 | 结论 |
|---|---|
| 租户内文档 CRUD、完整版本快照、发布/取消发布/归档 | 可用,但需补资源访问控制与并发策略 |
| 知识库/分类树 | 可用作导航;必须防止脏 ParentId 与环 |
| 标签、ACL、成员持久化 | 仅数据模型可用;ACL/成员不是当前安全控制 |
| 白板 JSON 版本更新 | 单请求版本冲突可检测;没有 JSON/大小校验与实时传输 |
| 搜索 | 仅内部 200 条内存搜索;无 HTTP 路由,不是共享 Search 引擎 |
| 文档缓存 | 服务已注册但业务 handler 未调用 |
| 文档上传/下载 | 内部简化服务,无端点;上传不保存,PDF/Word 下载不是格式转换 |
| 实时协作 | 单实例内存、默认 Hub 只记录日志,不可用于多实例生产 |
| 云盘 | 中性端口和 adapter 已存在;无 Documents 用例和组合根注册闭环 |
10. 阅读路径
关联章节:Authorization、Multitenancy、Files、Auditing。
11. 最小源码核对
# 列出真正生成的 HTTP 表面。rg -n '^\[GenerateEndpoint' src/Platform/Documents -g '*.cs'
# 对比授权消息与无 IAuthorizedRequest 的读查询。rg -n 'IAuthorizedRequest|IQuery<Result' \ src/Platform/Documents/BitzOrcas.Platform.Documents.Application -g '*.cs'
# 检查 ACL、成员和计数是否进入 handler;当前预期只有模型/命令命中。rg -n 'HasAccess\(|DefaultAccessLevel|IncrementDocumentCount\(|DecrementDocumentCount\(' \ src/Platform/Documents -g '*.cs'